Skip to content

Commit 4d19e64

Browse files
authored
feat: adds upload's relationship thumbnail (#7473)
## Description #5015 's version for beta branch. @JessChowdhury - [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) - [X] This change requires a documentation update ## 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 - [X] I have made corresponding changes to the documentation
1 parent 3114359 commit 4d19e64

File tree

11 files changed

+230
-1
lines changed

11 files changed

+230
-1
lines changed

docs/fields/upload.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const MyUploadField: Field = {
5757
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
5858
| **`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. |
5959
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
60+
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More](/docs/upload/overview#collection-upload-options). |
6061
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
6162
| **`required`** | Require this field to have a value. |
6263
| **`admin`** | Admin-specific configuration. [Admin Options](../admin/fields#admin-options). |

docs/upload/overview.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ _An asterisk denotes that an option is required._
9494
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
9595
| **`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) |
9696
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
97+
| **`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). |
9798
| **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. |
9899
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
99100
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |

packages/payload/src/admin/elements/Cell.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type CellComponentProps = {
1919
}[]
2020
className?: string
2121
dateDisplayFormat?: DateField['admin']['date']['displayFormat']
22+
displayPreview?: boolean
2223
fieldType?: Field['type']
2324
isFieldAffectingData?: boolean
2425
label?: FormFieldBase['label']

packages/payload/src/fields/config/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ export type UploadField = {
521521
Label?: LabelComponent
522522
}
523523
}
524+
displayPreview?: boolean
524525
filterOptions?: FilterOptions
525526
/**
526527
* Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached.

packages/payload/src/uploads/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ export type UploadConfig = {
9393
* @default false
9494
*/
9595
disableLocalStorage?: boolean
96+
/**
97+
* Enable displaying preview of the uploaded file in Upload fields related to this Collection.
98+
* Can be locally overridden by `displayPreview` option in Upload field.
99+
* @default false
100+
*/
101+
displayPreview?: boolean
96102
/**
97103
* Ability to filter/modify Request Headers when fetching a file.
98104
*

packages/ui/src/elements/Table/DefaultCell/fields/Relationship/index.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useTranslation } from '../../../../../providers/Translation/index.js'
1010
import { canUseDOM } from '../../../../../utilities/canUseDOM.js'
1111
import { formatDocTitle } from '../../../../../utilities/formatDocTitle.js'
1212
import { useListRelationships } from '../../../RelationshipProvider/index.js'
13+
import { FileCell } from '../File/index.js'
1314
import './index.scss'
1415

1516
type Value = { relationTo: string; value: number | string }
@@ -23,8 +24,10 @@ export interface RelationshipCellProps extends DefaultCellComponentProps<any> {
2324

2425
export const RelationshipCell: React.FC<RelationshipCellProps> = ({
2526
cellData,
27+
fieldType,
2628
label,
2729
relationTo,
30+
...props
2831
}) => {
2932
const config = useConfig()
3033
const { collections, routes } = config
@@ -90,11 +93,30 @@ export const RelationshipCell: React.FC<RelationshipCellProps> = ({
9093
i18n,
9194
})
9295

96+
let fileField = null
97+
if (fieldType === 'upload') {
98+
const { name, customCellContext, displayPreview, schemaPath } = props
99+
const relatedCollectionPreview = !!relatedCollection.upload.displayPreview
100+
const previewAllowed =
101+
displayPreview || (relatedCollectionPreview && displayPreview !== false)
102+
if (previewAllowed && document) {
103+
fileField = (
104+
<FileCell
105+
cellData={label}
106+
customCellContext={customCellContext}
107+
name={name}
108+
rowData={document}
109+
schemaPath={schemaPath}
110+
/>
111+
)
112+
}
113+
}
114+
93115
return (
94116
<React.Fragment key={i}>
95117
{document === false && `${t('general:untitled')} - ID: ${value}`}
96118
{document === null && `${t('general:loading')}...`}
97-
{document ? label : null}
119+
{document ? fileField || label : null}
98120
{values.length > i + 1 && ', '}
99121
</React.Fragment>
100122
)

packages/ui/src/providers/ComponentMap/buildComponentMap/fields.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,7 @@ export const mapFields = (args: {
647647
}
648648

649649
cellComponentProps.relationTo = field.relationTo
650+
cellComponentProps.displayPreview = field.displayPreview
650651
fieldComponentPropsBase = uploadField
651652
break
652653
}

test/uploads/config.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import {
1616
enlargeSlug,
1717
focalNoSizesSlug,
1818
mediaSlug,
19+
mediaWithRelationPreviewSlug,
20+
mediaWithoutRelationPreviewSlug,
1921
reduceSlug,
22+
relationPreviewSlug,
2023
relationSlug,
2124
unstoredMediaSlug,
2225
versionSlug,
@@ -554,6 +557,67 @@ export default buildConfigWithDefaults({
554557
},
555558
},
556559
CustomUploadFieldCollection,
560+
{
561+
slug: mediaWithRelationPreviewSlug,
562+
fields: [
563+
{
564+
name: 'title',
565+
type: 'text',
566+
},
567+
],
568+
upload: {
569+
displayPreview: true,
570+
},
571+
},
572+
{
573+
slug: mediaWithoutRelationPreviewSlug,
574+
fields: [
575+
{
576+
name: 'title',
577+
type: 'text',
578+
},
579+
],
580+
upload: true,
581+
},
582+
{
583+
slug: relationPreviewSlug,
584+
fields: [
585+
{
586+
name: 'imageWithPreview1',
587+
type: 'upload',
588+
relationTo: mediaWithRelationPreviewSlug,
589+
},
590+
{
591+
name: 'imageWithPreview2',
592+
type: 'upload',
593+
relationTo: mediaWithRelationPreviewSlug,
594+
displayPreview: true,
595+
},
596+
{
597+
name: 'imageWithoutPreview1',
598+
type: 'upload',
599+
relationTo: mediaWithRelationPreviewSlug,
600+
displayPreview: false,
601+
},
602+
{
603+
name: 'imageWithoutPreview2',
604+
type: 'upload',
605+
relationTo: mediaWithoutRelationPreviewSlug,
606+
},
607+
{
608+
name: 'imageWithPreview3',
609+
type: 'upload',
610+
relationTo: mediaWithoutRelationPreviewSlug,
611+
displayPreview: true,
612+
},
613+
{
614+
name: 'imageWithoutPreview3',
615+
type: 'upload',
616+
relationTo: mediaWithoutRelationPreviewSlug,
617+
displayPreview: false,
618+
},
619+
],
620+
},
557621
],
558622
onInit: async (payload) => {
559623
const uploadsDir = path.resolve(dirname, './media')
@@ -675,6 +739,31 @@ export default buildConfigWithDefaults({
675739
name: `function-image-${imageFile.name}`,
676740
},
677741
})
742+
743+
// Create media with and without relation preview
744+
const { id: uploadedImageWithPreview } = await payload.create({
745+
collection: mediaWithRelationPreviewSlug,
746+
data: {},
747+
file: imageFile,
748+
})
749+
750+
const { id: uploadedImageWithoutPreview } = await payload.create({
751+
collection: mediaWithoutRelationPreviewSlug,
752+
data: {},
753+
file: imageFile,
754+
})
755+
756+
await payload.create({
757+
collection: relationPreviewSlug,
758+
data: {
759+
imageWithPreview1: uploadedImageWithPreview,
760+
imageWithPreview2: uploadedImageWithPreview,
761+
imageWithoutPreview1: uploadedImageWithPreview,
762+
imageWithoutPreview2: uploadedImageWithoutPreview,
763+
imageWithPreview3: uploadedImageWithoutPreview,
764+
imageWithoutPreview3: uploadedImageWithoutPreview,
765+
},
766+
})
678767
},
679768
serverURL: undefined,
680769
upload: {

test/uploads/e2e.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { Config, Media } from './payload-types.js'
1010

1111
import {
1212
ensureCompilationIsDone,
13+
exactText,
1314
initPageConsoleErrorCatch,
1415
openDocDrawer,
1516
saveDocAndAssert,
@@ -25,6 +26,7 @@ import {
2526
audioSlug,
2627
focalOnlySlug,
2728
mediaSlug,
29+
relationPreviewSlug,
2830
relationSlug,
2931
withMetadataSlug,
3032
withOnlyJPEGMetadataSlug,
@@ -48,6 +50,7 @@ let focalOnlyURL: AdminUrlUtil
4850
let withMetadataURL: AdminUrlUtil
4951
let withoutMetadataURL: AdminUrlUtil
5052
let withOnlyJPEGMetadataURL: AdminUrlUtil
53+
let relationPreviewURL: AdminUrlUtil
5154

5255
describe('uploads', () => {
5356
let page: Page
@@ -70,6 +73,7 @@ describe('uploads', () => {
7073
withMetadataURL = new AdminUrlUtil(serverURL, withMetadataSlug)
7174
withoutMetadataURL = new AdminUrlUtil(serverURL, withoutMetadataSlug)
7275
withOnlyJPEGMetadataURL = new AdminUrlUtil(serverURL, withOnlyJPEGMetadataSlug)
76+
relationPreviewURL = new AdminUrlUtil(serverURL, relationPreviewSlug)
7377

7478
const context = await browser.newContext()
7579
page = await context.newPage()
@@ -594,4 +598,48 @@ describe('uploads', () => {
594598
expect(redDoc.sizes.focalTest.filesize).toEqual(1598)
595599
})
596600
})
601+
602+
test('should see upload previews in relation list if allowed in config', async () => {
603+
await page.goto(relationPreviewURL.list)
604+
605+
await wait(110)
606+
607+
// Show all columns with relations
608+
await page.locator('.list-controls__toggle-columns').click()
609+
await expect(page.locator('.column-selector')).toBeVisible()
610+
const imageWithoutPreview2Button = page.locator(`.column-selector .column-selector__column`, {
611+
hasText: exactText('Image Without Preview2'),
612+
})
613+
const imageWithPreview3Button = page.locator(`.column-selector .column-selector__column`, {
614+
hasText: exactText('Image With Preview3'),
615+
})
616+
const imageWithoutPreview3Button = page.locator(`.column-selector .column-selector__column`, {
617+
hasText: exactText('Image Without Preview3'),
618+
})
619+
await imageWithoutPreview2Button.click()
620+
await imageWithPreview3Button.click()
621+
await imageWithoutPreview3Button.click()
622+
623+
// Wait for the columns to be displayed
624+
await expect(page.locator('.cell-imageWithoutPreview3')).toBeVisible()
625+
626+
// collection's displayPreview: true, field's displayPreview: unset
627+
const relationPreview1 = page.locator('.cell-imageWithPreview1 img')
628+
await expect(relationPreview1).toBeVisible()
629+
// collection's displayPreview: true, field's displayPreview: true
630+
const relationPreview2 = page.locator('.cell-imageWithPreview2 img')
631+
await expect(relationPreview2).toBeVisible()
632+
// collection's displayPreview: true, field's displayPreview: false
633+
const relationPreview3 = page.locator('.cell-imageWithoutPreview1 img')
634+
await expect(relationPreview3).toBeHidden()
635+
// collection's displayPreview: false, field's displayPreview: unset
636+
const relationPreview4 = page.locator('.cell-imageWithoutPreview2 img')
637+
await expect(relationPreview4).toBeHidden()
638+
// collection's displayPreview: false, field's displayPreview: true
639+
const relationPreview5 = page.locator('.cell-imageWithPreview3 img')
640+
await expect(relationPreview5).toBeVisible()
641+
// collection's displayPreview: false, field's displayPreview: false
642+
const relationPreview6 = page.locator('.cell-imageWithoutPreview3 img')
643+
await expect(relationPreview6).toBeHidden()
644+
})
597645
})

test/uploads/payload-types.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export interface Config {
3737
'required-file': RequiredFile;
3838
versions: Version;
3939
'custom-upload-field': CustomUploadField;
40+
'media-with-relation-preview': MediaWithRelationPreview;
41+
'media-without-relation-preview': MediaWithoutRelationPreview;
42+
'relation-preview': RelationPreview;
4043
users: User;
4144
'payload-preferences': PayloadPreference;
4245
'payload-migrations': PayloadMigration;
@@ -949,6 +952,59 @@ export interface CustomUploadField {
949952
focalX?: number | null;
950953
focalY?: number | null;
951954
}
955+
/**
956+
* This interface was referenced by `Config`'s JSON-Schema
957+
* via the `definition` "media-with-relation-preview".
958+
*/
959+
export interface MediaWithRelationPreview {
960+
id: string;
961+
title?: string | null;
962+
updatedAt: string;
963+
createdAt: string;
964+
url?: string | null;
965+
thumbnailURL?: string | null;
966+
filename?: string | null;
967+
mimeType?: string | null;
968+
filesize?: number | null;
969+
width?: number | null;
970+
height?: number | null;
971+
focalX?: number | null;
972+
focalY?: number | null;
973+
}
974+
/**
975+
* This interface was referenced by `Config`'s JSON-Schema
976+
* via the `definition` "media-without-relation-preview".
977+
*/
978+
export interface MediaWithoutRelationPreview {
979+
id: string;
980+
title?: string | null;
981+
updatedAt: string;
982+
createdAt: string;
983+
url?: string | null;
984+
thumbnailURL?: string | null;
985+
filename?: string | null;
986+
mimeType?: string | null;
987+
filesize?: number | null;
988+
width?: number | null;
989+
height?: number | null;
990+
focalX?: number | null;
991+
focalY?: number | null;
992+
}
993+
/**
994+
* This interface was referenced by `Config`'s JSON-Schema
995+
* via the `definition` "relation-preview".
996+
*/
997+
export interface RelationPreview {
998+
id: string;
999+
imageWithPreview1?: string | MediaWithRelationPreview | null;
1000+
imageWithPreview2?: string | MediaWithRelationPreview | null;
1001+
imageWithoutPreview1?: string | MediaWithRelationPreview | null;
1002+
imageWithoutPreview2?: string | MediaWithoutRelationPreview | null;
1003+
imageWithPreview3?: string | MediaWithoutRelationPreview | null;
1004+
imageWithoutPreview3?: string | MediaWithoutRelationPreview | null;
1005+
updatedAt: string;
1006+
createdAt: string;
1007+
}
9521008
/**
9531009
* This interface was referenced by `Config`'s JSON-Schema
9541010
* via the `definition` "users".

0 commit comments

Comments
 (0)