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

@@ -67,7 +67,9 @@ export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFi
export { formatAdminURL } from '../utilities/formatAdminURL.js'
export { getDataByPath } from '../utilities/getDataByPath.js'
export { getFieldPermissions } from '../utilities/getFieldPermissions.js'
export { getSelectMode } from '../utilities/getSelectMode.js'
export { getSiblingData } from '../utilities/getSiblingData.js'
export { getUniqueListBy } from '../utilities/getUniqueListBy.js'

View File

@@ -0,0 +1,56 @@
// @ts-strict-ignore
import type { SanitizedFieldPermissions } from '../auth/types.js'
import type { ClientField, Field } from '../fields/config/types.js'
import type { Operation } from '../types/index.js'
/**
* Gets read and operation-level permissions for a given field based on cascading field permissions.
* @returns An object with the following properties:
* - `operation`: Whether the user has permission to perform the operation on the field (`create` or `update`).
* - `permissions`: The field-level permissions.
* - `read`: Whether the user has permission to read the field.
*/
export const getFieldPermissions = ({
field,
operation,
parentName,
permissions,
}: {
readonly field: ClientField | Field
readonly operation: Operation
readonly parentName: string
readonly permissions:
| {
[fieldName: string]: SanitizedFieldPermissions
}
| SanitizedFieldPermissions
}): {
operation: boolean
permissions: SanitizedFieldPermissions
read: boolean
} => ({
operation:
permissions === true ||
permissions?.[operation] === true ||
permissions?.[parentName] === true ||
('name' in field &&
typeof permissions === 'object' &&
permissions?.[field.name] &&
(permissions[field.name] === true ||
(operation in permissions[field.name] && permissions[field.name][operation]))),
permissions:
permissions === undefined || permissions === null || permissions === true
? true
: 'name' in field
? permissions?.[field.name]
: permissions,
read:
permissions === true ||
permissions?.read === true ||
permissions?.[parentName] === true ||
('name' in field &&
typeof permissions === 'object' &&
permissions?.[field.name] &&
(permissions[field.name] === true ||
('read' in permissions[field.name] && permissions[field.name].read))),
})

View File

@@ -1,6 +1,6 @@
'use client'
import type { ClientCollectionConfig, FieldWithPathClient, SelectType } from 'payload'
import type { ClientCollectionConfig, SelectType } from 'payload'
import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations'
@@ -9,11 +9,12 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { FormProps } from '../../../forms/Form/index.js'
import type { OnFieldSelect } from '../../FieldSelect/index.js'
import type { FieldOption } from '../../FieldSelect/reduceFieldOptions.js'
import type { State } from '../FormsManager/reducer.js'
import { Button } from '../../../elements/Button/index.js'
import { Form } from '../../../forms/Form/index.js'
import { RenderFields } from '../../../forms/RenderFields/index.js'
import { RenderField } from '../../../forms/RenderFields/RenderField.js'
import { XIcon } from '../../../icons/X/index.js'
import { useAuth } from '../../../providers/Auth/index.js'
import { useServerFunctions } from '../../../providers/ServerFunctions/index.js'
@@ -23,6 +24,7 @@ import { FieldSelect } from '../../FieldSelect/index.js'
import { useFormsManager } from '../FormsManager/index.js'
import { baseClass, type EditManyBulkUploadsProps } from './index.js'
import './index.scss'
import '../../../forms/RenderFields/index.scss'
export const EditManyBulkUploadsDrawerContent: React.FC<
{
@@ -46,13 +48,13 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
const { getFormState } = useServerFunctions()
const abortFormStateRef = React.useRef<AbortController>(null)
const [selectedFields, setSelectedFields] = useState<FieldWithPathClient[]>([])
const [selectedFields, setSelectedFields] = useState<FieldOption[]>([])
const collectionPermissions = permissions?.collections?.[collection.slug]
const select = useMemo<SelectType>(() => {
return unflatten(
selectedFields.reduce((acc, field) => {
acc[field.path] = true
selectedFields.reduce((acc, option) => {
acc[option.value.path] = true
return acc
}, {} as SelectType),
)
@@ -91,10 +93,9 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
const handleSubmit: FormProps['onSubmit'] = useCallback(
(formState) => {
const pairedData = selectedFields.reduce((acc, field) => {
const { path } = field
if (formState[path]) {
acc[path] = formState[path].value
const pairedData = selectedFields.reduce((acc, option) => {
if (formState[option.value.path]) {
acc[option.value.path] = formState[option.value.path].value
}
return acc
}, {})
@@ -108,11 +109,7 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
async ({ dispatchFields, formState, selected }) => {
setIsInitializing(true)
if (selected === null) {
setSelectedFields([])
} else {
setSelectedFields(selected.map(({ value }) => value))
}
setSelectedFields(selected || [])
const { state } = await getFormState({
collectionSlug: collection.slug,
@@ -165,16 +162,31 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
onChange={[onChange]}
onSubmit={handleSubmit}
>
<FieldSelect fields={fields} onChange={onFieldSelect} />
{selectedFields.length === 0 ? null : (
<RenderFields
fields={selectedFields}
parentIndexPath=""
parentPath=""
parentSchemaPath={collection.slug}
permissions={collectionPermissions?.fields}
readOnly={false}
<FieldSelect
fields={fields}
onChange={onFieldSelect}
permissions={collectionPermissions.fields}
/>
{selectedFields.length === 0 ? null : (
<div className="render-fields">
{selectedFields.map((option, i) => {
const {
value: { field, fieldPermissions, path },
} = option
return (
<RenderField
clientFieldConfig={field}
indexPath=""
key={`${path}-${i}`}
parentPath=""
parentSchemaPath=""
path={path}
permissions={fieldPermissions}
/>
)
})}
</div>
)}
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>

View File

@@ -1,6 +1,6 @@
'use client'
import type { FieldWithPathClient, SelectType } from 'payload'
import type { SelectType } from 'payload'
import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations'
@@ -11,6 +11,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { FormProps } from '../../forms/Form/index.js'
import type { OnFieldSelect } from '../FieldSelect/index.js'
import type { FieldOption } from '../FieldSelect/reduceFieldOptions.js'
import { useForm } from '../../forms/Form/context.js'
import { Form } from '../../forms/Form/index.js'
@@ -113,8 +114,8 @@ const SaveDraftButton: React.FC<{
export const EditManyDrawerContent: React.FC<
{
drawerSlug: string
selectedFields: FieldWithPathClient[]
setSelectedFields: React.Dispatch<React.SetStateAction<FieldWithPathClient[]>>
selectedFields: FieldOption[]
setSelectedFields: (fields: FieldOption[]) => void
} & EditManyProps
> = (props) => {
const {
@@ -151,8 +152,8 @@ export const EditManyDrawerContent: React.FC<
const select = useMemo<SelectType>(() => {
return unflatten(
selectedFields.reduce((acc, field) => {
acc[field.path] = true
selectedFields.reduce((acc, option) => {
acc[option.value.path] = true
return acc
}, {} as SelectType),
)
@@ -216,11 +217,7 @@ export const EditManyDrawerContent: React.FC<
async ({ dispatchFields, formState, selected }) => {
setIsInitializing(true)
if (selected === null) {
setSelectedFields([])
} else {
setSelectedFields(selected.map(({ value }) => value))
}
setSelectedFields(selected || [])
const { state } = await getFormState({
collectionSlug: collection.slug,
@@ -286,11 +283,17 @@ export const EditManyDrawerContent: React.FC<
onChange={[onChange]}
onSuccess={onSuccess}
>
<FieldSelect fields={fields} onChange={onFieldSelect} />
<FieldSelect
fields={fields}
onChange={onFieldSelect}
permissions={collectionPermissions.fields}
/>
{selectedFields.length === 0 ? null : (
<div className="render-fields">
{selectedFields.map((field, i) => {
const { path } = field
{selectedFields.map((option, i) => {
const {
value: { field, fieldPermissions, path },
} = option
return (
<RenderField
@@ -300,15 +303,7 @@ export const EditManyDrawerContent: React.FC<
parentPath=""
parentSchemaPath=""
path={path}
permissions={
collectionPermissions.fields === undefined ||
collectionPermissions.fields === null ||
collectionPermissions.fields === true
? true
: 'name' in field
? collectionPermissions.fields?.[field.name]
: undefined
}
permissions={fieldPermissions}
/>
)
})}

View File

@@ -1,9 +1,11 @@
'use client'
import type { ClientCollectionConfig, FieldWithPathClient } from 'payload'
import type { ClientCollectionConfig } from 'payload'
import { useModal } from '@faceless-ui/modal'
import React, { useState } from 'react'
import type { FieldOption } from '../FieldSelect/reduceFieldOptions.js'
import { useAuth } from '../../providers/Auth/index.js'
import { EditDepthProvider } from '../../providers/EditDepth/index.js'
import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
@@ -28,7 +30,8 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
const { selectAll } = useSelection()
const { t } = useTranslation()
const [selectedFields, setSelectedFields] = useState<FieldWithPathClient[]>([])
const [selectedFields, setSelectedFields] = useState<FieldOption[]>([])
const collectionPermissions = permissions?.collections?.[slug]

View File

@@ -1,9 +1,10 @@
'use client'
import type { ClientField, FieldWithPathClient, FormState } from 'payload'
import type { ClientField, FormState, SanitizedFieldPermissions } from 'payload'
import React, { useState } from 'react'
import type { FieldAction } from '../../forms/Form/types.js'
import type { FieldOption } from './reduceFieldOptions.js'
import { FieldLabel } from '../../fields/FieldLabel/index.js'
import { useForm } from '../../forms/Form/context.js'
@@ -28,16 +29,23 @@ export type OnFieldSelect = ({
export type FieldSelectProps = {
readonly fields: ClientField[]
readonly onChange: OnFieldSelect
readonly permissions:
| {
[fieldName: string]: SanitizedFieldPermissions
}
| SanitizedFieldPermissions
}
export type FieldOption = { Label: React.ReactNode; value: FieldWithPathClient }
export const FieldSelect: React.FC<FieldSelectProps> = ({ fields, onChange }) => {
export const FieldSelect: React.FC<FieldSelectProps> = ({ fields, onChange, permissions }) => {
const { t } = useTranslation()
const { dispatchFields, getFields } = useForm()
const [options] = useState<FieldOption[]>(() =>
reduceFieldOptions({ fields: filterOutUploadFields(fields), formState: getFields() }),
reduceFieldOptions({
fields: filterOutUploadFields(fields),
formState: getFields(),
permissions,
}),
)
return (

View File

@@ -1,11 +1,18 @@
import type { ClientField, FieldWithPathClient, FormState } from 'payload'
import type { ClientField, FormState, SanitizedFieldPermissions } from 'payload'
import { fieldAffectsData, fieldHasSubFields, fieldIsHiddenOrDisabled } from 'payload/shared'
import {
fieldAffectsData,
fieldHasSubFields,
fieldIsHiddenOrDisabled,
getFieldPermissions,
} from 'payload/shared'
import { createNestedClientFieldPath } from '../../forms/Form/createNestedClientFieldPath.js'
import { combineFieldLabel } from '../../utilities/combineFieldLabel.js'
export type SelectedField = {
field: ClientField
fieldPermissions: SanitizedFieldPermissions
path: string
}
@@ -27,13 +34,21 @@ export const reduceFieldOptions = ({
fields,
formState,
labelPrefix = null,
parentPath = '',
path = '',
permissions,
}: {
fields: ClientField[]
formState?: FormState
labelPrefix?: React.ReactNode
path?: string
}): { Label: React.ReactNode; value: FieldWithPathClient }[] => {
readonly fields: ClientField[]
readonly formState?: FormState
readonly labelPrefix?: React.ReactNode
readonly parentPath?: string
readonly path?: string
readonly permissions:
| {
[fieldName: string]: SanitizedFieldPermissions
}
| SanitizedFieldPermissions
}): FieldOption[] => {
if (!fields) {
return []
}
@@ -41,13 +56,28 @@ export const reduceFieldOptions = ({
const CustomLabel = formState?.[path]?.customComponents?.Label
return fields?.reduce((fieldsToUse, field) => {
const {
operation: hasOperationPermission,
permissions: fieldPermissions,
read: hasReadPermission,
} = getFieldPermissions({
field,
operation: 'update',
parentName: parentPath?.includes('.')
? parentPath.split('.')[parentPath.split('.').length - 1]
: parentPath,
permissions,
})
// escape for a variety of reasons, include ui fields as they have `name`.
if (
(fieldAffectsData(field) || field.type === 'ui') &&
(field.admin?.disableBulkEdit ||
field.unique ||
fieldIsHiddenOrDisabled(field) ||
('readOnly' in field && field.readOnly))
('readOnly' in field && field.readOnly) ||
!hasOperationPermission ||
!hasReadPermission)
) {
return fieldsToUse
}
@@ -58,7 +88,9 @@ export const reduceFieldOptions = ({
...reduceFieldOptions({
fields: field.fields,
labelPrefix: combineFieldLabel({ CustomLabel, field, prefix: labelPrefix }),
parentPath: path,
path: createNestedClientFieldPath(path, field),
permissions: fieldPermissions,
}),
]
}
@@ -74,7 +106,9 @@ export const reduceFieldOptions = ({
...reduceFieldOptions({
fields: tab.fields,
labelPrefix,
parentPath: path,
path: isNamedTab ? createNestedClientFieldPath(path, field) : path,
permissions: fieldPermissions,
}),
]
}
@@ -82,10 +116,11 @@ export const reduceFieldOptions = ({
]
}
const formattedField = {
const formattedField: FieldOption = {
label: combineFieldLabel({ CustomLabel, field, prefix: labelPrefix }),
value: {
...field,
field,
fieldPermissions,
path: createNestedClientFieldPath(path, field),
},
}

View File

@@ -1,6 +1,6 @@
'use client'
import { fieldIsHiddenOrDisabled, getFieldPaths } from 'payload/shared'
import { fieldIsHiddenOrDisabled, getFieldPaths, getFieldPermissions } from 'payload/shared'
import React from 'react'
import type { RenderFieldsProps } from './types.js'
@@ -49,22 +49,21 @@ export const RenderFields: React.FC<RenderFieldsProps> = (props) => {
return null
}
const parentName = parentPath?.includes('.')
const {
operation: hasOperationPermission,
permissions: fieldPermissions,
read: hasReadPermission,
} = getFieldPermissions({
field,
operation,
parentName: parentPath?.includes('.')
? parentPath.split('.')[parentPath.split('.').length - 1]
: parentPath
: parentPath,
permissions,
})
// If the user cannot read the field, then filter it out
// This is different from `admin.readOnly` which is executed based on `operation`
const hasReadPermission =
permissions === true ||
permissions?.read === true ||
permissions?.[parentName] === true ||
('name' in field &&
typeof permissions === 'object' &&
permissions?.[field.name] &&
(permissions[field.name] === true ||
('read' in permissions[field.name] && permissions[field.name].read)))
if ('name' in field && !hasReadPermission) {
return null
}
@@ -77,17 +76,7 @@ export const RenderFields: React.FC<RenderFieldsProps> = (props) => {
isReadOnly = false
}
// If the user does not have access control to begin with, force it to be read-only
const hasOperationPermission =
permissions === true ||
permissions?.[operation] === true ||
permissions?.[parentName] === true ||
('name' in field &&
typeof permissions === 'object' &&
permissions?.[field.name] &&
(permissions[field.name] === true ||
(operation in permissions[field.name] && permissions[field.name][operation])))
// If the user does not have access at the operation level, to begin with, force it to be read-only
if ('name' in field && !hasOperationPermission) {
isReadOnly = true
}
@@ -109,13 +98,7 @@ export const RenderFields: React.FC<RenderFieldsProps> = (props) => {
parentPath={parentPath}
parentSchemaPath={parentSchemaPath}
path={path}
permissions={
permissions === undefined || permissions === null || permissions === true
? true
: 'name' in field
? permissions?.[field.name]
: permissions
}
permissions={fieldPermissions}
readOnly={isReadOnly}
schemaPath={schemaPath}
/>

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;