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>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -331,5 +331,7 @@ test/databaseAdapter.js
|
|||||||
test/.localstack
|
test/.localstack
|
||||||
test/google-cloud-storage
|
test/google-cloud-storage
|
||||||
test/azurestoragedata/
|
test/azurestoragedata/
|
||||||
|
/media-without-delete-access
|
||||||
|
|
||||||
|
|
||||||
licenses.csv
|
licenses.csv
|
||||||
|
|||||||
@@ -339,8 +339,7 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
|
|||||||
}
|
}
|
||||||
}, [isFormSubmitting])
|
}, [isFormSubmitting])
|
||||||
|
|
||||||
const canRemoveUpload =
|
const canRemoveUpload = docPermissions?.update
|
||||||
docPermissions?.update && 'delete' in docPermissions && docPermissions?.delete
|
|
||||||
|
|
||||||
const hasImageSizes = uploadConfig?.imageSizes?.length > 0
|
const hasImageSizes = uploadConfig?.imageSizes?.length > 0
|
||||||
const hasResizeOptions = Boolean(uploadConfig?.resizeOptions)
|
const hasResizeOptions = Boolean(uploadConfig?.resizeOptions)
|
||||||
|
|||||||
1
test/uploads/.gitignore
vendored
1
test/uploads/.gitignore
vendored
@@ -10,3 +10,4 @@ without-meta-data
|
|||||||
svg-only
|
svg-only
|
||||||
/media-gif
|
/media-gif
|
||||||
/custom-file-name-media
|
/custom-file-name-media
|
||||||
|
/media-without-delete-access
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
listViewPreviewSlug,
|
listViewPreviewSlug,
|
||||||
mediaSlug,
|
mediaSlug,
|
||||||
mediaWithoutCacheTagsSlug,
|
mediaWithoutCacheTagsSlug,
|
||||||
|
mediaWithoutDeleteAccessSlug,
|
||||||
mediaWithoutRelationPreviewSlug,
|
mediaWithoutRelationPreviewSlug,
|
||||||
mediaWithRelationPreviewSlug,
|
mediaWithRelationPreviewSlug,
|
||||||
noRestrictFileMimeTypesSlug,
|
noRestrictFileMimeTypesSlug,
|
||||||
@@ -931,6 +932,12 @@ export default buildConfigWithDefaults({
|
|||||||
staticDir: path.resolve(dirname, './svg-only'),
|
staticDir: path.resolve(dirname, './svg-only'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
slug: mediaWithoutDeleteAccessSlug,
|
||||||
|
fields: [],
|
||||||
|
upload: true,
|
||||||
|
access: { delete: () => false },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
const uploadsDir = path.resolve(dirname, './media')
|
const uploadsDir = path.resolve(dirname, './media')
|
||||||
@@ -954,6 +961,8 @@ export default buildConfigWithDefaults({
|
|||||||
file: imageFile,
|
file: imageFile,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await payload.create({ collection: mediaWithoutDeleteAccessSlug, data: {}, file: imageFile })
|
||||||
|
|
||||||
const { id: versionedImage } = await payload.create({
|
const { id: versionedImage } = await payload.create({
|
||||||
collection: versionSlug,
|
collection: versionSlug,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
listViewPreviewSlug,
|
listViewPreviewSlug,
|
||||||
mediaSlug,
|
mediaSlug,
|
||||||
mediaWithoutCacheTagsSlug,
|
mediaWithoutCacheTagsSlug,
|
||||||
|
mediaWithoutDeleteAccessSlug,
|
||||||
relationPreviewSlug,
|
relationPreviewSlug,
|
||||||
relationSlug,
|
relationSlug,
|
||||||
svgOnlySlug,
|
svgOnlySlug,
|
||||||
@@ -89,6 +90,7 @@ let stopCollectingErrorsFromPage: () => boolean
|
|||||||
let bulkUploadsURL: AdminUrlUtil
|
let bulkUploadsURL: AdminUrlUtil
|
||||||
let fileMimeTypeURL: AdminUrlUtil
|
let fileMimeTypeURL: AdminUrlUtil
|
||||||
let svgOnlyURL: AdminUrlUtil
|
let svgOnlyURL: AdminUrlUtil
|
||||||
|
let mediaWithoutDeleteAccessURL: AdminUrlUtil
|
||||||
|
|
||||||
describe('Uploads', () => {
|
describe('Uploads', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
@@ -129,6 +131,7 @@ describe('Uploads', () => {
|
|||||||
bulkUploadsURL = new AdminUrlUtil(serverURL, bulkUploadsSlug)
|
bulkUploadsURL = new AdminUrlUtil(serverURL, bulkUploadsSlug)
|
||||||
fileMimeTypeURL = new AdminUrlUtil(serverURL, fileMimeTypeSlug)
|
fileMimeTypeURL = new AdminUrlUtil(serverURL, fileMimeTypeSlug)
|
||||||
svgOnlyURL = new AdminUrlUtil(serverURL, svgOnlySlug)
|
svgOnlyURL = new AdminUrlUtil(serverURL, svgOnlySlug)
|
||||||
|
mediaWithoutDeleteAccessURL = new AdminUrlUtil(serverURL, mediaWithoutDeleteAccessSlug)
|
||||||
|
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
@@ -1599,4 +1602,22 @@ describe('Uploads', () => {
|
|||||||
|
|
||||||
await saveDocAndAssert(page, '#action-save', 'error')
|
await saveDocAndAssert(page, '#action-save', 'error')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should be able to replace the file even if the user doesnt have delete access', async () => {
|
||||||
|
const docID = (await payload.find({ collection: mediaWithoutDeleteAccessSlug, limit: 1 }))
|
||||||
|
.docs[0]?.id as string
|
||||||
|
await page.goto(mediaWithoutDeleteAccessURL.edit(docID))
|
||||||
|
const removeButton = page.locator('.file-details__remove')
|
||||||
|
await expect(removeButton).toBeVisible()
|
||||||
|
await removeButton.click()
|
||||||
|
await expect(page.locator('input[type="file"]')).toBeAttached()
|
||||||
|
await page.setInputFiles('input[type="file"]', path.join(dirname, 'test-image.jpg'))
|
||||||
|
const filename = page.locator('.file-field__filename')
|
||||||
|
await expect(filename).toHaveValue('test-image.jpg')
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
const filenameFromAPI = (
|
||||||
|
await payload.find({ collection: mediaWithoutDeleteAccessSlug, limit: 1 })
|
||||||
|
).docs[0]?.filename
|
||||||
|
expect(filenameFromAPI).toBe('test-image.jpg')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export interface Config {
|
|||||||
'externally-served-media': ExternallyServedMedia;
|
'externally-served-media': ExternallyServedMedia;
|
||||||
'uploads-1': Uploads1;
|
'uploads-1': Uploads1;
|
||||||
'uploads-2': Uploads2;
|
'uploads-2': Uploads2;
|
||||||
|
'any-images': AnyImage;
|
||||||
'admin-thumbnail-function': AdminThumbnailFunction;
|
'admin-thumbnail-function': AdminThumbnailFunction;
|
||||||
'admin-thumbnail-with-search-queries': AdminThumbnailWithSearchQuery;
|
'admin-thumbnail-with-search-queries': AdminThumbnailWithSearchQuery;
|
||||||
'admin-thumbnail-size': AdminThumbnailSize;
|
'admin-thumbnail-size': AdminThumbnailSize;
|
||||||
@@ -119,6 +120,7 @@ export interface Config {
|
|||||||
'simple-relationship': SimpleRelationship;
|
'simple-relationship': SimpleRelationship;
|
||||||
'file-mime-type': FileMimeType;
|
'file-mime-type': FileMimeType;
|
||||||
'svg-only': SvgOnly;
|
'svg-only': SvgOnly;
|
||||||
|
'media-without-delete-access': MediaWithoutDeleteAccess;
|
||||||
users: User;
|
users: User;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
@@ -157,6 +159,7 @@ export interface Config {
|
|||||||
'externally-served-media': ExternallyServedMediaSelect<false> | ExternallyServedMediaSelect<true>;
|
'externally-served-media': ExternallyServedMediaSelect<false> | ExternallyServedMediaSelect<true>;
|
||||||
'uploads-1': Uploads1Select<false> | Uploads1Select<true>;
|
'uploads-1': Uploads1Select<false> | Uploads1Select<true>;
|
||||||
'uploads-2': Uploads2Select<false> | Uploads2Select<true>;
|
'uploads-2': Uploads2Select<false> | Uploads2Select<true>;
|
||||||
|
'any-images': AnyImagesSelect<false> | AnyImagesSelect<true>;
|
||||||
'admin-thumbnail-function': AdminThumbnailFunctionSelect<false> | AdminThumbnailFunctionSelect<true>;
|
'admin-thumbnail-function': AdminThumbnailFunctionSelect<false> | AdminThumbnailFunctionSelect<true>;
|
||||||
'admin-thumbnail-with-search-queries': AdminThumbnailWithSearchQueriesSelect<false> | AdminThumbnailWithSearchQueriesSelect<true>;
|
'admin-thumbnail-with-search-queries': AdminThumbnailWithSearchQueriesSelect<false> | AdminThumbnailWithSearchQueriesSelect<true>;
|
||||||
'admin-thumbnail-size': AdminThumbnailSizeSelect<false> | AdminThumbnailSizeSelect<true>;
|
'admin-thumbnail-size': AdminThumbnailSizeSelect<false> | AdminThumbnailSizeSelect<true>;
|
||||||
@@ -178,6 +181,7 @@ export interface Config {
|
|||||||
'simple-relationship': SimpleRelationshipSelect<false> | SimpleRelationshipSelect<true>;
|
'simple-relationship': SimpleRelationshipSelect<false> | SimpleRelationshipSelect<true>;
|
||||||
'file-mime-type': FileMimeTypeSelect<false> | FileMimeTypeSelect<true>;
|
'file-mime-type': FileMimeTypeSelect<false> | FileMimeTypeSelect<true>;
|
||||||
'svg-only': SvgOnlySelect<false> | SvgOnlySelect<true>;
|
'svg-only': SvgOnlySelect<false> | SvgOnlySelect<true>;
|
||||||
|
'media-without-delete-access': MediaWithoutDeleteAccessSelect<false> | MediaWithoutDeleteAccessSelect<true>;
|
||||||
users: UsersSelect<false> | UsersSelect<true>;
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
@@ -1313,6 +1317,24 @@ export interface AdminThumbnailSize {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "any-images".
|
||||||
|
*/
|
||||||
|
export interface AnyImage {
|
||||||
|
id: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
url?: string | null;
|
||||||
|
thumbnailURL?: string | null;
|
||||||
|
filename?: string | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
filesize?: number | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
focalX?: number | null;
|
||||||
|
focalY?: number | null;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "admin-thumbnail-function".
|
* via the `definition` "admin-thumbnail-function".
|
||||||
@@ -1623,6 +1645,24 @@ export interface SvgOnly {
|
|||||||
focalX?: number | null;
|
focalX?: number | null;
|
||||||
focalY?: number | null;
|
focalY?: number | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "media-without-delete-access".
|
||||||
|
*/
|
||||||
|
export interface MediaWithoutDeleteAccess {
|
||||||
|
id: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
url?: string | null;
|
||||||
|
thumbnailURL?: string | null;
|
||||||
|
filename?: string | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
filesize?: number | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
focalX?: number | null;
|
||||||
|
focalY?: number | null;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "users".
|
* via the `definition` "users".
|
||||||
@@ -1778,6 +1818,10 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'uploads-2';
|
relationTo: 'uploads-2';
|
||||||
value: string | Uploads2;
|
value: string | Uploads2;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'any-images';
|
||||||
|
value: string | AnyImage;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'admin-thumbnail-function';
|
relationTo: 'admin-thumbnail-function';
|
||||||
value: string | AdminThumbnailFunction;
|
value: string | AdminThumbnailFunction;
|
||||||
@@ -1862,6 +1906,10 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'svg-only';
|
relationTo: 'svg-only';
|
||||||
value: string | SvgOnly;
|
value: string | SvgOnly;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'media-without-delete-access';
|
||||||
|
value: string | MediaWithoutDeleteAccess;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: string | User;
|
||||||
@@ -3019,6 +3067,23 @@ export interface Uploads2Select<T extends boolean = true> {
|
|||||||
focalX?: T;
|
focalX?: T;
|
||||||
focalY?: T;
|
focalY?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "any-images_select".
|
||||||
|
*/
|
||||||
|
export interface AnyImagesSelect<T extends boolean = true> {
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
url?: T;
|
||||||
|
thumbnailURL?: T;
|
||||||
|
filename?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
focalX?: T;
|
||||||
|
focalY?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "admin-thumbnail-function_select".
|
* via the `definition` "admin-thumbnail-function_select".
|
||||||
@@ -3386,6 +3451,23 @@ export interface SvgOnlySelect<T extends boolean = true> {
|
|||||||
focalX?: T;
|
focalX?: T;
|
||||||
focalY?: T;
|
focalY?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "media-without-delete-access_select".
|
||||||
|
*/
|
||||||
|
export interface MediaWithoutDeleteAccessSelect<T extends boolean = true> {
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
url?: T;
|
||||||
|
thumbnailURL?: T;
|
||||||
|
filename?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
focalX?: T;
|
||||||
|
focalY?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "users_select".
|
* via the `definition` "users_select".
|
||||||
|
|||||||
@@ -40,3 +40,4 @@ export const bulkUploadsSlug = 'bulk-uploads'
|
|||||||
export const fileMimeTypeSlug = 'file-mime-type'
|
export const fileMimeTypeSlug = 'file-mime-type'
|
||||||
export const svgOnlySlug = 'svg-only'
|
export const svgOnlySlug = 'svg-only'
|
||||||
export const anyImagesSlug = 'any-images'
|
export const anyImagesSlug = 'any-images'
|
||||||
|
export const mediaWithoutDeleteAccessSlug = 'media-without-delete-access'
|
||||||
|
|||||||
Reference in New Issue
Block a user