fix(ui): admin.allowCreate in upload field (#8484)

The admin.allowCreate property on the upload field was not doing what it
was supposed to do. In fact, it was doing nothing.

From now on, when set to false, the option to create a new upload from
the UI disappears.


![image](https://github.com/user-attachments/assets/f6776c4e-833c-4a65-8ea0-68edc0a57235)


![image](https://github.com/user-attachments/assets/b99f1969-1a07-4f9f-8b5e-0d5a708f7802)


![image](https://github.com/user-attachments/assets/519e19ea-f0ba-410e-8930-dd5231556bf5)


The tests cover:
- the create new button disappears.
- the option to create one by drag and drop disappears.
- the create new button inside the drawer disappears.
This commit is contained in:
Germán Jabloñski
2024-09-30 18:54:34 -03:00
committed by GitHub
parent 27b1629927
commit d80410b228
8 changed files with 185 additions and 28 deletions

View File

@@ -13,6 +13,7 @@ const baseClass = 'dropzone'
export type Props = {
readonly children?: React.ReactNode
readonly className?: string
readonly disabled?: boolean
readonly dropzoneStyle?: 'default' | 'none'
readonly multipleFiles?: boolean
readonly onChange: (e: FileList) => void
@@ -21,6 +22,7 @@ export type Props = {
export function Dropzone({
children,
className,
disabled = false,
dropzoneStyle = 'default',
multipleFiles,
onChange,
@@ -84,7 +86,7 @@ export function Dropzone({
React.useEffect(() => {
const div = dropRef.current
if (div) {
if (div && !disabled) {
div.addEventListener('dragenter', handleDragEnter)
div.addEventListener('dragleave', handleDragLeave)
div.addEventListener('dragover', handleDragOver)
@@ -101,7 +103,7 @@ export function Dropzone({
}
return () => null
}, [handleDragEnter, handleDragLeave, handleDrop, handlePaste])
}, [disabled, handleDragEnter, handleDragLeave, handleDrop, handlePaste])
const classes = [
baseClass,

View File

@@ -44,6 +44,7 @@ export const hoistQueryParamsToAnd = (where: Where, queryParams: Where) => {
}
export const ListDrawerContent: React.FC<ListDrawerProps> = ({
allowCreate = true,
collectionSlugs,
customHeader,
drawerSlug,
@@ -135,7 +136,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
}, [selectedOption, enabledCollectionConfigs])
const collectionPermissions = permissions?.collections?.[selectedCollectionConfig?.slug]
const hasCreatePermission = collectionPermissions?.create?.permission
const hasCreatePermission = collectionPermissions?.create?.permission && allowCreate
// If modal is open, get active page of upload gallery
const isOpen = isModalOpen(drawerSlug)

View File

@@ -5,6 +5,7 @@ import type { HTMLAttributes } from 'react'
import type { useSelection } from '../../providers/Selection/index.js'
export type ListDrawerProps = {
readonly allowCreate?: boolean
readonly collectionSlugs: string[]
readonly customHeader?: React.ReactNode
readonly drawerSlug?: string
@@ -31,7 +32,9 @@ export type UseListDrawer = (args: {
selectedCollection?: string
uploads?: boolean // finds all collections with upload: true
}) => [
React.FC<Pick<ListDrawerProps, 'enableRowSelections' | 'onBulkSelect' | 'onSelect'>>, // drawer
React.FC<
Pick<ListDrawerProps, 'allowCreate' | 'enableRowSelections' | 'onBulkSelect' | 'onSelect'>
>, // drawer
React.FC<Pick<ListTogglerProps, 'children' | 'className' | 'disabled'>>, // toggler
{
closeDrawer: () => void

View File

@@ -48,7 +48,6 @@ export type UploadInputProps = {
/**
* Controls the visibility of the "Create new collection" button
*/
readonly allowNewUpload?: boolean
readonly api?: string
readonly className?: string
readonly collection?: ClientCollectionConfig
@@ -80,7 +79,6 @@ export type UploadInputProps = {
export function UploadInput(props: UploadInputProps) {
const {
allowNewUpload,
api,
className,
Description,
@@ -107,6 +105,7 @@ export function UploadInput(props: UploadInputProps) {
value,
width,
} = props
const allowCreate = field?.admin?.allowCreate !== false
const [populatedDocs, setPopulatedDocs] = React.useState<
{
@@ -485,27 +484,33 @@ export function UploadInput(props: UploadInputProps) {
) : null}
{showDropzone ? (
<Dropzone multipleFiles={hasMany} onChange={onLocalFileSelection}>
<Dropzone disabled={!allowCreate} multipleFiles={hasMany} onChange={onLocalFileSelection}>
<div className={`${baseClass}__dropzoneContent`}>
<div className={`${baseClass}__dropzoneContent__buttons`}>
<Button
buttonStyle="pill"
className={`${baseClass}__createNewToggler`}
disabled={readOnly || !canCreate}
onClick={() => {
if (!readOnly) {
if (hasMany) {
onLocalFileSelection()
} else {
openCreateDocDrawer()
}
}
}}
size="small"
>
{t('general:createNew')}
</Button>
<span className={`${baseClass}__dropzoneContent__orText`}>{t('general:or')}</span>
{allowCreate && (
<>
<Button
buttonStyle="pill"
className={`${baseClass}__createNewToggler`}
disabled={readOnly || !canCreate}
onClick={() => {
if (!readOnly) {
if (hasMany) {
onLocalFileSelection()
} else {
openCreateDocDrawer()
}
}
}}
size="small"
>
{t('general:createNew')}
</Button>
<span className={`${baseClass}__dropzoneContent__orText`}>
{t('general:or')}
</span>
</>
)}
<Button
buttonStyle="pill"
className={`${baseClass}__listToggler`}
@@ -518,15 +523,18 @@ export function UploadInput(props: UploadInputProps) {
<CreateDocDrawer onSave={onDocCreate} />
<ListDrawer
allowCreate={allowCreate}
enableRowSelections={hasMany}
onBulkSelect={onListBulkSelect}
onSelect={onListSelect}
/>
</div>
<p className={`${baseClass}__dragAndDropText`}>
{t('general:or')} {t('upload:dragAndDrop')}
</p>
{allowCreate && (
<p className={`${baseClass}__dragAndDropText`}>
{t('general:or')} {t('upload:dragAndDrop')}
</p>
)}
</div>
</Dropzone>
) : (

View File

@@ -0,0 +1,105 @@
import type { Page } from '@playwright/test'
import type { PayloadTestSDK } from 'helpers/sdk/index.js'
import { expect, test } from '@playwright/test'
import path from 'path'
import { fileURLToPath } from 'url'
import type { Config } from '../../payload-types.js'
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
import { RESTClient } from '../../../helpers/rest.js'
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
import { uploadsRestricted } from '../../slugs.js'
const filename = fileURLToPath(import.meta.url)
const currentFolder = path.dirname(filename)
const dirname = path.resolve(currentFolder, '../../')
const { beforeAll, beforeEach, describe } = test
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let payload: PayloadTestSDK<Config>
let client: RESTClient
let page: Page
let serverURL: string
// If we want to make this run in parallel: test.describe.configure({ mode: 'parallel' })
let url: AdminUrlUtil
describe('Upload with restrictions', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
dirname,
// prebuild,
}))
url = new AdminUrlUtil(serverURL, uploadsRestricted)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsUploadRestrictedTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
await ensureCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsUploadRestrictedTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
if (client) {
await client.logout()
}
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
await ensureCompilationIsDone({ page, serverURL })
})
test('allowCreate = false should hide create new button and drag and drop text', async () => {
await page.goto(url.create)
const fieldWithoutRestriction = page.locator('#field-uploadWithoutRestriction')
await expect(fieldWithoutRestriction).toBeVisible()
await expect(fieldWithoutRestriction.getByRole('button', { name: 'Create New' })).toBeVisible()
await expect(fieldWithoutRestriction.getByText('or drag and drop a file')).toBeVisible()
const fieldWithAllowCreateFalse = page.locator('#field-uploadWithAllowCreateFalse')
await expect(fieldWithAllowCreateFalse).toBeVisible()
await expect(fieldWithAllowCreateFalse.getByRole('button', { name: 'Create New' })).toBeHidden()
// We could also test that the D&D functionality is disabled. But I think seeing the label
// disappear is enough. Maybe if there is some regression...
await expect(fieldWithAllowCreateFalse.getByText('or drag and drop a file')).toBeHidden()
const fieldMultipleWithAllow = page.locator('#field-uploadMultipleWithAllowCreateFalse')
await expect(fieldMultipleWithAllow).toBeVisible()
await expect(fieldMultipleWithAllow.getByRole('button', { name: 'Create New' })).toBeHidden()
await expect(fieldMultipleWithAllow.getByText('or drag and drop a file')).toBeHidden()
})
test('allowCreate = false should hide create new button in the list drawer', async () => {
await page.goto(url.create)
const fieldWithoutRestriction = page.locator('#field-uploadWithoutRestriction')
await expect(fieldWithoutRestriction).toBeVisible()
await fieldWithoutRestriction.getByRole('button', { name: 'Choose from existing' }).click()
const drawer = page.locator('.drawer__content')
await expect(drawer).toBeVisible()
const createNewHeader = page
.locator('.list-drawer__header')
.locator('button', { hasText: 'Create New' })
await expect(createNewHeader).toBeVisible()
await page.locator('.list-drawer__header-close').click()
await expect(drawer).toBeHidden()
const fieldWithAllowCreateFalse = page.locator('#field-uploadWithAllowCreateFalse')
await expect(fieldWithAllowCreateFalse).toBeVisible()
await fieldWithAllowCreateFalse.getByRole('button', { name: 'Choose from existing' }).click()
await expect(drawer).toBeVisible()
await expect(createNewHeader).toBeHidden()
})
})

View File

@@ -0,0 +1,35 @@
import type { CollectionConfig } from 'payload'
import { uploadsRestricted, uploadsSlug } from '../../slugs.js'
const Uploads: CollectionConfig = {
slug: uploadsRestricted,
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'uploadWithoutRestriction',
type: 'upload',
relationTo: uploadsSlug,
},
{
name: 'uploadWithAllowCreateFalse',
type: 'upload',
relationTo: uploadsSlug,
admin: {
allowCreate: false,
},
},
{
name: 'uploadMultipleWithAllowCreateFalse',
type: 'upload',
relationTo: uploadsSlug,
hasMany: true,
admin: { allowCreate: false },
},
],
}
export default Uploads

View File

@@ -37,6 +37,7 @@ import Uploads2 from './collections/Upload2/index.js'
import UploadsMulti from './collections/UploadMulti/index.js'
import UploadsMultiPoly from './collections/UploadMultiPoly/index.js'
import UploadsPoly from './collections/UploadPoly/index.js'
import UploadRestricted from './collections/UploadRestricted/index.js'
import Uploads3 from './collections/Uploads3/index.js'
import TabsWithRichText from './globals/TabsWithRichText.js'
import { clearAndSeedEverything } from './seed.js'
@@ -87,6 +88,7 @@ export const collectionSlugs: CollectionConfig[] = [
UploadsMulti,
UploadsPoly,
UploadsMultiPoly,
UploadRestricted,
UIFields,
]

View File

@@ -30,6 +30,7 @@ export const uploads3Slug = 'uploads3'
export const uploadsMulti = 'uploads-multi'
export const uploadsMultiPoly = 'uploads-multi-poly'
export const uploadsPoly = 'uploads-poly'
export const uploadsRestricted = 'uploads-restricted'
export const uiSlug = 'ui-fields'
export const collectionSlugs = [