Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ability to disable cache tags for admin thumbnails #10319

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,4 @@ test/databaseAdapter.js
/filename-compound-index
/media-with-relation-preview
/media-without-relation-preview
/media-without-cache-tags
1 change: 1 addition & 0 deletions docs/upload/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ _An asterisk denotes that an option is required._
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true |
| **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. |
| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/collections/config/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export const sanitizeCollection = async (
// sanitize fields for reserved names
sanitizeUploadFields(sanitized.fields, sanitized)

sanitized.upload.cacheTags = sanitized.upload?.cacheTags ?? true
sanitized.upload.bulkUpload = sanitized.upload?.bulkUpload ?? true
sanitized.upload.staticDir = sanitized.upload.staticDir || sanitized.slug
sanitized.admin.useAsTitle =
Expand Down
6 changes: 6 additions & 0 deletions packages/payload/src/uploads/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export type UploadConfig = {
* @default true
*/
bulkUpload?: boolean
/**
* Appends a cache tag to the image URL when fetching the thumbnail in the admin panel. It may be desirable to disable this when hosting via CDNs with strict parameters.
*
* @default true
*/
cacheTags?: boolean
/**
* Enables cropping of images.
* @default true
Expand Down
32 changes: 22 additions & 10 deletions packages/ui/src/elements/Thumbnail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const baseClass = 'thumbnail'
import type { SanitizedCollectionConfig } from 'payload'

import { File } from '../../graphics/File/index.js'
import { useIntersect } from '../../hooks/useIntersect.js'
import { ShimmerEffect } from '../ShimmerEffect/index.js'

export type ThumbnailProps = {
Expand Down Expand Up @@ -43,15 +42,20 @@ export const Thumbnail: React.FC<ThumbnailProps> = (props) => {
}
}, [fileSrc])

/**
* If an imageCacheTag is provided, append it to the fileSrc
* Check if the fileSrc already has a query string, if it does, append the imageCacheTag with an ampersand
*/
const src = imageCacheTag
? fileSrc.includes('?')
? `${fileSrc}&${imageCacheTag}`
: `${fileSrc}?${imageCacheTag}`
: fileSrc

return (
<div className={classNames}>
{fileExists === undefined && <ShimmerEffect height="100%" />}
{fileExists && (
<img
alt={filename as string}
src={`${fileSrc}${imageCacheTag ? `?${imageCacheTag}` : ''}`}
/>
)}
{fileExists && <img alt={filename as string} src={src} />}
{fileExists === false && <File />}
</div>
)
Expand Down Expand Up @@ -87,12 +91,20 @@ export function ThumbnailComponent(props: ThumbnailComponentProps) {
}
}, [fileSrc])

/**
* If an imageCacheTag is provided, append it to the fileSrc
* Check if the fileSrc already has a query string, if it does, append the imageCacheTag with an ampersand
*/
const src = imageCacheTag
? fileSrc.includes('?')
? `${fileSrc}&${imageCacheTag}`
: `${fileSrc}?${imageCacheTag}`
: fileSrc

return (
<div className={classNames}>
{fileExists === undefined && <ShimmerEffect height="100%" />}
{fileExists && (
<img alt={alt || filename} src={`${fileSrc}${imageCacheTag ? `?${imageCacheTag}` : ''}`} />
)}
{fileExists && <img alt={alt || filename} src={src} />}
{fileExists === false && <File />}
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/elements/Upload/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ export const Upload: React.FC<UploadProps> = (props) => {
enableAdjustments={showCrop || showFocalPoint}
handleRemove={canRemoveUpload ? handleFileRemoval : undefined}
hasImageSizes={hasImageSizes}
imageCacheTag={savedDocumentData.updatedAt}
imageCacheTag={uploadConfig?.cacheTags && savedDocumentData.updatedAt}
uploadConfig={uploadConfig}
/>
)}
Expand Down
27 changes: 27 additions & 0 deletions test/uploads/collections/AdminThumbnailWithSearchQueries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { CollectionConfig } from 'payload'

import path from 'path'
import { fileURLToPath } from 'url'

import { adminThumbnailWithSearchQueries } from '../../shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

export const AdminThumbnailWithSearchQueries: CollectionConfig = {
slug: adminThumbnailWithSearchQueries,
hooks: {
afterRead: [
({ doc }) => {
return {
...doc,
// Test that URLs with additional queries are handled correctly
thumbnailURL: `/_next/image?url=${doc.url}&w=384&q=5`,
}
},
],
},
upload: {
staticDir: path.resolve(dirname, 'test/uploads/media'),
},
fields: [],
}
33 changes: 33 additions & 0 deletions test/uploads/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { devUser } from '../credentials.js'
import removeFiles from '../helpers/removeFiles.js'
import { AdminThumbnailFunction } from './collections/AdminThumbnailFunction/index.js'
import { AdminThumbnailSize } from './collections/AdminThumbnailSize/index.js'
import { AdminThumbnailWithSearchQueries } from './collections/AdminThumbnailWithSearchQueries/index.js'
import { CustomUploadFieldCollection } from './collections/CustomUploadField/index.js'
import { Uploads1 } from './collections/Upload1/index.js'
import { Uploads2 } from './collections/Upload2/index.js'
Expand All @@ -17,6 +18,7 @@ import {
enlargeSlug,
focalNoSizesSlug,
mediaSlug,
mediaWithoutCacheTagsSlug,
mediaWithoutRelationPreviewSlug,
mediaWithRelationPreviewSlug,
reduceSlug,
Expand Down Expand Up @@ -582,6 +584,7 @@ export default buildConfigWithDefaults({
Uploads1,
Uploads2,
AdminThumbnailFunction,
AdminThumbnailWithSearchQueries,
AdminThumbnailSize,
{
slug: 'optional-file',
Expand Down Expand Up @@ -628,6 +631,18 @@ export default buildConfigWithDefaults({
displayPreview: true,
},
},
{
slug: mediaWithoutCacheTagsSlug,
fields: [
{
name: 'title',
type: 'text',
},
],
upload: {
cacheTags: false,
},
},
{
slug: mediaWithoutRelationPreviewSlug,
fields: [
Expand Down Expand Up @@ -799,13 +814,31 @@ export default buildConfigWithDefaults({
},
})

await payload.create({
collection: AdminThumbnailWithSearchQueries.slug,
data: {},
file: {
...imageFile,
name: `searchQueries-image-${imageFile.name}`,
},
})

// Create media with and without relation preview
const { id: uploadedImageWithPreview } = await payload.create({
collection: mediaWithRelationPreviewSlug,
data: {},
file: imageFile,
})

await payload.create({
collection: mediaWithoutCacheTagsSlug,
data: {},
file: {
...imageFile,
name: `withoutCacheTags-image-${imageFile.name}`,
},
})

const { id: uploadedImageWithoutPreview } = await payload.create({
collection: mediaWithoutRelationPreviewSlug,
data: {},
Expand Down
80 changes: 80 additions & 0 deletions test/uploads/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { RESTClient } from '../helpers/rest.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
import {
adminThumbnailFunctionSlug,
adminThumbnailWithSearchQueries,
mediaWithoutCacheTagsSlug,
adminThumbnailSizeSlug,
animatedTypeMedia,
audioSlug,
Expand All @@ -48,6 +50,8 @@ let audioURL: AdminUrlUtil
let relationURL: AdminUrlUtil
let adminThumbnailSizeURL: AdminUrlUtil
let adminThumbnailFunctionURL: AdminUrlUtil
let adminThumbnailWithSearchQueriesURL: AdminUrlUtil
let mediaWithoutCacheTagsSlugURL: AdminUrlUtil
let focalOnlyURL: AdminUrlUtil
let withMetadataURL: AdminUrlUtil
let withoutMetadataURL: AdminUrlUtil
Expand All @@ -68,6 +72,11 @@ describe('Uploads', () => {
relationURL = new AdminUrlUtil(serverURL, relationSlug)
adminThumbnailSizeURL = new AdminUrlUtil(serverURL, adminThumbnailSizeSlug)
adminThumbnailFunctionURL = new AdminUrlUtil(serverURL, adminThumbnailFunctionSlug)
adminThumbnailWithSearchQueriesURL = new AdminUrlUtil(
serverURL,
adminThumbnailWithSearchQueries,
)
mediaWithoutCacheTagsSlugURL = new AdminUrlUtil(serverURL, mediaWithoutCacheTagsSlug)
focalOnlyURL = new AdminUrlUtil(serverURL, focalOnlySlug)
withMetadataURL = new AdminUrlUtil(serverURL, withMetadataSlug)
withoutMetadataURL = new AdminUrlUtil(serverURL, withoutMetadataSlug)
Expand Down Expand Up @@ -430,6 +439,77 @@ describe('Uploads', () => {
)
})

test('should render adminThumbnail when using a custom thumbnail URL with additional queries', async () => {
await page.goto(adminThumbnailWithSearchQueriesURL.list)
await page.waitForURL(adminThumbnailWithSearchQueriesURL.list)

const genericUploadImage = page.locator('tr.row-1 .thumbnail img')
// Match the URL with the regex pattern
const regexPattern = /\/_next\/image\?url=.*?&w=384&q=5/

await expect(genericUploadImage).toHaveAttribute('src', regexPattern)
})

test('should render adminThumbnail without the additional cache tag', async () => {
const imageDoc = (
await payload.find({
collection: mediaWithoutCacheTagsSlug,
depth: 0,
pagination: false,
where: {
mimeType: {
equals: 'image/png',
},
},
})
).docs[0]

await page.goto(mediaWithoutCacheTagsSlugURL.edit(imageDoc.id))

const genericUploadImage = page.locator('.file-details .thumbnail img')

const src = await genericUploadImage.getAttribute('src')

/**
* Regex matcher for date cache tags.
*
* @example it will match `?2022-01-01T00:00:00.000Z`
*/
const cacheTagPattern = /\?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/

expect(src).not.toMatch(cacheTagPattern)
})

test('should render adminThumbnail with the cache tag by default', async () => {
const imageDoc = (
await payload.find({
collection: adminThumbnailFunctionSlug,
depth: 0,
pagination: false,
where: {
mimeType: {
equals: 'image/png',
},
},
})
).docs[0]

await page.goto(adminThumbnailFunctionURL.edit(imageDoc.id))

const genericUploadImage = page.locator('.file-details .thumbnail img')

const src = await genericUploadImage.getAttribute('src')

/**
* Regex matcher for date cache tags.
*
* @example it will match `?2022-01-01T00:00:00.000Z`
*/
const cacheTagPattern = /\?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/

expect(src).toMatch(cacheTagPattern)
})

test('should render adminThumbnail when using a specific size', async () => {
await page.goto(adminThumbnailSizeURL.list)
await page.waitForURL(adminThumbnailSizeURL.list)
Expand Down
Loading
Loading