Skip to content

Commit cedd916

Browse files
fix: corrects permission access reading for disabling fields (#6815)
Fixes issues where access control was not properly affecting the read-only setting on fields.
1 parent 4587148 commit cedd916

File tree

9 files changed

+99
-27
lines changed

9 files changed

+99
-27
lines changed

packages/ui/src/fields/Array/index.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import type { ArrayField as ArrayFieldType, FieldPermissions } from 'payload'
2+
import type { ArrayField as ArrayFieldType } from 'payload'
33

44
import { getTranslation } from '@payloadcms/translations'
55
import React, { useCallback } from 'react'
@@ -40,7 +40,6 @@ export type ArrayFieldProps = FormFieldBase & {
4040
maxRows?: ArrayFieldType['maxRows']
4141
minRows?: ArrayFieldType['minRows']
4242
name?: string
43-
permissions: FieldPermissions
4443
width?: string
4544
}
4645

@@ -63,13 +62,17 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
6362
maxRows,
6463
minRows: minRowsProp,
6564
path: pathFromProps,
66-
permissions,
6765
readOnly: readOnlyFromProps,
6866
required,
6967
validate,
7068
} = props
7169

72-
const { indexPath, readOnly: readOnlyFromContext } = useFieldProps()
70+
const {
71+
indexPath,
72+
path: pathFromContext,
73+
permissions,
74+
readOnly: readOnlyFromContext,
75+
} = useFieldProps()
7376
const minRows = minRowsProp ?? required ? 1 : 0
7477

7578
const { setDocFieldPreferences } = useDocumentInfo()
@@ -110,8 +113,6 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
110113
[maxRows, minRows, required, validate, editingDefaultLocale],
111114
)
112115

113-
const { path: pathFromContext } = useFieldProps()
114-
115116
const {
116117
errorPaths,
117118
formInitializing,

packages/ui/src/fields/Blocks/index.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,39 @@
11
'use client'
2+
import type { BlockField } from 'payload'
3+
24
import { getTranslation } from '@payloadcms/translations'
35
import React, { Fragment, useCallback } from 'react'
46

7+
import type { ReducedBlock } from '../../providers/ComponentMap/buildComponentMap/types.js'
8+
import type { FormFieldBase } from '../shared/index.js'
9+
510
import { Banner } from '../../elements/Banner/index.js'
611
import { Button } from '../../elements/Button/index.js'
712
import { DraggableSortableItem } from '../../elements/DraggableSortable/DraggableSortableItem/index.js'
813
import { DraggableSortable } from '../../elements/DraggableSortable/index.js'
914
import { DrawerToggler } from '../../elements/Drawer/index.js'
1015
import { useDrawerSlug } from '../../elements/Drawer/useDrawerSlug.js'
1116
import { ErrorPill } from '../../elements/ErrorPill/index.js'
17+
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
1218
import { useForm, useFormSubmitted } from '../../forms/Form/context.js'
1319
import { NullifyLocaleField } from '../../forms/NullifyField/index.js'
1420
import { useField } from '../../forms/useField/index.js'
21+
import { withCondition } from '../../forms/withCondition/index.js'
1522
import { useConfig } from '../../providers/Config/index.js'
1623
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
1724
import { useLocale } from '../../providers/Locale/index.js'
1825
import { useTranslation } from '../../providers/Translation/index.js'
1926
import { scrollToID } from '../../utilities/scrollToID.js'
27+
import { FieldDescription } from '../FieldDescription/index.js'
28+
import { FieldError } from '../FieldError/index.js'
29+
import { FieldLabel } from '../FieldLabel/index.js'
2030
import { fieldBaseClass } from '../shared/index.js'
2131
import { BlockRow } from './BlockRow.js'
2232
import { BlocksDrawer } from './BlocksDrawer/index.js'
2333
import './index.scss'
2434

2535
const baseClass = 'blocks-field'
2636

27-
import type { BlockField, FieldPermissions } from 'payload'
28-
29-
import type { ReducedBlock } from '../../providers/ComponentMap/buildComponentMap/types.js'
30-
import type { FormFieldBase } from '../shared/index.js'
31-
32-
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
33-
import { withCondition } from '../../forms/withCondition/index.js'
34-
import { FieldDescription } from '../FieldDescription/index.js'
35-
import { FieldError } from '../FieldError/index.js'
36-
import { FieldLabel } from '../FieldLabel/index.js'
37-
3837
export type BlocksFieldProps = FormFieldBase & {
3938
blocks?: ReducedBlock[]
4039
forceRender?: boolean
@@ -43,7 +42,6 @@ export type BlocksFieldProps = FormFieldBase & {
4342
maxRows?: number
4443
minRows?: number
4544
name?: string
46-
permissions: FieldPermissions
4745
slug?: string
4846
width?: string
4947
}

packages/ui/src/fields/Collapsible/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import { FieldDescription } from '../FieldDescription/index.js'
2727
export type CollapsibleFieldProps = FormFieldBase & {
2828
fieldMap: FieldMap
2929
initCollapsed?: boolean
30-
permissions: FieldPermissions
3130
width?: string
3231
}
3332

packages/ui/src/fields/Group/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
'use client'
2-
import type { FieldPermissions } from 'payload'
32

43
import { getTranslation } from '@payloadcms/translations'
54
import React, { Fragment } from 'react'
@@ -33,7 +32,6 @@ export type GroupFieldProps = FormFieldBase & {
3332
forceRender?: boolean
3433
hideGutter?: boolean
3534
name?: string
36-
permissions: FieldPermissions
3735
width?: string
3836
}
3937

packages/ui/src/fields/Tabs/index.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import type { DocumentPreferences, FieldPermissions } from 'payload'
2+
import type { DocumentPreferences } from 'payload'
33

44
import { getTranslation } from '@payloadcms/translations'
55
import { toKebabCase } from 'payload/shared'
@@ -29,7 +29,6 @@ export type TabsFieldProps = FormFieldBase & {
2929
forceRender?: boolean
3030
name?: string
3131
path?: string
32-
permissions: FieldPermissions
3332
tabs?: MappedTab[]
3433
width?: string
3534
}
@@ -49,9 +48,9 @@ const TabsField: React.FC<TabsFieldProps> = (props) => {
4948
const {
5049
indexPath,
5150
path: pathFromContext,
52-
permissions,
5351
readOnly: readOnlyFromContext,
5452
schemaPath,
53+
siblingPermissions,
5554
} = useFieldProps()
5655

5756
const readOnly = readOnlyFromProps || readOnlyFromContext
@@ -180,9 +179,9 @@ const TabsField: React.FC<TabsFieldProps> = (props) => {
180179
margins="small"
181180
path={generateTabPath()}
182181
permissions={
183-
'name' in activeTabConfig && permissions?.fields?.[activeTabConfig.name]?.fields
184-
? permissions?.fields?.[activeTabConfig.name]?.fields
185-
: permissions?.fields
182+
'name' in activeTabConfig && siblingPermissions?.[activeTabConfig.name]?.fields
183+
? siblingPermissions[activeTabConfig.name]?.fields
184+
: siblingPermissions
186185
}
187186
readOnly={readOnly}
188187
schemaPath={`${schemaPath ? `${schemaPath}` : ''}${activeTabConfig.name ? `.${activeTabConfig.name}` : ''}`}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { CollectionConfig, Field } from 'payload/types'
2+
3+
import { disabledSlug } from '../../shared.js'
4+
5+
const disabledFromUpdateAccessControl = (fieldName = 'text'): Field => ({
6+
type: 'text',
7+
name: fieldName,
8+
access: {
9+
update: () => {
10+
return false
11+
},
12+
},
13+
})
14+
15+
export const Disabled: CollectionConfig = {
16+
slug: disabledSlug,
17+
fields: [
18+
{
19+
type: 'group',
20+
name: 'group',
21+
fields: [disabledFromUpdateAccessControl()],
22+
},
23+
{
24+
type: 'tabs',
25+
tabs: [
26+
{
27+
name: 'namedTab',
28+
fields: [disabledFromUpdateAccessControl()],
29+
},
30+
{
31+
label: 'unnamedTab',
32+
fields: [disabledFromUpdateAccessControl('unnamedTab')],
33+
},
34+
],
35+
},
36+
{
37+
type: 'array',
38+
name: 'array',
39+
fields: [disabledFromUpdateAccessControl()],
40+
},
41+
],
42+
}

test/access-control/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { FieldAccess } from 'payload'
77
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
88
import { devUser } from '../credentials.js'
99
import { TestButton } from './TestButton.js'
10+
import { Disabled } from './collections/Disabled/index.js'
1011
import {
1112
createNotUpdateCollectionSlug,
1213
docLevelAccessSlug,
@@ -533,6 +534,7 @@ export default buildConfigWithDefaults({
533534
},
534535
],
535536
},
537+
Disabled,
536538
],
537539
onInit: async (payload) => {
538540
await payload.create({

test/access-control/e2e.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
3030
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
3131
import {
3232
createNotUpdateCollectionSlug,
33+
disabledSlug,
3334
docLevelAccessSlug,
3435
fullyRestrictedSlug,
3536
noAdminAccessEmail,
@@ -67,6 +68,7 @@ describe('access control', () => {
6768
let restrictedVersionsUrl: AdminUrlUtil
6869
let userRestrictedCollectionURL: AdminUrlUtil
6970
let userRestrictedGlobalURL: AdminUrlUtil
71+
let disabledFields: AdminUrlUtil
7072
let serverURL: string
7173
let context: BrowserContext
7274
let logoutURL: string
@@ -83,6 +85,7 @@ describe('access control', () => {
8385
restrictedVersionsUrl = new AdminUrlUtil(serverURL, restrictedVersionsSlug)
8486
userRestrictedCollectionURL = new AdminUrlUtil(serverURL, userRestrictedCollectionSlug)
8587
userRestrictedGlobalURL = new AdminUrlUtil(serverURL, userRestrictedGlobalSlug)
88+
disabledFields = new AdminUrlUtil(serverURL, disabledSlug)
8689

8790
context = await browser.newContext()
8891
page = await context.newPage()
@@ -521,6 +524,34 @@ describe('access control', () => {
521524
await expect(page.locator('.next-error-h1')).toBeVisible()
522525
})
523526
})
527+
528+
describe('read-only from access control', () => {
529+
test('should be read-only when update returns false', async () => {
530+
await page.goto(disabledFields.create)
531+
532+
// group field
533+
await page.locator('#field-group__text').fill('group')
534+
535+
// named tab
536+
await page.locator('#field-namedTab__text').fill('named tab')
537+
538+
// unnamed tab
539+
await page.locator('.tabs-field__tab-button').nth(1).click()
540+
await page.locator('#field-unnamedTab').fill('unnamed tab')
541+
542+
// array field
543+
await page.locator('#field-array button').click()
544+
await page.locator('#field-array__0__text').fill('array row 0')
545+
546+
await saveDocAndAssert(page)
547+
548+
await expect(page.locator('#field-group__text')).toBeDisabled()
549+
await expect(page.locator('#field-namedTab__text')).toBeDisabled()
550+
await page.locator('.tabs-field__tab-button').nth(1).click()
551+
await expect(page.locator('#field-unnamedTab')).toBeDisabled()
552+
await expect(page.locator('#field-array__0__text')).toBeDisabled()
553+
})
554+
})
524555
})
525556

526557
// eslint-disable-next-line @typescript-eslint/require-await

test/access-control/shared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ export const noAdminAccessEmail = 'no-admin-access@payloadcms.com'
2424
export const nonAdminUserEmail = 'non-admin-user@payloadcms.com'
2525

2626
export const nonAdminUserSlug = 'non-admin-user'
27+
28+
export const disabledSlug = 'disabled'

0 commit comments

Comments
 (0)