Skip to content

Commit d80410b

Browse files
authored
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.
1 parent 27b1629 commit d80410b

File tree

8 files changed

+185
-28
lines changed

8 files changed

+185
-28
lines changed

packages/ui/src/elements/Dropzone/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const baseClass = 'dropzone'
1313
export type Props = {
1414
readonly children?: React.ReactNode
1515
readonly className?: string
16+
readonly disabled?: boolean
1617
readonly dropzoneStyle?: 'default' | 'none'
1718
readonly multipleFiles?: boolean
1819
readonly onChange: (e: FileList) => void
@@ -21,6 +22,7 @@ export type Props = {
2122
export function Dropzone({
2223
children,
2324
className,
25+
disabled = false,
2426
dropzoneStyle = 'default',
2527
multipleFiles,
2628
onChange,
@@ -84,7 +86,7 @@ export function Dropzone({
8486
React.useEffect(() => {
8587
const div = dropRef.current
8688

87-
if (div) {
89+
if (div && !disabled) {
8890
div.addEventListener('dragenter', handleDragEnter)
8991
div.addEventListener('dragleave', handleDragLeave)
9092
div.addEventListener('dragover', handleDragOver)
@@ -101,7 +103,7 @@ export function Dropzone({
101103
}
102104

103105
return () => null
104-
}, [handleDragEnter, handleDragLeave, handleDrop, handlePaste])
106+
}, [disabled, handleDragEnter, handleDragLeave, handleDrop, handlePaste])
105107

106108
const classes = [
107109
baseClass,

packages/ui/src/elements/ListDrawer/DrawerContent.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const hoistQueryParamsToAnd = (where: Where, queryParams: Where) => {
4444
}
4545

4646
export const ListDrawerContent: React.FC<ListDrawerProps> = ({
47+
allowCreate = true,
4748
collectionSlugs,
4849
customHeader,
4950
drawerSlug,
@@ -135,7 +136,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
135136
}, [selectedOption, enabledCollectionConfigs])
136137

137138
const collectionPermissions = permissions?.collections?.[selectedCollectionConfig?.slug]
138-
const hasCreatePermission = collectionPermissions?.create?.permission
139+
const hasCreatePermission = collectionPermissions?.create?.permission && allowCreate
139140

140141
// If modal is open, get active page of upload gallery
141142
const isOpen = isModalOpen(drawerSlug)

packages/ui/src/elements/ListDrawer/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { HTMLAttributes } from 'react'
55
import type { useSelection } from '../../providers/Selection/index.js'
66

77
export type ListDrawerProps = {
8+
readonly allowCreate?: boolean
89
readonly collectionSlugs: string[]
910
readonly customHeader?: React.ReactNode
1011
readonly drawerSlug?: string
@@ -31,7 +32,9 @@ export type UseListDrawer = (args: {
3132
selectedCollection?: string
3233
uploads?: boolean // finds all collections with upload: true
3334
}) => [
34-
React.FC<Pick<ListDrawerProps, 'enableRowSelections' | 'onBulkSelect' | 'onSelect'>>, // drawer
35+
React.FC<
36+
Pick<ListDrawerProps, 'allowCreate' | 'enableRowSelections' | 'onBulkSelect' | 'onSelect'>
37+
>, // drawer
3538
React.FC<Pick<ListTogglerProps, 'children' | 'className' | 'disabled'>>, // toggler
3639
{
3740
closeDrawer: () => void

packages/ui/src/fields/Upload/Input.tsx

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export type UploadInputProps = {
4848
/**
4949
* Controls the visibility of the "Create new collection" button
5050
*/
51-
readonly allowNewUpload?: boolean
5251
readonly api?: string
5352
readonly className?: string
5453
readonly collection?: ClientCollectionConfig
@@ -80,7 +79,6 @@ export type UploadInputProps = {
8079

8180
export function UploadInput(props: UploadInputProps) {
8281
const {
83-
allowNewUpload,
8482
api,
8583
className,
8684
Description,
@@ -107,6 +105,7 @@ export function UploadInput(props: UploadInputProps) {
107105
value,
108106
width,
109107
} = props
108+
const allowCreate = field?.admin?.allowCreate !== false
110109

111110
const [populatedDocs, setPopulatedDocs] = React.useState<
112111
{
@@ -485,27 +484,33 @@ export function UploadInput(props: UploadInputProps) {
485484
) : null}
486485

487486
{showDropzone ? (
488-
<Dropzone multipleFiles={hasMany} onChange={onLocalFileSelection}>
487+
<Dropzone disabled={!allowCreate} multipleFiles={hasMany} onChange={onLocalFileSelection}>
489488
<div className={`${baseClass}__dropzoneContent`}>
490489
<div className={`${baseClass}__dropzoneContent__buttons`}>
491-
<Button
492-
buttonStyle="pill"
493-
className={`${baseClass}__createNewToggler`}
494-
disabled={readOnly || !canCreate}
495-
onClick={() => {
496-
if (!readOnly) {
497-
if (hasMany) {
498-
onLocalFileSelection()
499-
} else {
500-
openCreateDocDrawer()
501-
}
502-
}
503-
}}
504-
size="small"
505-
>
506-
{t('general:createNew')}
507-
</Button>
508-
<span className={`${baseClass}__dropzoneContent__orText`}>{t('general:or')}</span>
490+
{allowCreate && (
491+
<>
492+
<Button
493+
buttonStyle="pill"
494+
className={`${baseClass}__createNewToggler`}
495+
disabled={readOnly || !canCreate}
496+
onClick={() => {
497+
if (!readOnly) {
498+
if (hasMany) {
499+
onLocalFileSelection()
500+
} else {
501+
openCreateDocDrawer()
502+
}
503+
}
504+
}}
505+
size="small"
506+
>
507+
{t('general:createNew')}
508+
</Button>
509+
<span className={`${baseClass}__dropzoneContent__orText`}>
510+
{t('general:or')}
511+
</span>
512+
</>
513+
)}
509514
<Button
510515
buttonStyle="pill"
511516
className={`${baseClass}__listToggler`}
@@ -518,15 +523,18 @@ export function UploadInput(props: UploadInputProps) {
518523

519524
<CreateDocDrawer onSave={onDocCreate} />
520525
<ListDrawer
526+
allowCreate={allowCreate}
521527
enableRowSelections={hasMany}
522528
onBulkSelect={onListBulkSelect}
523529
onSelect={onListSelect}
524530
/>
525531
</div>
526532

527-
<p className={`${baseClass}__dragAndDropText`}>
528-
{t('general:or')} {t('upload:dragAndDrop')}
529-
</p>
533+
{allowCreate && (
534+
<p className={`${baseClass}__dragAndDropText`}>
535+
{t('general:or')} {t('upload:dragAndDrop')}
536+
</p>
537+
)}
530538
</div>
531539
</Dropzone>
532540
) : (
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { Page } from '@playwright/test'
2+
import type { PayloadTestSDK } from 'helpers/sdk/index.js'
3+
4+
import { expect, test } from '@playwright/test'
5+
import path from 'path'
6+
import { fileURLToPath } from 'url'
7+
8+
import type { Config } from '../../payload-types.js'
9+
10+
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../../../helpers.js'
11+
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
12+
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
13+
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
14+
import { RESTClient } from '../../../helpers/rest.js'
15+
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
16+
import { uploadsRestricted } from '../../slugs.js'
17+
18+
const filename = fileURLToPath(import.meta.url)
19+
const currentFolder = path.dirname(filename)
20+
const dirname = path.resolve(currentFolder, '../../')
21+
22+
const { beforeAll, beforeEach, describe } = test
23+
24+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
25+
let payload: PayloadTestSDK<Config>
26+
let client: RESTClient
27+
let page: Page
28+
let serverURL: string
29+
// If we want to make this run in parallel: test.describe.configure({ mode: 'parallel' })
30+
let url: AdminUrlUtil
31+
32+
describe('Upload with restrictions', () => {
33+
beforeAll(async ({ browser }, testInfo) => {
34+
testInfo.setTimeout(TEST_TIMEOUT_LONG)
35+
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
36+
;({ payload, serverURL } = await initPayloadE2ENoConfig({
37+
dirname,
38+
// prebuild,
39+
}))
40+
url = new AdminUrlUtil(serverURL, uploadsRestricted)
41+
42+
const context = await browser.newContext()
43+
page = await context.newPage()
44+
initPageConsoleErrorCatch(page)
45+
await reInitializeDB({
46+
serverURL,
47+
snapshotKey: 'fieldsUploadRestrictedTest',
48+
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
49+
})
50+
await ensureCompilationIsDone({ page, serverURL })
51+
})
52+
beforeEach(async () => {
53+
await reInitializeDB({
54+
serverURL,
55+
snapshotKey: 'fieldsUploadRestrictedTest',
56+
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
57+
})
58+
59+
if (client) {
60+
await client.logout()
61+
}
62+
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
63+
await client.login()
64+
65+
await ensureCompilationIsDone({ page, serverURL })
66+
})
67+
68+
test('allowCreate = false should hide create new button and drag and drop text', async () => {
69+
await page.goto(url.create)
70+
const fieldWithoutRestriction = page.locator('#field-uploadWithoutRestriction')
71+
await expect(fieldWithoutRestriction).toBeVisible()
72+
await expect(fieldWithoutRestriction.getByRole('button', { name: 'Create New' })).toBeVisible()
73+
await expect(fieldWithoutRestriction.getByText('or drag and drop a file')).toBeVisible()
74+
const fieldWithAllowCreateFalse = page.locator('#field-uploadWithAllowCreateFalse')
75+
await expect(fieldWithAllowCreateFalse).toBeVisible()
76+
await expect(fieldWithAllowCreateFalse.getByRole('button', { name: 'Create New' })).toBeHidden()
77+
// We could also test that the D&D functionality is disabled. But I think seeing the label
78+
// disappear is enough. Maybe if there is some regression...
79+
await expect(fieldWithAllowCreateFalse.getByText('or drag and drop a file')).toBeHidden()
80+
const fieldMultipleWithAllow = page.locator('#field-uploadMultipleWithAllowCreateFalse')
81+
await expect(fieldMultipleWithAllow).toBeVisible()
82+
await expect(fieldMultipleWithAllow.getByRole('button', { name: 'Create New' })).toBeHidden()
83+
await expect(fieldMultipleWithAllow.getByText('or drag and drop a file')).toBeHidden()
84+
})
85+
86+
test('allowCreate = false should hide create new button in the list drawer', async () => {
87+
await page.goto(url.create)
88+
const fieldWithoutRestriction = page.locator('#field-uploadWithoutRestriction')
89+
await expect(fieldWithoutRestriction).toBeVisible()
90+
await fieldWithoutRestriction.getByRole('button', { name: 'Choose from existing' }).click()
91+
const drawer = page.locator('.drawer__content')
92+
await expect(drawer).toBeVisible()
93+
const createNewHeader = page
94+
.locator('.list-drawer__header')
95+
.locator('button', { hasText: 'Create New' })
96+
await expect(createNewHeader).toBeVisible()
97+
await page.locator('.list-drawer__header-close').click()
98+
await expect(drawer).toBeHidden()
99+
const fieldWithAllowCreateFalse = page.locator('#field-uploadWithAllowCreateFalse')
100+
await expect(fieldWithAllowCreateFalse).toBeVisible()
101+
await fieldWithAllowCreateFalse.getByRole('button', { name: 'Choose from existing' }).click()
102+
await expect(drawer).toBeVisible()
103+
await expect(createNewHeader).toBeHidden()
104+
})
105+
})
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
import { uploadsRestricted, uploadsSlug } from '../../slugs.js'
4+
5+
const Uploads: CollectionConfig = {
6+
slug: uploadsRestricted,
7+
fields: [
8+
{
9+
name: 'text',
10+
type: 'text',
11+
},
12+
{
13+
name: 'uploadWithoutRestriction',
14+
type: 'upload',
15+
relationTo: uploadsSlug,
16+
},
17+
{
18+
name: 'uploadWithAllowCreateFalse',
19+
type: 'upload',
20+
relationTo: uploadsSlug,
21+
admin: {
22+
allowCreate: false,
23+
},
24+
},
25+
{
26+
name: 'uploadMultipleWithAllowCreateFalse',
27+
type: 'upload',
28+
relationTo: uploadsSlug,
29+
hasMany: true,
30+
admin: { allowCreate: false },
31+
},
32+
],
33+
}
34+
35+
export default Uploads

test/fields/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import Uploads2 from './collections/Upload2/index.js'
3737
import UploadsMulti from './collections/UploadMulti/index.js'
3838
import UploadsMultiPoly from './collections/UploadMultiPoly/index.js'
3939
import UploadsPoly from './collections/UploadPoly/index.js'
40+
import UploadRestricted from './collections/UploadRestricted/index.js'
4041
import Uploads3 from './collections/Uploads3/index.js'
4142
import TabsWithRichText from './globals/TabsWithRichText.js'
4243
import { clearAndSeedEverything } from './seed.js'
@@ -87,6 +88,7 @@ export const collectionSlugs: CollectionConfig[] = [
8788
UploadsMulti,
8889
UploadsPoly,
8990
UploadsMultiPoly,
91+
UploadRestricted,
9092
UIFields,
9193
]
9294

test/fields/slugs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const uploads3Slug = 'uploads3'
3030
export const uploadsMulti = 'uploads-multi'
3131
export const uploadsMultiPoly = 'uploads-multi-poly'
3232
export const uploadsPoly = 'uploads-poly'
33+
export const uploadsRestricted = 'uploads-restricted'
3334
export const uiSlug = 'ui-fields'
3435

3536
export const collectionSlugs = [

0 commit comments

Comments
 (0)