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.    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:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
105
test/fields/collections/UploadRestricted/e2e.spec.ts
Normal file
105
test/fields/collections/UploadRestricted/e2e.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
35
test/fields/collections/UploadRestricted/index.ts
Normal file
35
test/fields/collections/UploadRestricted/index.ts
Normal 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
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user