Skip to content

Commit a5ec55c

Browse files
authored
feat: collection-level disableBulkEdit (#12850)
1 parent 11ac230 commit a5ec55c

File tree

12 files changed

+113
-4
lines changed

12 files changed

+113
-4
lines changed

docs/configuration/collections.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ The following options are available:
8585
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
8686
| `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. |
8787
| `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 |
88+
| `disableBulkEdit` | Disable the bulk edit operation for the collection in the admin panel and the REST API |
8889

8990
_\* An asterisk denotes that a property is required._
9091

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ export const renderListView = async (
274274
collectionSlug,
275275
columnState,
276276
disableBulkDelete,
277-
disableBulkEdit,
277+
disableBulkEdit: collectionConfig.disableBulkEdit ?? disableBulkEdit,
278278
disableQueryPresets,
279279
enableRowSelections,
280280
hasCreatePermission,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,10 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
440440
* Default field to sort by in collection list view
441441
*/
442442
defaultSort?: Sort
443+
/**
444+
* Disable the bulk edit operation for the collection in the admin panel and the API
445+
*/
446+
disableBulkEdit?: boolean
443447
/**
444448
* When true, do not show the "Duplicate" button while editing documents within this collection and prevent `duplicate` from all APIs
445449
*/

packages/payload/src/collections/operations/update.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {
1515
import executeAccess from '../../auth/executeAccess.js'
1616
import { combineQueries } from '../../database/combineQueries.js'
1717
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
18-
import { APIError } from '../../errors/index.js'
18+
import { APIError, Forbidden } from '../../errors/index.js'
1919
import { type CollectionSlug, deepCopyObjectSimple } from '../../index.js'
2020
import { generateFileData } from '../../uploads/generateFileData.js'
2121
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles.js'
@@ -63,6 +63,10 @@ export const updateOperation = async <
6363
): Promise<BulkOperationResult<TSlug, TSelect>> => {
6464
let args = incomingArgs
6565

66+
if (args.collection.config.disableBulkEdit && !args.overrideAccess) {
67+
throw new APIError(`Collection ${args.collection.config.slug} has disabled bulk edit`, 403)
68+
}
69+
6670
try {
6771
const shouldCommit = !args.disableTransaction && (await initTransaction(args.req))
6872

packages/ui/src/views/CollectionFolder/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ function CollectionFolderViewInContext(props: CollectionFolderViewInContextProps
264264
!smallBreak && (
265265
<ListSelection
266266
disableBulkDelete={disableBulkDelete}
267-
disableBulkEdit={disableBulkEdit}
267+
disableBulkEdit={collectionConfig.disableBulkEdit ?? disableBulkEdit}
268268
key="list-selection"
269269
/>
270270
),
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
export const DisableBulkEdit: CollectionConfig = {
4+
slug: 'disable-bulk-edit',
5+
fields: [],
6+
disableBulkEdit: true,
7+
}

test/admin/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable no-restricted-exports */
21
import { fileURLToPath } from 'node:url'
32
import path from 'path'
43

@@ -8,6 +7,7 @@ import { BaseListFilter } from './collections/BaseListFilter.js'
87
import { CustomFields } from './collections/CustomFields/index.js'
98
import { CustomViews1 } from './collections/CustomViews1.js'
109
import { CustomViews2 } from './collections/CustomViews2.js'
10+
import { DisableBulkEdit } from './collections/DisableBulkEdit.js'
1111
import { DisableCopyToLocale } from './collections/DisableCopyToLocale.js'
1212
import { DisableDuplicate } from './collections/DisableDuplicate.js'
1313
import { EditMenuItems } from './collections/editMenuItems.js'
@@ -180,6 +180,7 @@ export default buildConfigWithDefaults({
180180
ListDrawer,
181181
Placeholder,
182182
UseAsTitleGroupField,
183+
DisableBulkEdit,
183184
],
184185
globals: [
185186
GlobalHidden,

test/admin/e2e/list-view/e2e.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ describe('List View', () => {
6666
let with300DocumentsUrl: AdminUrlUtil
6767
let withListViewUrl: AdminUrlUtil
6868
let placeholderUrl: AdminUrlUtil
69+
let disableBulkEditUrl: AdminUrlUtil
6970
let user: any
7071

7172
let serverURL: string
@@ -90,6 +91,7 @@ describe('List View', () => {
9091
customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug)
9192
withListViewUrl = new AdminUrlUtil(serverURL, listDrawerSlug)
9293
placeholderUrl = new AdminUrlUtil(serverURL, placeholderCollectionSlug)
94+
disableBulkEditUrl = new AdminUrlUtil(serverURL, 'disable-bulk-edit')
9395
const context = await browser.newContext()
9496
page = await context.newPage()
9597
initPageConsoleErrorCatch(page)
@@ -1302,6 +1304,16 @@ describe('List View', () => {
13021304
await page.locator('#confirm-delete-many-docs #confirm-action').click()
13031305
await expect(page.locator('.cell-_select')).toHaveCount(1)
13041306
})
1307+
1308+
test('should hide edit many from collection with disableBulkEdit: true', async () => {
1309+
await payload.create({ collection: 'disable-bulk-edit', data: {} })
1310+
await page.goto(disableBulkEditUrl.list)
1311+
1312+
// select one row
1313+
await page.locator('.row-1 .cell-_select input').check()
1314+
// ensure the edit many button is hidden
1315+
await expect(page.locator('.edit-many button')).toBeHidden()
1316+
})
13051317
})
13061318

13071319
describe('pagination', () => {

test/admin/payload-types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export interface Config {
9292
'with-list-drawer': WithListDrawer;
9393
placeholder: Placeholder;
9494
'use-as-title-group-field': UseAsTitleGroupField;
95+
'disable-bulk-edit': DisableBulkEdit;
9596
'payload-locked-documents': PayloadLockedDocument;
9697
'payload-preferences': PayloadPreference;
9798
'payload-migrations': PayloadMigration;
@@ -123,6 +124,7 @@ export interface Config {
123124
'with-list-drawer': WithListDrawerSelect<false> | WithListDrawerSelect<true>;
124125
placeholder: PlaceholderSelect<false> | PlaceholderSelect<true>;
125126
'use-as-title-group-field': UseAsTitleGroupFieldSelect<false> | UseAsTitleGroupFieldSelect<true>;
127+
'disable-bulk-edit': DisableBulkEditSelect<false> | DisableBulkEditSelect<true>;
126128
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
127129
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
128130
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -547,6 +549,15 @@ export interface UseAsTitleGroupField {
547549
updatedAt: string;
548550
createdAt: string;
549551
}
552+
/**
553+
* This interface was referenced by `Config`'s JSON-Schema
554+
* via the `definition` "disable-bulk-edit".
555+
*/
556+
export interface DisableBulkEdit {
557+
id: string;
558+
updatedAt: string;
559+
createdAt: string;
560+
}
550561
/**
551562
* This interface was referenced by `Config`'s JSON-Schema
552563
* via the `definition` "payload-locked-documents".
@@ -653,6 +664,10 @@ export interface PayloadLockedDocument {
653664
| ({
654665
relationTo: 'use-as-title-group-field';
655666
value: string | UseAsTitleGroupField;
667+
} | null)
668+
| ({
669+
relationTo: 'disable-bulk-edit';
670+
value: string | DisableBulkEdit;
656671
} | null);
657672
globalSlug?: string | null;
658673
user: {
@@ -1037,6 +1052,14 @@ export interface UseAsTitleGroupFieldSelect<T extends boolean = true> {
10371052
updatedAt?: T;
10381053
createdAt?: T;
10391054
}
1055+
/**
1056+
* This interface was referenced by `Config`'s JSON-Schema
1057+
* via the `definition` "disable-bulk-edit_select".
1058+
*/
1059+
export interface DisableBulkEditSelect<T extends boolean = true> {
1060+
updatedAt?: T;
1061+
createdAt?: T;
1062+
}
10401063
/**
10411064
* This interface was referenced by `Config`'s JSON-Schema
10421065
* via the `definition` "payload-locked-documents_select".

test/collections-rest/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,16 @@ export default buildConfigWithDefaults({
270270
path: `/${method}-test`,
271271
})),
272272
},
273+
{
274+
slug: 'disabled-bulk-edit-docs',
275+
fields: [
276+
{
277+
name: 'text',
278+
type: 'text',
279+
},
280+
],
281+
disableBulkEdit: true,
282+
},
273283
],
274284
endpoints: [
275285
{

test/collections-rest/int.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1797,6 +1797,28 @@ describe('collections-rest', () => {
17971797
expect((await result.json()).message.startsWith('Route not found')).toBeTruthy()
17981798
}
17991799
})
1800+
1801+
it('should disable bulk edit for the collection with disableBulkEdit: true', async () => {
1802+
const res = await restClient.PATCH('/disabled-bulk-edit-docs?where[id][equals]=0', {})
1803+
expect(res.status).toBe(403)
1804+
1805+
await expect(
1806+
payload.update({
1807+
collection: 'disabled-bulk-edit-docs',
1808+
where: {},
1809+
data: {},
1810+
overrideAccess: false,
1811+
}),
1812+
).rejects.toBeInstanceOf(APIError)
1813+
1814+
await expect(
1815+
payload.update({
1816+
collection: 'disabled-bulk-edit-docs',
1817+
where: {},
1818+
data: {},
1819+
}),
1820+
).resolves.toBeTruthy()
1821+
})
18001822
})
18011823

18021824
async function createPost(overrides?: Partial<Post>) {

test/collections-rest/payload-types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export interface Config {
7575
'custom-id-number': CustomIdNumber;
7676
'error-on-hooks': ErrorOnHook;
7777
endpoints: Endpoint;
78+
'disabled-bulk-edit-docs': DisabledBulkEditDoc;
7879
users: User;
7980
'payload-locked-documents': PayloadLockedDocument;
8081
'payload-preferences': PayloadPreference;
@@ -90,6 +91,7 @@ export interface Config {
9091
'custom-id-number': CustomIdNumberSelect<false> | CustomIdNumberSelect<true>;
9192
'error-on-hooks': ErrorOnHooksSelect<false> | ErrorOnHooksSelect<true>;
9293
endpoints: EndpointsSelect<false> | EndpointsSelect<true>;
94+
'disabled-bulk-edit-docs': DisabledBulkEditDocsSelect<false> | DisabledBulkEditDocsSelect<true>;
9395
users: UsersSelect<false> | UsersSelect<true>;
9496
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
9597
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -247,6 +249,16 @@ export interface Endpoint {
247249
updatedAt: string;
248250
createdAt: string;
249251
}
252+
/**
253+
* This interface was referenced by `Config`'s JSON-Schema
254+
* via the `definition` "disabled-bulk-edit-docs".
255+
*/
256+
export interface DisabledBulkEditDoc {
257+
id: string;
258+
text?: string | null;
259+
updatedAt: string;
260+
createdAt: string;
261+
}
250262
/**
251263
* This interface was referenced by `Config`'s JSON-Schema
252264
* via the `definition` "users".
@@ -303,6 +315,10 @@ export interface PayloadLockedDocument {
303315
relationTo: 'endpoints';
304316
value: string | Endpoint;
305317
} | null)
318+
| ({
319+
relationTo: 'disabled-bulk-edit-docs';
320+
value: string | DisabledBulkEditDoc;
321+
} | null)
306322
| ({
307323
relationTo: 'users';
308324
value: string | User;
@@ -446,6 +462,15 @@ export interface EndpointsSelect<T extends boolean = true> {
446462
updatedAt?: T;
447463
createdAt?: T;
448464
}
465+
/**
466+
* This interface was referenced by `Config`'s JSON-Schema
467+
* via the `definition` "disabled-bulk-edit-docs_select".
468+
*/
469+
export interface DisabledBulkEditDocsSelect<T extends boolean = true> {
470+
text?: T;
471+
updatedAt?: T;
472+
createdAt?: T;
473+
}
449474
/**
450475
* This interface was referenced by `Config`'s JSON-Schema
451476
* via the `definition` "users_select".

0 commit comments

Comments
 (0)