diff --git a/packages/ui/src/fields/Array/index.tsx b/packages/ui/src/fields/Array/index.tsx index 24f249bf1..4fc219c55 100644 --- a/packages/ui/src/fields/Array/index.tsx +++ b/packages/ui/src/fields/Array/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { ArrayField as ArrayFieldType, FieldPermissions } from 'payload' +import type { ArrayField as ArrayFieldType } from 'payload' import { getTranslation } from '@payloadcms/translations' import React, { useCallback } from 'react' @@ -40,7 +40,6 @@ export type ArrayFieldProps = FormFieldBase & { maxRows?: ArrayFieldType['maxRows'] minRows?: ArrayFieldType['minRows'] name?: string - permissions: FieldPermissions width?: string } @@ -63,13 +62,17 @@ export const _ArrayField: React.FC = (props) => { maxRows, minRows: minRowsProp, path: pathFromProps, - permissions, readOnly: readOnlyFromProps, required, validate, } = props - const { indexPath, readOnly: readOnlyFromContext } = useFieldProps() + const { + indexPath, + path: pathFromContext, + permissions, + readOnly: readOnlyFromContext, + } = useFieldProps() const minRows = minRowsProp ?? required ? 1 : 0 const { setDocFieldPreferences } = useDocumentInfo() @@ -110,8 +113,6 @@ export const _ArrayField: React.FC = (props) => { [maxRows, minRows, required, validate, editingDefaultLocale], ) - const { path: pathFromContext } = useFieldProps() - const { errorPaths, formInitializing, diff --git a/packages/ui/src/fields/Blocks/index.tsx b/packages/ui/src/fields/Blocks/index.tsx index 4236fb163..6a194e1ae 100644 --- a/packages/ui/src/fields/Blocks/index.tsx +++ b/packages/ui/src/fields/Blocks/index.tsx @@ -1,7 +1,12 @@ 'use client' +import type { BlockField } from 'payload' + import { getTranslation } from '@payloadcms/translations' import React, { Fragment, useCallback } from 'react' +import type { ReducedBlock } from '../../providers/ComponentMap/buildComponentMap/types.js' +import type { FormFieldBase } from '../shared/index.js' + import { Banner } from '../../elements/Banner/index.js' import { Button } from '../../elements/Button/index.js' import { DraggableSortableItem } from '../../elements/DraggableSortable/DraggableSortableItem/index.js' @@ -9,14 +14,19 @@ import { DraggableSortable } from '../../elements/DraggableSortable/index.js' import { DrawerToggler } from '../../elements/Drawer/index.js' import { useDrawerSlug } from '../../elements/Drawer/useDrawerSlug.js' import { ErrorPill } from '../../elements/ErrorPill/index.js' +import { useFieldProps } from '../../forms/FieldPropsProvider/index.js' import { useForm, useFormSubmitted } from '../../forms/Form/context.js' import { NullifyLocaleField } from '../../forms/NullifyField/index.js' import { useField } from '../../forms/useField/index.js' +import { withCondition } from '../../forms/withCondition/index.js' import { useConfig } from '../../providers/Config/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { scrollToID } from '../../utilities/scrollToID.js' +import { FieldDescription } from '../FieldDescription/index.js' +import { FieldError } from '../FieldError/index.js' +import { FieldLabel } from '../FieldLabel/index.js' import { fieldBaseClass } from '../shared/index.js' import { BlockRow } from './BlockRow.js' import { BlocksDrawer } from './BlocksDrawer/index.js' @@ -24,17 +34,6 @@ import './index.scss' const baseClass = 'blocks-field' -import type { BlockField, FieldPermissions } from 'payload' - -import type { ReducedBlock } from '../../providers/ComponentMap/buildComponentMap/types.js' -import type { FormFieldBase } from '../shared/index.js' - -import { useFieldProps } from '../../forms/FieldPropsProvider/index.js' -import { withCondition } from '../../forms/withCondition/index.js' -import { FieldDescription } from '../FieldDescription/index.js' -import { FieldError } from '../FieldError/index.js' -import { FieldLabel } from '../FieldLabel/index.js' - export type BlocksFieldProps = FormFieldBase & { blocks?: ReducedBlock[] forceRender?: boolean @@ -43,7 +42,6 @@ export type BlocksFieldProps = FormFieldBase & { maxRows?: number minRows?: number name?: string - permissions: FieldPermissions slug?: string width?: string } diff --git a/packages/ui/src/fields/Collapsible/index.tsx b/packages/ui/src/fields/Collapsible/index.tsx index e1f460d8c..6d993f992 100644 --- a/packages/ui/src/fields/Collapsible/index.tsx +++ b/packages/ui/src/fields/Collapsible/index.tsx @@ -27,7 +27,6 @@ import { FieldDescription } from '../FieldDescription/index.js' export type CollapsibleFieldProps = FormFieldBase & { fieldMap: FieldMap initCollapsed?: boolean - permissions: FieldPermissions width?: string } diff --git a/packages/ui/src/fields/Group/index.tsx b/packages/ui/src/fields/Group/index.tsx index 0c523abf3..ade2794c6 100644 --- a/packages/ui/src/fields/Group/index.tsx +++ b/packages/ui/src/fields/Group/index.tsx @@ -1,5 +1,4 @@ 'use client' -import type { FieldPermissions } from 'payload' import { getTranslation } from '@payloadcms/translations' import React, { Fragment } from 'react' @@ -33,7 +32,6 @@ export type GroupFieldProps = FormFieldBase & { forceRender?: boolean hideGutter?: boolean name?: string - permissions: FieldPermissions width?: string } diff --git a/packages/ui/src/fields/Tabs/index.tsx b/packages/ui/src/fields/Tabs/index.tsx index f8be58b4c..592b8b2cd 100644 --- a/packages/ui/src/fields/Tabs/index.tsx +++ b/packages/ui/src/fields/Tabs/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { DocumentPreferences, FieldPermissions } from 'payload' +import type { DocumentPreferences } from 'payload' import { getTranslation } from '@payloadcms/translations' import { toKebabCase } from 'payload/shared' @@ -29,7 +29,6 @@ export type TabsFieldProps = FormFieldBase & { forceRender?: boolean name?: string path?: string - permissions: FieldPermissions tabs?: MappedTab[] width?: string } @@ -49,9 +48,9 @@ const TabsField: React.FC = (props) => { const { indexPath, path: pathFromContext, - permissions, readOnly: readOnlyFromContext, schemaPath, + siblingPermissions, } = useFieldProps() const readOnly = readOnlyFromProps || readOnlyFromContext @@ -180,9 +179,9 @@ const TabsField: React.FC = (props) => { margins="small" path={generateTabPath()} permissions={ - 'name' in activeTabConfig && permissions?.fields?.[activeTabConfig.name]?.fields - ? permissions?.fields?.[activeTabConfig.name]?.fields - : permissions?.fields + 'name' in activeTabConfig && siblingPermissions?.[activeTabConfig.name]?.fields + ? siblingPermissions[activeTabConfig.name]?.fields + : siblingPermissions } readOnly={readOnly} schemaPath={`${schemaPath ? `${schemaPath}` : ''}${activeTabConfig.name ? `.${activeTabConfig.name}` : ''}`} diff --git a/test/access-control/collections/Disabled/index.ts b/test/access-control/collections/Disabled/index.ts new file mode 100644 index 000000000..93c0e5f4d --- /dev/null +++ b/test/access-control/collections/Disabled/index.ts @@ -0,0 +1,42 @@ +import type { CollectionConfig, Field } from 'payload/types' + +import { disabledSlug } from '../../shared.js' + +const disabledFromUpdateAccessControl = (fieldName = 'text'): Field => ({ + type: 'text', + name: fieldName, + access: { + update: () => { + return false + }, + }, +}) + +export const Disabled: CollectionConfig = { + slug: disabledSlug, + fields: [ + { + type: 'group', + name: 'group', + fields: [disabledFromUpdateAccessControl()], + }, + { + type: 'tabs', + tabs: [ + { + name: 'namedTab', + fields: [disabledFromUpdateAccessControl()], + }, + { + label: 'unnamedTab', + fields: [disabledFromUpdateAccessControl('unnamedTab')], + }, + ], + }, + { + type: 'array', + name: 'array', + fields: [disabledFromUpdateAccessControl()], + }, + ], +} diff --git a/test/access-control/config.ts b/test/access-control/config.ts index c3f4f071b..f95b75742 100644 --- a/test/access-control/config.ts +++ b/test/access-control/config.ts @@ -7,6 +7,7 @@ import type { FieldAccess } from 'payload' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { devUser } from '../credentials.js' import { TestButton } from './TestButton.js' +import { Disabled } from './collections/Disabled/index.js' import { createNotUpdateCollectionSlug, docLevelAccessSlug, @@ -533,6 +534,7 @@ export default buildConfigWithDefaults({ }, ], }, + Disabled, ], onInit: async (payload) => { await payload.create({ diff --git a/test/access-control/e2e.spec.ts b/test/access-control/e2e.spec.ts index d5a895148..49dd6485e 100644 --- a/test/access-control/e2e.spec.ts +++ b/test/access-control/e2e.spec.ts @@ -30,6 +30,7 @@ import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js' import { createNotUpdateCollectionSlug, + disabledSlug, docLevelAccessSlug, fullyRestrictedSlug, noAdminAccessEmail, @@ -67,6 +68,7 @@ describe('access control', () => { let restrictedVersionsUrl: AdminUrlUtil let userRestrictedCollectionURL: AdminUrlUtil let userRestrictedGlobalURL: AdminUrlUtil + let disabledFields: AdminUrlUtil let serverURL: string let context: BrowserContext let logoutURL: string @@ -83,6 +85,7 @@ describe('access control', () => { restrictedVersionsUrl = new AdminUrlUtil(serverURL, restrictedVersionsSlug) userRestrictedCollectionURL = new AdminUrlUtil(serverURL, userRestrictedCollectionSlug) userRestrictedGlobalURL = new AdminUrlUtil(serverURL, userRestrictedGlobalSlug) + disabledFields = new AdminUrlUtil(serverURL, disabledSlug) context = await browser.newContext() page = await context.newPage() @@ -521,6 +524,34 @@ describe('access control', () => { await expect(page.locator('.next-error-h1')).toBeVisible() }) }) + + describe('read-only from access control', () => { + test('should be read-only when update returns false', async () => { + await page.goto(disabledFields.create) + + // group field + await page.locator('#field-group__text').fill('group') + + // named tab + await page.locator('#field-namedTab__text').fill('named tab') + + // unnamed tab + await page.locator('.tabs-field__tab-button').nth(1).click() + await page.locator('#field-unnamedTab').fill('unnamed tab') + + // array field + await page.locator('#field-array button').click() + await page.locator('#field-array__0__text').fill('array row 0') + + await saveDocAndAssert(page) + + await expect(page.locator('#field-group__text')).toBeDisabled() + await expect(page.locator('#field-namedTab__text')).toBeDisabled() + await page.locator('.tabs-field__tab-button').nth(1).click() + await expect(page.locator('#field-unnamedTab')).toBeDisabled() + await expect(page.locator('#field-array__0__text')).toBeDisabled() + }) + }) }) // eslint-disable-next-line @typescript-eslint/require-await diff --git a/test/access-control/shared.ts b/test/access-control/shared.ts index e982ed0d3..c6c94d669 100644 --- a/test/access-control/shared.ts +++ b/test/access-control/shared.ts @@ -24,3 +24,5 @@ export const noAdminAccessEmail = 'no-admin-access@payloadcms.com' export const nonAdminUserEmail = 'non-admin-user@payloadcms.com' export const nonAdminUserSlug = 'non-admin-user' + +export const disabledSlug = 'disabled'