feat: collection-level disableBulkEdit (#12850)
This commit is contained in:
@@ -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._
|
||||
|
||||
|
||||
@@ -274,7 +274,7 @@ export const renderListView = async (
|
||||
collectionSlug,
|
||||
columnState,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
disableBulkEdit: collectionConfig.disableBulkEdit ?? disableBulkEdit,
|
||||
disableQueryPresets,
|
||||
enableRowSelections,
|
||||
hasCreatePermission,
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ function CollectionFolderViewInContext(props: CollectionFolderViewInContextProps
|
||||
!smallBreak && (
|
||||
<ListSelection
|
||||
disableBulkDelete={disableBulkDelete}
|
||||
disableBulkEdit={disableBulkEdit}
|
||||
disableBulkEdit={collectionConfig.disableBulkEdit ?? disableBulkEdit}
|
||||
key="list-selection"
|
||||
/>
|
||||
),
|
||||
|
||||
7
test/admin/collections/DisableBulkEdit.ts
Normal file
7
test/admin/collections/DisableBulkEdit.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const DisableBulkEdit: CollectionConfig = {
|
||||
slug: 'disable-bulk-edit',
|
||||
fields: [],
|
||||
disableBulkEdit: true,
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -270,6 +270,16 @@ export default buildConfigWithDefaults({
|
||||
path: `/${method}-test`,
|
||||
})),
|
||||
},
|
||||
{
|
||||
slug: 'disabled-bulk-edit-docs',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
disableBulkEdit: true,
|
||||
},
|
||||
],
|
||||
endpoints: [
|
||||
{
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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".
|
||||
|
||||
Reference in New Issue
Block a user