fix(ui): exclude fields lacking permissions from bulk edit (#11776)

Top-level fields that lack read or update permissions still appear as
options in the field selector within the bulk edit drawer.
This commit is contained in:
Jacob Fletcher
2025-03-21 14:44:49 -04:00
committed by GitHub
parent 5f7202bbb8
commit 7532c4ab66
11 changed files with 354 additions and 157 deletions

View File

@@ -65,6 +65,20 @@ export const PostsCollection: CollectionConfig = {
},
],
},
{
name: 'noRead',
type: 'text',
access: {
read: () => false,
},
},
{
name: 'noUpdate',
type: 'text',
access: {
update: () => false,
},
},
],
},
{
@@ -82,5 +96,19 @@ export const PostsCollection: CollectionConfig = {
},
],
},
{
name: 'noRead',
type: 'text',
access: {
read: () => false,
},
},
{
name: 'noUpdate',
type: 'text',
access: {
update: () => false,
},
},
],
}

View File

@@ -1,4 +1,4 @@
import type { BrowserContext, Page } from '@playwright/test'
import type { BrowserContext, Locator, Page } from '@playwright/test'
import type { PayloadTestSDK } from 'helpers/sdk/index.js'
import { expect, test } from '@playwright/test'
@@ -176,18 +176,14 @@ test.describe('Bulk Edit', () => {
await page.locator('input#select-all').check()
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()
const titleOption = page.locator('.field-select .rs__option', {
hasText: exactText('Title'),
const { field, modal } = await selectFieldToEdit(page, {
fieldLabel: 'Title',
fieldID: 'title',
})
await expect(titleOption).toBeVisible()
await titleOption.click()
const titleInput = page.locator('#field-title')
await expect(titleInput).toBeVisible()
await titleInput.fill(updatedPostTitle)
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
await field.fill(updatedPostTitle)
await modal.locator('.form-submit button[type="submit"].edit-many__publish').click()
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
'Updated 3 Posts successfully.',
@@ -222,14 +218,17 @@ test.describe('Bulk Edit', () => {
await selectTableRow(page, titleOfPostToPublish1)
await selectTableRow(page, titleOfPostToPublish2)
// Bulk edit the selected rows to `published` status
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()
const options = page.locator('.rs__option')
const field = options.locator('text=Description')
await field.click()
await page.locator('#field-description').fill(description)
await page.locator('.form-submit .edit-many__publish').click()
const { field, modal } = await selectFieldToEdit(page, {
fieldLabel: 'Description',
fieldID: 'description',
})
await field.fill(description)
// Bulk edit the selected rows to `published` status
await modal.locator('.form-submit .edit-many__publish').click()
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
'Updated 2 Posts successfully.',
@@ -258,12 +257,16 @@ test.describe('Bulk Edit', () => {
await selectTableRow(page, titleOfPostToDraft2)
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()
const options = page.locator('.rs__option')
const field = options.locator('text=Description')
await field.click()
await page.locator('#field-description').fill(description)
await page.locator('.form-submit .edit-many__draft').click()
const { field, modal } = await selectFieldToEdit(page, {
fieldLabel: 'Description',
fieldID: 'description',
})
await field.fill(description)
// Bulk edit the selected rows to `draft` status
await modal.locator('.form-submit .edit-many__draft').click()
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
'Updated 2 Posts successfully.',
@@ -336,18 +339,14 @@ test.describe('Bulk Edit', () => {
await page.locator('button#select-all-across-pages').click()
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()
const titleOption = page.locator('.field-select .rs__option', {
hasText: exactText('Title'),
const { field } = await selectFieldToEdit(page, {
fieldLabel: 'Title',
fieldID: 'title',
})
await expect(titleOption).toBeVisible()
await titleOption.click()
const titleInput = page.locator('#field-title')
await expect(titleInput).toBeVisible()
const updatedTitle = `Post (Updated)`
await titleInput.fill(updatedTitle)
const updatedTitle = 'Post (Updated)'
await field.fill(updatedTitle)
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
@@ -389,19 +388,18 @@ test.describe('Bulk Edit', () => {
const updatedPostTitle = 'Post 1 (Updated)'
const { id: postID } = await createPost(postData)
await page.goto(postsUrl.list)
await page.locator('input#select-all').check()
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()
const titleOption = page.locator('.field-select .rs__option', {
hasText: exactText('Title'),
await page.goto(postsUrl.list)
const { modal } = await selectAllAndEditMany(page)
const { field } = await selectFieldToEdit(page, {
fieldLabel: 'Title',
fieldID: 'title',
})
await titleOption.click()
const titleInput = page.locator('#field-title')
await titleInput.fill(updatedPostTitle)
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
await field.fill(updatedPostTitle)
await modal.locator('.form-submit button[type="submit"].edit-many__publish').click()
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
'Updated 1 Post successfully.',
@@ -427,23 +425,19 @@ test.describe('Bulk Edit', () => {
test('should bulk edit fields with subfields', async () => {
await deleteAllPosts()
const { id: docID } = await createPost()
await createPost()
await page.goto(postsUrl.list)
await page.locator('input#select-all').check()
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()
const bulkEditModal = page.locator('#edit-posts')
await selectAllAndEditMany(page)
const titleOption = bulkEditModal.locator('.field-select .rs__option', {
hasText: exactText('Group > Title'),
const { modal, field } = await selectFieldToEdit(page, {
fieldLabel: 'Group > Title',
fieldID: 'group__title',
})
await titleOption.click()
const titleInput = bulkEditModal.locator('#field-group__title')
await titleInput.fill('New Group Title')
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
await field.fill('New Group Title')
await modal.locator('.form-submit button[type="submit"].edit-many__publish').click()
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
'Updated 1 Post successfully.',
@@ -458,8 +452,81 @@ test.describe('Bulk Edit', () => {
expect(updatedPost?.group?.title).toBe('New Group Title')
})
test('should not display fields options lacking read and update permissions', async () => {
await deleteAllPosts()
await createPost()
await page.goto(postsUrl.list)
const { modal } = await selectAllAndEditMany(page)
await expect(
modal.locator('.field-select .rs__option', { hasText: exactText('No Read') }),
).toBeHidden()
await expect(
modal.locator('.field-select .rs__option', { hasText: exactText('No Update') }),
).toBeHidden()
})
test('should thread field permissions through subfields', async () => {
await deleteAllPosts()
await createPost()
await page.goto(postsUrl.list)
await selectAllAndEditMany(page)
const { field } = await selectFieldToEdit(page, { fieldLabel: 'Array', fieldID: 'array' })
await field.locator('button.array-field__add-row').click()
await expect(field.locator('#field-array__0__optional')).toBeVisible()
await expect(field.locator('#field-array__0__noRead')).toBeHidden()
await expect(field.locator('#field-array__0__noUpdate')).toBeDisabled()
})
})
async function selectFieldToEdit(
page: Page,
{
fieldLabel,
fieldID,
}: {
fieldID: string
fieldLabel: string
},
): Promise<{ field: Locator; modal: Locator }> {
// ensure modal is open, open if needed
const isModalOpen = await page.locator('#edit-posts').isVisible()
if (!isModalOpen) {
await page.locator('.edit-many__toggle').click()
}
const modal = page.locator('#edit-posts')
await expect(modal).toBeVisible()
await modal.locator('.field-select .rs__control').click()
await modal.locator('.field-select .rs__option', { hasText: exactText(fieldLabel) }).click()
const field = modal.locator(`#field-${fieldID}`)
await expect(field).toBeVisible()
return { modal, field }
}
async function selectAllAndEditMany(page: Page): Promise<{ modal: Locator }> {
await page.locator('input#select-all').check()
await page.locator('.edit-many__toggle').click()
const modal = page.locator('#edit-posts')
await expect(modal).toBeVisible()
return { modal }
}
async function deleteAllPosts() {
await payload.delete({ collection: postsSlug, where: { id: { exists: true } } })
}

View File

@@ -82,7 +82,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: number;
defaultIDType: string;
};
globals: {};
globalsSelect: {};
@@ -118,7 +118,7 @@ export interface UserAuthOperations {
* via the `definition` "posts".
*/
export interface Post {
id: number;
id: string;
title?: string | null;
description?: string | null;
defaultValueField?: string | null;
@@ -135,6 +135,8 @@ export interface Post {
id?: string | null;
}[]
| null;
noRead?: string | null;
noUpdate?: string | null;
id?: string | null;
}[]
| null;
@@ -146,6 +148,8 @@ export interface Post {
blockType: 'textBlock';
}[]
| null;
noRead?: string | null;
noUpdate?: string | null;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
@@ -155,7 +159,7 @@ export interface Post {
* via the `definition` "users".
*/
export interface User {
id: number;
id: string;
updatedAt: string;
createdAt: string;
email: string;
@@ -172,20 +176,20 @@ export interface User {
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: number;
id: string;
document?:
| ({
relationTo: 'posts';
value: number | Post;
value: string | Post;
} | null)
| ({
relationTo: 'users';
value: number | User;
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: number | User;
value: string | User;
};
updatedAt: string;
createdAt: string;
@@ -195,10 +199,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: number;
id: string;
user: {
relationTo: 'users';
value: number | User;
value: string | User;
};
key?: string | null;
value?:
@@ -218,7 +222,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: number;
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
@@ -248,6 +252,8 @@ export interface PostsSelect<T extends boolean = true> {
innerOptional?: T;
id?: T;
};
noRead?: T;
noUpdate?: T;
id?: T;
};
blocks?:
@@ -261,6 +267,8 @@ export interface PostsSelect<T extends boolean = true> {
blockName?: T;
};
};
noRead?: T;
noUpdate?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;