Skip to content

Commit 3dd142c

Browse files
r1tsuupaulpopus
andauthored
fix(ui): cannot replace the file if the user does not have delete access (#13484)
Currently, if you don't have delete access to the document, the UI doesn't allow you to replace the file, which isn't expected. This is also a UI only restriction, and the API allows you do this fine. This PR makes so the "remove file" button renders even if you don't have delete access, while still ensures you have update access. --------- Co-authored-by: Paul Popus <paul@payloadcms.com>
1 parent 1909063 commit 3dd142c

File tree

7 files changed

+119
-4
lines changed

7 files changed

+119
-4
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,5 +331,7 @@ test/databaseAdapter.js
331331
test/.localstack
332332
test/google-cloud-storage
333333
test/azurestoragedata/
334+
/media-without-delete-access
335+
334336

335337
licenses.csv

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,7 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
339339
}
340340
}, [isFormSubmitting])
341341

342-
const canRemoveUpload =
343-
docPermissions?.update && 'delete' in docPermissions && docPermissions?.delete
342+
const canRemoveUpload = docPermissions?.update
344343

345344
const hasImageSizes = uploadConfig?.imageSizes?.length > 0
346345
const hasResizeOptions = Boolean(uploadConfig?.resizeOptions)

test/uploads/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ without-meta-data
1010
svg-only
1111
/media-gif
1212
/custom-file-name-media
13+
/media-without-delete-access

test/uploads/config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
listViewPreviewSlug,
3232
mediaSlug,
3333
mediaWithoutCacheTagsSlug,
34+
mediaWithoutDeleteAccessSlug,
3435
mediaWithoutRelationPreviewSlug,
3536
mediaWithRelationPreviewSlug,
3637
noRestrictFileMimeTypesSlug,
@@ -931,6 +932,12 @@ export default buildConfigWithDefaults({
931932
staticDir: path.resolve(dirname, './svg-only'),
932933
},
933934
},
935+
{
936+
slug: mediaWithoutDeleteAccessSlug,
937+
fields: [],
938+
upload: true,
939+
access: { delete: () => false },
940+
},
934941
],
935942
onInit: async (payload) => {
936943
const uploadsDir = path.resolve(dirname, './media')
@@ -954,6 +961,8 @@ export default buildConfigWithDefaults({
954961
file: imageFile,
955962
})
956963

964+
await payload.create({ collection: mediaWithoutDeleteAccessSlug, data: {}, file: imageFile })
965+
957966
const { id: versionedImage } = await payload.create({
958967
collection: versionSlug,
959968
data: {

test/uploads/e2e.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
listViewPreviewSlug,
4141
mediaSlug,
4242
mediaWithoutCacheTagsSlug,
43+
mediaWithoutDeleteAccessSlug,
4344
relationPreviewSlug,
4445
relationSlug,
4546
svgOnlySlug,
@@ -89,6 +90,7 @@ let stopCollectingErrorsFromPage: () => boolean
8990
let bulkUploadsURL: AdminUrlUtil
9091
let fileMimeTypeURL: AdminUrlUtil
9192
let svgOnlyURL: AdminUrlUtil
93+
let mediaWithoutDeleteAccessURL: AdminUrlUtil
9294

9395
describe('Uploads', () => {
9496
let page: Page
@@ -129,6 +131,7 @@ describe('Uploads', () => {
129131
bulkUploadsURL = new AdminUrlUtil(serverURL, bulkUploadsSlug)
130132
fileMimeTypeURL = new AdminUrlUtil(serverURL, fileMimeTypeSlug)
131133
svgOnlyURL = new AdminUrlUtil(serverURL, svgOnlySlug)
134+
mediaWithoutDeleteAccessURL = new AdminUrlUtil(serverURL, mediaWithoutDeleteAccessSlug)
132135

133136
const context = await browser.newContext()
134137
page = await context.newPage()
@@ -1599,4 +1602,22 @@ describe('Uploads', () => {
15991602

16001603
await saveDocAndAssert(page, '#action-save', 'error')
16011604
})
1605+
1606+
test('should be able to replace the file even if the user doesnt have delete access', async () => {
1607+
const docID = (await payload.find({ collection: mediaWithoutDeleteAccessSlug, limit: 1 }))
1608+
.docs[0]?.id as string
1609+
await page.goto(mediaWithoutDeleteAccessURL.edit(docID))
1610+
const removeButton = page.locator('.file-details__remove')
1611+
await expect(removeButton).toBeVisible()
1612+
await removeButton.click()
1613+
await expect(page.locator('input[type="file"]')).toBeAttached()
1614+
await page.setInputFiles('input[type="file"]', path.join(dirname, 'test-image.jpg'))
1615+
const filename = page.locator('.file-field__filename')
1616+
await expect(filename).toHaveValue('test-image.jpg')
1617+
await saveDocAndAssert(page)
1618+
const filenameFromAPI = (
1619+
await payload.find({ collection: mediaWithoutDeleteAccessSlug, limit: 1 })
1620+
).docs[0]?.filename
1621+
expect(filenameFromAPI).toBe('test-image.jpg')
1622+
})
16021623
})

test/uploads/payload-types.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export interface Config {
9898
'externally-served-media': ExternallyServedMedia;
9999
'uploads-1': Uploads1;
100100
'uploads-2': Uploads2;
101+
'any-images': AnyImage;
101102
'admin-thumbnail-function': AdminThumbnailFunction;
102103
'admin-thumbnail-with-search-queries': AdminThumbnailWithSearchQuery;
103104
'admin-thumbnail-size': AdminThumbnailSize;
@@ -119,6 +120,7 @@ export interface Config {
119120
'simple-relationship': SimpleRelationship;
120121
'file-mime-type': FileMimeType;
121122
'svg-only': SvgOnly;
123+
'media-without-delete-access': MediaWithoutDeleteAccess;
122124
users: User;
123125
'payload-locked-documents': PayloadLockedDocument;
124126
'payload-preferences': PayloadPreference;
@@ -157,6 +159,7 @@ export interface Config {
157159
'externally-served-media': ExternallyServedMediaSelect<false> | ExternallyServedMediaSelect<true>;
158160
'uploads-1': Uploads1Select<false> | Uploads1Select<true>;
159161
'uploads-2': Uploads2Select<false> | Uploads2Select<true>;
162+
'any-images': AnyImagesSelect<false> | AnyImagesSelect<true>;
160163
'admin-thumbnail-function': AdminThumbnailFunctionSelect<false> | AdminThumbnailFunctionSelect<true>;
161164
'admin-thumbnail-with-search-queries': AdminThumbnailWithSearchQueriesSelect<false> | AdminThumbnailWithSearchQueriesSelect<true>;
162165
'admin-thumbnail-size': AdminThumbnailSizeSelect<false> | AdminThumbnailSizeSelect<true>;
@@ -178,6 +181,7 @@ export interface Config {
178181
'simple-relationship': SimpleRelationshipSelect<false> | SimpleRelationshipSelect<true>;
179182
'file-mime-type': FileMimeTypeSelect<false> | FileMimeTypeSelect<true>;
180183
'svg-only': SvgOnlySelect<false> | SvgOnlySelect<true>;
184+
'media-without-delete-access': MediaWithoutDeleteAccessSelect<false> | MediaWithoutDeleteAccessSelect<true>;
181185
users: UsersSelect<false> | UsersSelect<true>;
182186
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
183187
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -1313,6 +1317,24 @@ export interface AdminThumbnailSize {
13131317
};
13141318
};
13151319
}
1320+
/**
1321+
* This interface was referenced by `Config`'s JSON-Schema
1322+
* via the `definition` "any-images".
1323+
*/
1324+
export interface AnyImage {
1325+
id: string;
1326+
updatedAt: string;
1327+
createdAt: string;
1328+
url?: string | null;
1329+
thumbnailURL?: string | null;
1330+
filename?: string | null;
1331+
mimeType?: string | null;
1332+
filesize?: number | null;
1333+
width?: number | null;
1334+
height?: number | null;
1335+
focalX?: number | null;
1336+
focalY?: number | null;
1337+
}
13161338
/**
13171339
* This interface was referenced by `Config`'s JSON-Schema
13181340
* via the `definition` "admin-thumbnail-function".
@@ -1623,6 +1645,24 @@ export interface SvgOnly {
16231645
focalX?: number | null;
16241646
focalY?: number | null;
16251647
}
1648+
/**
1649+
* This interface was referenced by `Config`'s JSON-Schema
1650+
* via the `definition` "media-without-delete-access".
1651+
*/
1652+
export interface MediaWithoutDeleteAccess {
1653+
id: string;
1654+
updatedAt: string;
1655+
createdAt: string;
1656+
url?: string | null;
1657+
thumbnailURL?: string | null;
1658+
filename?: string | null;
1659+
mimeType?: string | null;
1660+
filesize?: number | null;
1661+
width?: number | null;
1662+
height?: number | null;
1663+
focalX?: number | null;
1664+
focalY?: number | null;
1665+
}
16261666
/**
16271667
* This interface was referenced by `Config`'s JSON-Schema
16281668
* via the `definition` "users".
@@ -1778,6 +1818,10 @@ export interface PayloadLockedDocument {
17781818
relationTo: 'uploads-2';
17791819
value: string | Uploads2;
17801820
} | null)
1821+
| ({
1822+
relationTo: 'any-images';
1823+
value: string | AnyImage;
1824+
} | null)
17811825
| ({
17821826
relationTo: 'admin-thumbnail-function';
17831827
value: string | AdminThumbnailFunction;
@@ -1862,6 +1906,10 @@ export interface PayloadLockedDocument {
18621906
relationTo: 'svg-only';
18631907
value: string | SvgOnly;
18641908
} | null)
1909+
| ({
1910+
relationTo: 'media-without-delete-access';
1911+
value: string | MediaWithoutDeleteAccess;
1912+
} | null)
18651913
| ({
18661914
relationTo: 'users';
18671915
value: string | User;
@@ -3019,6 +3067,23 @@ export interface Uploads2Select<T extends boolean = true> {
30193067
focalX?: T;
30203068
focalY?: T;
30213069
}
3070+
/**
3071+
* This interface was referenced by `Config`'s JSON-Schema
3072+
* via the `definition` "any-images_select".
3073+
*/
3074+
export interface AnyImagesSelect<T extends boolean = true> {
3075+
updatedAt?: T;
3076+
createdAt?: T;
3077+
url?: T;
3078+
thumbnailURL?: T;
3079+
filename?: T;
3080+
mimeType?: T;
3081+
filesize?: T;
3082+
width?: T;
3083+
height?: T;
3084+
focalX?: T;
3085+
focalY?: T;
3086+
}
30223087
/**
30233088
* This interface was referenced by `Config`'s JSON-Schema
30243089
* via the `definition` "admin-thumbnail-function_select".
@@ -3386,6 +3451,23 @@ export interface SvgOnlySelect<T extends boolean = true> {
33863451
focalX?: T;
33873452
focalY?: T;
33883453
}
3454+
/**
3455+
* This interface was referenced by `Config`'s JSON-Schema
3456+
* via the `definition` "media-without-delete-access_select".
3457+
*/
3458+
export interface MediaWithoutDeleteAccessSelect<T extends boolean = true> {
3459+
updatedAt?: T;
3460+
createdAt?: T;
3461+
url?: T;
3462+
thumbnailURL?: T;
3463+
filename?: T;
3464+
mimeType?: T;
3465+
filesize?: T;
3466+
width?: T;
3467+
height?: T;
3468+
focalX?: T;
3469+
focalY?: T;
3470+
}
33893471
/**
33903472
* This interface was referenced by `Config`'s JSON-Schema
33913473
* via the `definition` "users_select".
@@ -3450,6 +3532,6 @@ export interface Auth {
34503532

34513533

34523534
declare module 'payload' {
3453-
// @ts-ignore
3535+
// @ts-ignore
34543536
export interface GeneratedTypes extends Config {}
3455-
}
3537+
}

test/uploads/shared.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ export const bulkUploadsSlug = 'bulk-uploads'
4040
export const fileMimeTypeSlug = 'file-mime-type'
4141
export const svgOnlySlug = 'svg-only'
4242
export const anyImagesSlug = 'any-images'
43+
export const mediaWithoutDeleteAccessSlug = 'media-without-delete-access'

0 commit comments

Comments
 (0)