Skip to content

Commit

Permalink
feat: adds upload's relationship thumbnail (#5015)
Browse files Browse the repository at this point in the history
## Description

I've made an implementation of the feature requested here:
#3407

Before:
![CleanShot 2024-02-07 at 00 39
47](https://github.com/payloadcms/payload/assets/34719093/4b182118-41bd-47f7-af03-a0b739f7e407)

After:
![CleanShot 2024-02-07 at 00 40
17](https://github.com/payloadcms/payload/assets/34719093/d813de81-bab5-40b2-b31c-5a7ee107dabd)


- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

<!-- Please delete options that are not relevant. -->

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
  • Loading branch information
rklos authored Aug 1, 2024
1 parent 3e780b9 commit 39e110e
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 4 deletions.
1 change: 1 addition & 0 deletions docs/fields/upload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ caption="Admin panel screenshot of an Upload field"
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More](/docs/upload/overview#collection-upload-options). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
Expand Down
1 change: 1 addition & 0 deletions docs/upload/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. 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). |
| **`externalFileHeaderFilter`** | Accepts existing headers and can filter/modify them. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'

import type { RelationshipField } from '../../../../../../../../exports/types'
import type { RelationshipField, UploadField } from '../../../../../../../../exports/types'
import type { CellComponentProps } from '../../types'

import { getTranslation } from '../../../../../../../../utilities/getTranslation'
import useIntersect from '../../../../../../../hooks/useIntersect'
import { formatUseAsTitle } from '../../../../../../../hooks/useTitle'
import { useConfig } from '../../../../../../utilities/Config'
import { useListRelationships } from '../../../RelationshipProvider'
import File from '../File'
import './index.scss'

type Value = { relationTo: string; value: number | string }
const baseClass = 'relationship-cell'
const totalToShow = 3

const RelationshipCell: React.FC<CellComponentProps<RelationshipField>> = (props) => {
const RelationshipCell: React.FC<CellComponentProps<RelationshipField | UploadField>> = (props) => {
const { data: cellData, field } = props
const config = useConfig()
const { collections, routes } = config
Expand Down Expand Up @@ -68,11 +69,24 @@ const RelationshipCell: React.FC<CellComponentProps<RelationshipField>> = (props
i18n,
})

let fileField = null
if (field.type === 'upload') {
const relatedCollectionPreview = !!relatedCollection.upload.displayPreview
const fieldPreview = field.displayPreview
const previewAllowed =
fieldPreview || (relatedCollectionPreview && fieldPreview !== false)
if (previewAllowed && document) {
fileField = (
<File collection={relatedCollection} data={label} field={field} rowData={document} />
)
}
}

return (
<React.Fragment key={i}>
{document === false && `${t('untitled')} - ID: ${value}`}
{document === null && `${t('loading')}...`}
{document && (label || `${t('untitled')} - ID: ${value}`)}
{document && (fileField || label || `${t('untitled')} - ID: ${value}`)}
{values.length > i + 1 && ', '}
</React.Fragment>
)
Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/collections/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ const collectionSchema = joi.object().keys({
adminThumbnail: joi.alternatives().try(joi.string(), joi.func()),
crop: joi.bool(),
disableLocalStorage: joi.bool(),
displayPreview: joi.bool().default(false),
externalFileHeaderFilter: joi.func(),
filesRequiredOnCreate: joi.bool(),
focalPoint: joi.bool(),
Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/fields/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ export const upload = baseField.keys({
}),
}),
defaultValue: joi.alternatives().try(joi.object(), joi.func()),
displayPreview: joi.boolean().default(false),
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
maxDepth: joi.number(),
relationTo: joi.string().required(),
Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/fields/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ export type UploadField = FieldBase & {
Label?: React.ComponentType<LabelProps>
}
}
displayPreview?: boolean
filterOptions?: FilterOptions
maxDepth?: number
relationTo: string
Expand Down
2 changes: 2 additions & 0 deletions packages/payload/src/uploads/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export type IncomingUploadType = {
adminThumbnail?: GetAdminThumbnail | string
crop?: boolean
disableLocalStorage?: boolean
displayPreview?: boolean
/**
* Accepts existing headers and can filter/modify them.
*
Expand All @@ -102,6 +103,7 @@ export type Upload = {
adminThumbnail?: GetAdminThumbnail | string
crop?: boolean
disableLocalStorage?: boolean
displayPreview?: boolean
filesRequiredOnCreate?: boolean
focalPoint?: boolean
formatOptions?: ImageUploadFormatOptions
Expand Down
89 changes: 89 additions & 0 deletions test/uploads/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
focalOnlySlug,
globalWithMedia,
mediaSlug,
mediaWithRelationPreviewSlug,
mediaWithoutRelationPreviewSlug,
reduceSlug,
relationPreviewSlug,
relationSlug,
versionSlug,
} from './shared'
Expand Down Expand Up @@ -583,6 +586,67 @@ export default buildConfigWithDefaults({
drafts: true,
},
},
{
slug: mediaWithRelationPreviewSlug,
fields: [
{
name: 'title',
type: 'text',
},
],
upload: {
displayPreview: true,
},
},
{
slug: mediaWithoutRelationPreviewSlug,
fields: [
{
name: 'title',
type: 'text',
},
],
upload: true,
},
{
slug: relationPreviewSlug,
fields: [
{
name: 'imageWithPreview1',
type: 'upload',
relationTo: mediaWithRelationPreviewSlug,
},
{
name: 'imageWithPreview2',
type: 'upload',
relationTo: mediaWithRelationPreviewSlug,
displayPreview: true,
},
{
name: 'imageWithoutPreview1',
type: 'upload',
relationTo: mediaWithRelationPreviewSlug,
displayPreview: false,
},
{
name: 'imageWithoutPreview2',
type: 'upload',
relationTo: mediaWithoutRelationPreviewSlug,
},
{
name: 'imageWithPreview3',
type: 'upload',
relationTo: mediaWithoutRelationPreviewSlug,
displayPreview: true,
},
{
name: 'imageWithoutPreview3',
type: 'upload',
relationTo: mediaWithoutRelationPreviewSlug,
displayPreview: false,
},
],
},
],
globals: [
{
Expand Down Expand Up @@ -707,6 +771,31 @@ export default buildConfigWithDefaults({
name: `thumb-${imageFile.name}`,
},
})

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

const { id: uploadedImageWithoutPreview } = await payload.create({
collection: mediaWithoutRelationPreviewSlug,
data: {},
file: imageFile,
})

await payload.create({
collection: relationPreviewSlug,
data: {
imageWithPreview1: uploadedImageWithPreview,
imageWithPreview2: uploadedImageWithPreview,
imageWithoutPreview1: uploadedImageWithPreview,
imageWithoutPreview2: uploadedImageWithoutPreview,
imageWithPreview3: uploadedImageWithoutPreview,
imageWithoutPreview3: uploadedImageWithoutPreview,
},
})
},
serverURL: undefined,
})
49 changes: 48 additions & 1 deletion test/uploads/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Media } from './payload-types'

import payload from '../../packages/payload/src'
import wait from '../../packages/payload/src/utilities/wait'
import { initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers'
import { exactText, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers'
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
import { initPayloadE2E } from '../helpers/configHelpers'
import { RESTClient } from '../helpers/rest'
Expand All @@ -19,6 +19,7 @@ import {
focalOnlySlug,
globalWithMedia,
mediaSlug,
relationPreviewSlug,
relationSlug,
withMetadataSlug,
withOnlyJPEGMetadataSlug,
Expand All @@ -38,6 +39,7 @@ let focalOnlyURL: AdminUrlUtil
let withMetadataURL: AdminUrlUtil
let withoutMetadataURL: AdminUrlUtil
let withOnlyJPEGMetadataURL: AdminUrlUtil
let relationPreviewURL: AdminUrlUtil

describe('uploads', () => {
let page: Page
Expand All @@ -59,6 +61,7 @@ describe('uploads', () => {
withMetadataURL = new AdminUrlUtil(serverURL, withMetadataSlug)
withoutMetadataURL = new AdminUrlUtil(serverURL, withoutMetadataSlug)
withOnlyJPEGMetadataURL = new AdminUrlUtil(serverURL, withOnlyJPEGMetadataSlug)
relationPreviewURL = new AdminUrlUtil(serverURL, relationPreviewSlug)

const context = await browser.newContext()
page = await context.newPage()
Expand Down Expand Up @@ -536,6 +539,50 @@ describe('uploads', () => {
})
})

test('should see upload previews in relation list if allowed in config', async () => {
await page.goto(relationPreviewURL.list)

await wait(110)

// Show all columns with relations
await page.locator('.list-controls__toggle-columns').click()
await expect(page.locator('.column-selector')).toBeVisible()
const imageWithoutPreview2Button = page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Image Without Preview2'),
})
const imageWithPreview3Button = page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Image With Preview3'),
})
const imageWithoutPreview3Button = page.locator(`.column-selector .column-selector__column`, {
hasText: exactText('Image Without Preview3'),
})
await imageWithoutPreview2Button.click()
await imageWithPreview3Button.click()
await imageWithoutPreview3Button.click()

// Wait for the columns to be displayed
await expect(page.locator('.cell-imageWithoutPreview3')).toBeVisible()

// collection's displayPreview: true, field's displayPreview: unset
const relationPreview1 = page.locator('.cell-imageWithPreview1 img')
await expect(relationPreview1).toBeVisible()
// collection's displayPreview: true, field's displayPreview: true
const relationPreview2 = page.locator('.cell-imageWithPreview2 img')
await expect(relationPreview2).toBeVisible()
// collection's displayPreview: true, field's displayPreview: false
const relationPreview3 = page.locator('.cell-imageWithoutPreview1 img')
await expect(relationPreview3).toBeHidden()
// collection's displayPreview: false, field's displayPreview: unset
const relationPreview4 = page.locator('.cell-imageWithoutPreview2 img')
await expect(relationPreview4).toBeHidden()
// collection's displayPreview: false, field's displayPreview: true
const relationPreview5 = page.locator('.cell-imageWithPreview3 img')
await expect(relationPreview5).toBeVisible()
// collection's displayPreview: false, field's displayPreview: false
const relationPreview6 = page.locator('.cell-imageWithoutPreview3 img')
await expect(relationPreview6).toBeHidden()
})

describe('globals', () => {
test('should be able to crop media from a global', async () => {
await page.goto(globalURL)
Expand Down
3 changes: 3 additions & 0 deletions test/uploads/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const focalOnlySlug = 'focal-only'
export const mediaSlug = 'media'
export const reduceSlug = 'reduce'
export const relationSlug = 'relation'
export const relationPreviewSlug = 'relation-preview'
export const mediaWithRelationPreviewSlug = 'media-with-relation-preview'
export const mediaWithoutRelationPreviewSlug = 'media-without-relation-preview'
export const versionSlug = 'versions'
export const globalWithMedia = 'global-with-media'
export const animatedTypeMedia = 'animated-type-media'
Expand Down

0 comments on commit 39e110e

Please sign in to comment.