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:
@@ -6,8 +6,8 @@ desc: The Join field provides the ability to work on related documents. Learn ho
|
|||||||
keywords: join, relationship, junction, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
keywords: join, relationship, junction, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||||
---
|
---
|
||||||
|
|
||||||
The Join Field is used to make Relationship fields in the opposite direction. It is used to show the relationship from
|
The Join Field is used to make Relationship and Upload fields available in the opposite direction. With a Join you can edit and view collections
|
||||||
the other side. The field itself acts as a virtual field, in that no new data is stored on the collection with a Join
|
having reference to a specific collection document. The field itself acts as a virtual field, in that no new data is stored on the collection with a Join
|
||||||
field. Instead, the Admin UI surfaces the related documents for a better editing experience and is surfaced by Payload's
|
field. Instead, the Admin UI surfaces the related documents for a better editing experience and is surfaced by Payload's
|
||||||
APIs.
|
APIs.
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ The Join field is useful in scenarios including:
|
|||||||
- To surface `Order`s for a given `Product`
|
- To surface `Order`s for a given `Product`
|
||||||
- To view and edit `Posts` belonging to a `Category`
|
- To view and edit `Posts` belonging to a `Category`
|
||||||
- To work with any bi-directional relationship data
|
- To work with any bi-directional relationship data
|
||||||
|
- Displaying where a document or upload is used in other documents
|
||||||
|
|
||||||
<LightDarkImage
|
<LightDarkImage
|
||||||
srcLight="https://payloadcms.com/images/docs/fields/join.png"
|
srcLight="https://payloadcms.com/images/docs/fields/join.png"
|
||||||
@@ -24,8 +25,8 @@ The Join field is useful in scenarios including:
|
|||||||
caption="Admin Panel screenshot of Join field"
|
caption="Admin Panel screenshot of Join field"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
For the Join field to work, you must have an existing [relationship](./relationship) field in the collection you are
|
For the Join field to work, you must have an existing [relationship](./relationship) or [upload](./upload) field in the
|
||||||
joining. This will reference the collection and path of the field of the related documents.
|
collection you are joining. This will reference the collection and path of the field of the related documents.
|
||||||
To add a Relationship Field, set the `type` to `join` in your [Field Config](./overview):
|
To add a Relationship Field, set the `type` to `join` in your [Field Config](./overview):
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -122,7 +123,7 @@ complete control over any type of relational architecture in Payload, all wrappe
|
|||||||
|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
|
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||||
| **`collection`** \* | The `slug`s having the relationship field. |
|
| **`collection`** \* | The `slug`s having the relationship field. |
|
||||||
| **`on`** \* | The relationship field name of the field that relates to collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
|
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
|
||||||
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth) |
|
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth) |
|
||||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ desc: Upload fields will allow a file to be uploaded, only from a collection sup
|
|||||||
keywords: upload, images media, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
keywords: upload, images media, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||||
---
|
---
|
||||||
|
|
||||||
The Upload Field allows for the selection of a Document from a Collection supporting [Uploads](../upload/overview), and formats the selection as a thumbnail in the Admin Panel.
|
The Upload Field allows for the selection of a Document from a Collection supporting [Uploads](../upload/overview), and
|
||||||
|
formats the selection as a thumbnail in the Admin Panel.
|
||||||
|
|
||||||
Upload fields are useful for a variety of use cases, such as:
|
Upload fields are useful for a variety of use cases, such as:
|
||||||
|
|
||||||
@@ -15,10 +16,10 @@ Upload fields are useful for a variety of use cases, such as:
|
|||||||
- To give a layout building block the ability to feature a background image
|
- To give a layout building block the ability to feature a background image
|
||||||
|
|
||||||
<LightDarkImage
|
<LightDarkImage
|
||||||
srcLight="https://payloadcms.com/images/docs/fields/upload.png"
|
srcLight="https://payloadcms.com/images/docs/fields/upload.png"
|
||||||
srcDark="https://payloadcms.com/images/docs/fields/upload-dark.png"
|
srcDark="https://payloadcms.com/images/docs/fields/upload-dark.png"
|
||||||
alt="Shows an upload field in the Payload Admin Panel"
|
alt="Shows an upload field in the Payload Admin Panel"
|
||||||
caption="Admin Panel screenshot of an Upload field"
|
caption="Admin Panel screenshot of an Upload field"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
To create an Upload Field, set the `type` to `upload` in your [Field Config](./overview):
|
To create an Upload Field, set the `type` to `upload` in your [Field Config](./overview):
|
||||||
@@ -43,7 +44,7 @@ export const MyUploadField: Field = {
|
|||||||
## Config Options
|
## Config Options
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||||
| **`*relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. <strong>Note: the related collection must be configured to support Uploads.</strong> |
|
| **`*relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. <strong>Note: the related collection must be configured to support Uploads.</strong> |
|
||||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
|
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
|
||||||
@@ -97,7 +98,7 @@ prevent all, or a `Where` query. When using a function, it will be
|
|||||||
called with an argument object with the following properties:
|
called with an argument object with the following properties:
|
||||||
|
|
||||||
| Property | Description |
|
| Property | Description |
|
||||||
| ------------- | ----------------------------------------------------------------------------------------------------- |
|
|---------------|-------------------------------------------------------------------------------------------------------|
|
||||||
| `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property |
|
| `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property |
|
||||||
| `data` | An object containing the full collection or global document currently being edited |
|
| `data` | An object containing the full collection or global document currently being edited |
|
||||||
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field |
|
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field |
|
||||||
@@ -127,3 +128,10 @@ You can learn more about writing queries [here](/docs/queries/overview).
|
|||||||
unless you call the default upload field validation function imported from{' '}
|
unless you call the default upload field validation function imported from{' '}
|
||||||
<strong>payload/shared</strong> in your validate function.
|
<strong>payload/shared</strong> in your validate function.
|
||||||
</Banner>
|
</Banner>
|
||||||
|
|
||||||
|
## Bi-directional relationships
|
||||||
|
|
||||||
|
The `upload` field on its own is used to reference documents in an upload collection. This can be considered a "one-way"
|
||||||
|
relationship. If you wish to allow an editor to visit the upload document and see where it is being used, you may use
|
||||||
|
the `join` field in the upload enabled collection. Read more about bi-directional relationships using
|
||||||
|
the [Join field](./join)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { SanitizedJoins } from '../../collections/config/types.js'
|
import type { SanitizedJoins } from '../../collections/config/types.js'
|
||||||
import type { Config } from '../../config/types.js'
|
import type { Config } from '../../config/types.js'
|
||||||
import type { JoinField, RelationshipField } from './types.js'
|
import type { JoinField, RelationshipField, UploadField } from './types.js'
|
||||||
|
|
||||||
import { APIError } from '../../errors/index.js'
|
import { APIError } from '../../errors/index.js'
|
||||||
import { InvalidFieldJoin } from '../../errors/InvalidFieldJoin.js'
|
import { InvalidFieldJoin } from '../../errors/InvalidFieldJoin.js'
|
||||||
@@ -33,7 +33,7 @@ export const sanitizeJoinField = ({
|
|||||||
if (!joinCollection) {
|
if (!joinCollection) {
|
||||||
throw new InvalidFieldJoin(field)
|
throw new InvalidFieldJoin(field)
|
||||||
}
|
}
|
||||||
let joinRelationship: RelationshipField | undefined
|
let joinRelationship: RelationshipField | UploadField
|
||||||
|
|
||||||
const pathSegments = field.on.split('.') // Split the schema path into segments
|
const pathSegments = field.on.split('.') // Split the schema path into segments
|
||||||
let currentSegmentIndex = 0
|
let currentSegmentIndex = 0
|
||||||
@@ -49,9 +49,10 @@ export const sanitizeJoinField = ({
|
|||||||
if ('name' in field && field.name === currentSegment) {
|
if ('name' in field && field.name === currentSegment) {
|
||||||
// Check if this is the last segment in the path
|
// Check if this is the last segment in the path
|
||||||
if (
|
if (
|
||||||
currentSegmentIndex === pathSegments.length - 1 &&
|
(currentSegmentIndex === pathSegments.length - 1 &&
|
||||||
'type' in field &&
|
'type' in field &&
|
||||||
field.type === 'relationship'
|
field.type === 'relationship') ||
|
||||||
|
field.type === 'upload'
|
||||||
) {
|
) {
|
||||||
joinRelationship = field // Return the matched field
|
joinRelationship = field // Return the matched field
|
||||||
next()
|
next()
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ export const TableCellProvider: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<TableCellContext.Provider
|
<TableCellContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
...contextToInherit,
|
||||||
cellData,
|
cellData,
|
||||||
cellProps,
|
cellProps,
|
||||||
columnIndex,
|
columnIndex,
|
||||||
customCellContext,
|
customCellContext,
|
||||||
rowData,
|
rowData,
|
||||||
...contextToInherit,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -153,6 +153,9 @@ export function UploadInput(props: UploadInputProps) {
|
|||||||
collectionSlug: activeRelationTo,
|
collectionSlug: activeRelationTo,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent initial retrieval of documents from running more than once
|
||||||
|
*/
|
||||||
const loadedValueDocsRef = React.useRef<boolean>(false)
|
const loadedValueDocsRef = React.useRef<boolean>(false)
|
||||||
|
|
||||||
const canCreate = useMemo(() => {
|
const canCreate = useMemo(() => {
|
||||||
@@ -388,6 +391,7 @@ export function UploadInput(props: UploadInputProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadInitialDocs() {
|
async function loadInitialDocs() {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
loadedValueDocsRef.current = true
|
||||||
const loadedDocs = await populateDocs(
|
const loadedDocs = await populateDocs(
|
||||||
Array.isArray(value) ? value : [value],
|
Array.isArray(value) ? value : [value],
|
||||||
activeRelationTo,
|
activeRelationTo,
|
||||||
@@ -398,8 +402,6 @@ export function UploadInput(props: UploadInputProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedValueDocsRef.current = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loadedValueDocsRef.current) {
|
if (!loadedValueDocsRef.current) {
|
||||||
|
|||||||
1
test/joins/.gitignore
vendored
Normal file
1
test/joins/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/uploads/
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
import { categoriesSlug, postsSlug } from '../shared.js'
|
import { categoriesSlug, postsSlug, uploadsSlug } from '../shared.js'
|
||||||
|
|
||||||
export const Posts: CollectionConfig = {
|
export const Posts: CollectionConfig = {
|
||||||
slug: postsSlug,
|
slug: postsSlug,
|
||||||
@@ -13,6 +13,11 @@ export const Posts: CollectionConfig = {
|
|||||||
name: 'title',
|
name: 'title',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'upload',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: uploadsSlug,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'category',
|
name: 'category',
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
|
|||||||
23
test/joins/collections/Uploads.ts
Normal file
23
test/joins/collections/Uploads.ts
Normal 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'),
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import path from 'path'
|
|||||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
import { Categories } from './collections/Categories.js'
|
import { Categories } from './collections/Categories.js'
|
||||||
import { Posts } from './collections/Posts.js'
|
import { Posts } from './collections/Posts.js'
|
||||||
|
import { Uploads } from './collections/Uploads.js'
|
||||||
import { seed } from './seed.js'
|
import { seed } from './seed.js'
|
||||||
import { localizedCategoriesSlug, localizedPostsSlug } from './shared.js'
|
import { localizedCategoriesSlug, localizedPostsSlug } from './shared.js'
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ export default buildConfigWithDefaults({
|
|||||||
collections: [
|
collections: [
|
||||||
Posts,
|
Posts,
|
||||||
Categories,
|
Categories,
|
||||||
|
Uploads,
|
||||||
{
|
{
|
||||||
slug: localizedPostsSlug,
|
slug: localizedPostsSlug,
|
||||||
admin: {
|
admin: {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
|||||||
import { navigateToDoc } from '../helpers/e2e/navigateToDoc.js'
|
import { navigateToDoc } from '../helpers/e2e/navigateToDoc.js'
|
||||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.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 filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@@ -18,6 +18,7 @@ const dirname = path.dirname(filename)
|
|||||||
test.describe('Admin Panel', () => {
|
test.describe('Admin Panel', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
let categoriesURL: AdminUrlUtil
|
let categoriesURL: AdminUrlUtil
|
||||||
|
let uploadsURL: AdminUrlUtil
|
||||||
let postsURL: AdminUrlUtil
|
let postsURL: AdminUrlUtil
|
||||||
|
|
||||||
test.beforeAll(async ({ browser }, testInfo) => {
|
test.beforeAll(async ({ browser }, testInfo) => {
|
||||||
@@ -26,6 +27,7 @@ test.describe('Admin Panel', () => {
|
|||||||
const { payload, serverURL } = await initPayloadE2ENoConfig({ dirname })
|
const { payload, serverURL } = await initPayloadE2ENoConfig({ dirname })
|
||||||
postsURL = new AdminUrlUtil(serverURL, postsSlug)
|
postsURL = new AdminUrlUtil(serverURL, postsSlug)
|
||||||
categoriesURL = new AdminUrlUtil(serverURL, categoriesSlug)
|
categoriesURL = new AdminUrlUtil(serverURL, categoriesSlug)
|
||||||
|
uploadsURL = new AdminUrlUtil(serverURL, uploadsSlug)
|
||||||
|
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
@@ -183,4 +185,65 @@ test.describe('Admin Panel', () => {
|
|||||||
await expect(joinField).toBeVisible()
|
await expect(joinField).toBeVisible()
|
||||||
await expect(joinField.locator('.relationship-table tbody tr')).toBeHidden()
|
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
BIN
test/joins/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@@ -1,6 +1,7 @@
|
|||||||
import type { Payload } from 'payload'
|
import type { Payload } from 'payload'
|
||||||
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { getFileByPath } from 'payload'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
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 { devUser } from '../credentials.js'
|
||||||
import { idToString } from '../helpers/idToString.js'
|
import { idToString } from '../helpers/idToString.js'
|
||||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||||
|
import { categoriesSlug, uploadsSlug } from './shared.js'
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@@ -40,19 +42,30 @@ describe('Joins Field', () => {
|
|||||||
token = data.token
|
token = data.token
|
||||||
|
|
||||||
category = await payload.create({
|
category = await payload.create({
|
||||||
collection: 'categories',
|
collection: categoriesSlug,
|
||||||
data: {
|
data: {
|
||||||
name: 'paginate example',
|
name: 'paginate example',
|
||||||
group: {},
|
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)
|
categoryID = idToString(category.id, payload)
|
||||||
|
|
||||||
for (let i = 0; i < 15; i++) {
|
for (let i = 0; i < 15; i++) {
|
||||||
await createPost({
|
await createPost({
|
||||||
title: `test ${i}`,
|
title: `test ${i}`,
|
||||||
category: category.id,
|
category: category.id,
|
||||||
|
upload: uploadedImage,
|
||||||
group: {
|
group: {
|
||||||
category: category.id,
|
category: category.id,
|
||||||
},
|
},
|
||||||
@@ -94,6 +107,16 @@ describe('Joins Field', () => {
|
|||||||
expect(docs[0].category.relatedPosts.docs).toHaveLength(10)
|
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 () => {
|
it('should filter joins using where query', async () => {
|
||||||
const categoryWithPosts = await payload.findByID({
|
const categoryWithPosts = await payload.findByID({
|
||||||
id: category.id,
|
id: category.id,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface Config {
|
|||||||
collections: {
|
collections: {
|
||||||
posts: Post;
|
posts: Post;
|
||||||
categories: Category;
|
categories: Category;
|
||||||
|
uploads: Upload;
|
||||||
'localized-posts': LocalizedPost;
|
'localized-posts': LocalizedPost;
|
||||||
'localized-categories': LocalizedCategory;
|
'localized-categories': LocalizedCategory;
|
||||||
users: User;
|
users: User;
|
||||||
@@ -54,6 +55,7 @@ export interface UserAuthOperations {
|
|||||||
export interface Post {
|
export interface Post {
|
||||||
id: string;
|
id: string;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
|
upload?: (string | null) | Upload;
|
||||||
category?: (string | null) | Category;
|
category?: (string | null) | Category;
|
||||||
group?: {
|
group?: {
|
||||||
category?: (string | null) | Category;
|
category?: (string | null) | Category;
|
||||||
@@ -61,6 +63,28 @@ export interface Post {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: 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
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "categories".
|
* via the `definition` "categories".
|
||||||
@@ -138,6 +162,10 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'categories';
|
relationTo: 'categories';
|
||||||
value: string | Category;
|
value: string | Category;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'uploads';
|
||||||
|
value: string | Upload;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'localized-posts';
|
relationTo: 'localized-posts';
|
||||||
value: string | LocalizedPost;
|
value: string | LocalizedPost;
|
||||||
@@ -150,7 +178,6 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: string | User;
|
||||||
} | null);
|
} | null);
|
||||||
editedAt?: string | null;
|
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import type { Payload } from 'payload'
|
import type { Payload } from 'payload'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
import { getFileByPath } from 'payload'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import { devUser } from '../credentials.js'
|
import { devUser } from '../credentials.js'
|
||||||
import { seedDB } from '../helpers/seed.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) => {
|
export const seed = async (_payload) => {
|
||||||
await _payload.create({
|
await _payload.create({
|
||||||
@@ -53,6 +60,23 @@ export const seed = async (_payload) => {
|
|||||||
title: 'Test Post 3',
|
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) {
|
export async function clearAndSeedEverything(_payload: Payload) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export const categoriesSlug = 'categories'
|
|||||||
|
|
||||||
export const postsSlug = 'posts'
|
export const postsSlug = 'posts'
|
||||||
|
|
||||||
|
export const uploadsSlug = 'uploads'
|
||||||
|
|
||||||
export const localizedPostsSlug = 'localized-posts'
|
export const localizedPostsSlug = 'localized-posts'
|
||||||
|
|
||||||
export const localizedCategoriesSlug = 'localized-categories'
|
export const localizedCategoriesSlug = 'localized-categories'
|
||||||
|
|||||||
Reference in New Issue
Block a user