feat: join field on upload fields (#8379)

This PR makes it possible to use the new `join` field in connection with
an `upload` field. Previously `join` was reserved only for
relationships.
This commit is contained in:
Dan Ribbens
2024-09-30 13:12:30 -04:00
committed by GitHub
parent 3847428f0a
commit 3f375cc6ee
15 changed files with 207 additions and 25 deletions

1
test/joins/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/uploads/

View File

@@ -1,6 +1,6 @@
import type { CollectionConfig } from 'payload'
import { categoriesSlug, postsSlug } from '../shared.js'
import { categoriesSlug, postsSlug, uploadsSlug } from '../shared.js'
export const Posts: CollectionConfig = {
slug: postsSlug,
@@ -13,6 +13,11 @@ export const Posts: CollectionConfig = {
name: 'title',
type: 'text',
},
{
name: 'upload',
type: 'upload',
relationTo: uploadsSlug,
},
{
name: 'category',
type: 'relationship',

View File

@@ -0,0 +1,23 @@
import type { CollectionConfig } from 'payload'
import path from 'path'
import { fileURLToPath } from 'url'
import { uploadsSlug } from '../shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export const Uploads: CollectionConfig = {
slug: uploadsSlug,
fields: [
{
name: 'relatedPosts',
type: 'join',
collection: 'posts',
on: 'upload',
},
],
upload: {
staticDir: path.resolve(dirname, '../uploads'),
},
}

View File

@@ -4,6 +4,7 @@ import path from 'path'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { Categories } from './collections/Categories.js'
import { Posts } from './collections/Posts.js'
import { Uploads } from './collections/Uploads.js'
import { seed } from './seed.js'
import { localizedCategoriesSlug, localizedPostsSlug } from './shared.js'
@@ -14,6 +15,7 @@ export default buildConfigWithDefaults({
collections: [
Posts,
Categories,
Uploads,
{
slug: localizedPostsSlug,
admin: {

View File

@@ -10,7 +10,7 @@ import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { navigateToDoc } from '../helpers/e2e/navigateToDoc.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
import { categoriesSlug, postsSlug } from './shared.js'
import { categoriesSlug, postsSlug, uploadsSlug } from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -18,6 +18,7 @@ const dirname = path.dirname(filename)
test.describe('Admin Panel', () => {
let page: Page
let categoriesURL: AdminUrlUtil
let uploadsURL: AdminUrlUtil
let postsURL: AdminUrlUtil
test.beforeAll(async ({ browser }, testInfo) => {
@@ -26,6 +27,7 @@ test.describe('Admin Panel', () => {
const { payload, serverURL } = await initPayloadE2ENoConfig({ dirname })
postsURL = new AdminUrlUtil(serverURL, postsSlug)
categoriesURL = new AdminUrlUtil(serverURL, categoriesSlug)
uploadsURL = new AdminUrlUtil(serverURL, uploadsSlug)
const context = await browser.newContext()
page = await context.newPage()
@@ -183,4 +185,65 @@ test.describe('Admin Panel', () => {
await expect(joinField).toBeVisible()
await expect(joinField.locator('.relationship-table tbody tr')).toBeHidden()
})
test('should update relationship table when new upload is created', async () => {
await navigateToDoc(page, uploadsURL)
const joinField = page.locator('.field-type.join').first()
await expect(joinField).toBeVisible()
const addButton = joinField.locator('.relationship-table__actions button.doc-drawer__toggler', {
hasText: exactText('Add new'),
})
await expect(addButton).toBeVisible()
await addButton.click()
const drawer = page.locator('[id^=doc-drawer_posts_1_]')
await expect(drawer).toBeVisible()
const uploadField = drawer.locator('#field-upload')
await expect(uploadField).toBeVisible()
const uploadValue = uploadField.locator('.upload-relationship-details img')
await expect(uploadValue).toBeVisible()
const titleField = drawer.locator('#field-title')
await expect(titleField).toBeVisible()
await titleField.fill('Test post with upload')
await drawer.locator('button[id="action-save"]').click()
await expect(drawer).toBeHidden()
await expect(
joinField.locator('tbody tr td:nth-child(2)', {
hasText: exactText('Test post with upload'),
}),
).toBeVisible()
})
test('should update relationship table when new upload is created', async () => {
await navigateToDoc(page, uploadsURL)
const joinField = page.locator('.field-type.join').first()
await expect(joinField).toBeVisible()
// TODO: change this to edit the first row in the join table
const addButton = joinField.locator('.relationship-table__actions button.doc-drawer__toggler', {
hasText: exactText('Add new'),
})
await expect(addButton).toBeVisible()
await addButton.click()
const drawer = page.locator('[id^=doc-drawer_posts_1_]')
await expect(drawer).toBeVisible()
const uploadField = drawer.locator('#field-upload')
await expect(uploadField).toBeVisible()
const uploadValue = uploadField.locator('.upload-relationship-details img')
await expect(uploadValue).toBeVisible()
const titleField = drawer.locator('#field-title')
await expect(titleField).toBeVisible()
await titleField.fill('Edited title for upload')
await drawer.locator('button[id="action-save"]').click()
await expect(drawer).toBeHidden()
await expect(
joinField.locator('tbody tr td:nth-child(2)', {
hasText: exactText('Edited title for upload'),
}),
).toBeVisible()
})
})

BIN
test/joins/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -1,6 +1,7 @@
import type { Payload } from 'payload'
import path from 'path'
import { getFileByPath } from 'payload'
import { fileURLToPath } from 'url'
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
@@ -9,6 +10,7 @@ import type { Category, Post } from './payload-types.js'
import { devUser } from '../credentials.js'
import { idToString } from '../helpers/idToString.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import { categoriesSlug, uploadsSlug } from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -40,19 +42,30 @@ describe('Joins Field', () => {
token = data.token
category = await payload.create({
collection: 'categories',
collection: categoriesSlug,
data: {
name: 'paginate example',
group: {},
},
})
// create an upload
const imageFilePath = path.resolve(dirname, './image.png')
const imageFile = await getFileByPath(imageFilePath)
const { id: uploadedImage } = await payload.create({
collection: uploadsSlug,
data: {},
file: imageFile,
})
categoryID = idToString(category.id, payload)
for (let i = 0; i < 15; i++) {
await createPost({
title: `test ${i}`,
category: category.id,
upload: uploadedImage,
group: {
category: category.id,
},
@@ -94,6 +107,16 @@ describe('Joins Field', () => {
expect(docs[0].category.relatedPosts.docs).toHaveLength(10)
})
it('should populate uploads in joins', async () => {
const { docs } = await payload.find({
limit: 1,
collection: 'posts',
})
expect(docs[0].upload.id).toBeDefined()
expect(docs[0].upload.relatedPosts.docs).toHaveLength(10)
})
it('should filter joins using where query', async () => {
const categoryWithPosts = await payload.findByID({
id: category.id,

View File

@@ -13,6 +13,7 @@ export interface Config {
collections: {
posts: Post;
categories: Category;
uploads: Upload;
'localized-posts': LocalizedPost;
'localized-categories': LocalizedCategory;
users: User;
@@ -54,6 +55,7 @@ export interface UserAuthOperations {
export interface Post {
id: string;
title?: string | null;
upload?: (string | null) | Upload;
category?: (string | null) | Category;
group?: {
category?: (string | null) | Category;
@@ -61,6 +63,28 @@ export interface Post {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "uploads".
*/
export interface Upload {
id: string;
relatedPosts?: {
docs?: (string | Post)[] | null;
hasNextPage?: boolean | null;
} | null;
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
* via the `definition` "categories".
@@ -138,6 +162,10 @@ export interface PayloadLockedDocument {
relationTo: 'categories';
value: string | Category;
} | null)
| ({
relationTo: 'uploads';
value: string | Upload;
} | null)
| ({
relationTo: 'localized-posts';
value: string | LocalizedPost;
@@ -150,7 +178,6 @@ export interface PayloadLockedDocument {
relationTo: 'users';
value: string | User;
} | null);
editedAt?: string | null;
globalSlug?: string | null;
user: {
relationTo: 'users';

View File

@@ -1,8 +1,15 @@
import type { Payload } from 'payload'
import path from 'path'
import { getFileByPath } from 'payload'
import { fileURLToPath } from 'url'
import { devUser } from '../credentials.js'
import { seedDB } from '../helpers/seed.js'
import { categoriesSlug, collectionSlugs, postsSlug } from './shared.js'
import { categoriesSlug, collectionSlugs, postsSlug, uploadsSlug } from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export const seed = async (_payload) => {
await _payload.create({
@@ -53,6 +60,23 @@ export const seed = async (_payload) => {
title: 'Test Post 3',
},
})
// create an upload with image.png
const imageFilePath = path.resolve(dirname, './image.png')
const imageFile = await getFileByPath(imageFilePath)
const { id: uploadedImage } = await _payload.create({
collection: uploadsSlug,
data: {},
file: imageFile,
})
// create a post that uses the upload
await _payload.create({
collection: postsSlug,
data: {
upload: uploadedImage.id,
},
})
}
export async function clearAndSeedEverything(_payload: Payload) {

View File

@@ -2,6 +2,8 @@ export const categoriesSlug = 'categories'
export const postsSlug = 'posts'
export const uploadsSlug = 'uploads'
export const localizedPostsSlug = 'localized-posts'
export const localizedCategoriesSlug = 'localized-categories'