From 2ad73401546ef6608fd67d1f00b537f149640d6a Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Wed, 8 Nov 2023 17:28:35 -0500 Subject: [PATCH] fix(live-preview): field recursion and relationship population (#4045) --- .vscode/launch.json | 7 + packages/live-preview/src/mergeData.ts | 13 +- packages/live-preview/src/promise.ts | 17 +- packages/live-preview/src/traverseFields.ts | 182 +++--- .../views/LivePreview/Context/index.tsx | 2 +- test/live-preview/collections/Pages.ts | 42 ++ test/live-preview/fields/link.ts | 2 +- test/live-preview/int.spec.ts | 402 ++++++++++--- .../next-app/app/_components/Blocks/index.tsx | 2 +- .../app/_components/Media/Image/index.tsx | 4 +- .../next-app/app/_heros/HighImpact/index.tsx | 18 +- .../next-app/app/_heros/LowImpact/index.tsx | 2 +- test/live-preview/next-app/payload-types.ts | 518 ++++++++-------- test/live-preview/payload-types.ts | 558 ++++++++++-------- test/live-preview/seed/home.ts | 11 +- test/live-preview/seed/post-1.ts | 2 +- test/live-preview/seed/post-2.ts | 2 +- test/live-preview/seed/post-3.ts | 2 +- 18 files changed, 1096 insertions(+), 690 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index a624884534..71af47b7ec 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -64,6 +64,13 @@ "NODE_ENV": "production" } }, + { + "command": "pnpm run test:int live-preview", + "cwd": "${workspaceFolder}", + "name": "Live Preview Integration", + "request": "launch", + "type": "node-terminal" + }, { "command": "ts-node ./packages/payload/src/bin/index.ts build", "env": { diff --git a/packages/live-preview/src/mergeData.ts b/packages/live-preview/src/mergeData.ts index d522311559..2e1e26ad8f 100644 --- a/packages/live-preview/src/mergeData.ts +++ b/packages/live-preview/src/mergeData.ts @@ -8,6 +8,7 @@ export type MergeLiveDataArgs = { fieldSchema: ReturnType incomingData: Partial initialData: T + returnNumberOfRequests?: boolean serverURL: string } @@ -17,8 +18,13 @@ export const mergeData = async ({ fieldSchema, incomingData, initialData, + returnNumberOfRequests, serverURL, -}: MergeLiveDataArgs): Promise => { +}: MergeLiveDataArgs): Promise< + T & { + _numberOfRequests?: number + } +> => { const result = { ...initialData } const populationPromises: Promise[] = [] @@ -35,5 +41,8 @@ export const mergeData = async ({ await Promise.all(populationPromises) - return result + return { + ...result, + ...(returnNumberOfRequests ? { _numberOfRequests: populationPromises.length } : {}), + } } diff --git a/packages/live-preview/src/promise.ts b/packages/live-preview/src/promise.ts index 60dd250e81..fdf71442c9 100644 --- a/packages/live-preview/src/promise.ts +++ b/packages/live-preview/src/promise.ts @@ -17,9 +17,20 @@ export const promise = async ({ ref, serverURL, }: Args): Promise => { - const res: any = await fetch( - `${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}`, - ).then((res) => res.json()) + const url = `${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}` + + let res: any = null + + try { + res = await fetch(url, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }).then((res) => res.json()) + } catch (err) { + console.error(err) // eslint-disable-line no-console + } ref[accessor] = res } diff --git a/packages/live-preview/src/traverseFields.ts b/packages/live-preview/src/traverseFields.ts index dfe9a9421a..0d36907351 100644 --- a/packages/live-preview/src/traverseFields.ts +++ b/packages/live-preview/src/traverseFields.ts @@ -15,38 +15,39 @@ type Args = { export const traverseFields = ({ apiRoute, depth, - fieldSchema, + fieldSchema: fieldSchemas, incomingData, populationPromises, result, serverURL, }: Args): void => { - fieldSchema.forEach((fieldJSON) => { - if ('name' in fieldJSON && typeof fieldJSON.name === 'string') { - const fieldName = fieldJSON.name + fieldSchemas.forEach((fieldSchema) => { + if ('name' in fieldSchema && typeof fieldSchema.name === 'string') { + const fieldName = fieldSchema.name - switch (fieldJSON.type) { + switch (fieldSchema.type) { case 'array': if (Array.isArray(incomingData[fieldName])) { - result[fieldName] = incomingData[fieldName].map((row, i) => { - const hasExistingRow = - Array.isArray(result[fieldName]) && - typeof result[fieldName][i] === 'object' && - result[fieldName][i] !== null + result[fieldName] = incomingData[fieldName].map((incomingRow, i) => { + if (!result[fieldName]) { + result[fieldName] = [] + } - const newRow = hasExistingRow ? { ...result[fieldName][i] } : {} + if (!result[fieldName][i]) { + result[fieldName][i] = {} + } traverseFields({ apiRoute, depth, - fieldSchema: fieldJSON.fields, - incomingData: row, + fieldSchema: fieldSchema.fields, + incomingData: incomingRow, populationPromises, - result: newRow, + result: result[fieldName][i], serverURL, }) - return newRow + return result[fieldName][i] }) } break @@ -54,18 +55,21 @@ export const traverseFields = ({ case 'blocks': if (Array.isArray(incomingData[fieldName])) { result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => { - const incomingBlockJSON = fieldJSON.blocks[incomingBlock.blockType] + const incomingBlockJSON = fieldSchema.blocks[incomingBlock.blockType] - // Compare the index and id to determine if this block already exists in the result - // If so, we want to use the existing block as the base, otherwise take the incoming block - // Either way, we will traverse the fields of the block to populate relationships - const isExistingBlock = - Array.isArray(result[fieldName]) && - typeof result[fieldName][i] === 'object' && - result[fieldName][i] !== null && - result[fieldName][i].id === incomingBlock.id + if (!result[fieldName]) { + result[fieldName] = [] + } - const block = isExistingBlock ? result[fieldName][i] : incomingBlock + if ( + !result[fieldName][i] || + result[fieldName][i].id !== incomingBlock.id || + result[fieldName][i].blockType !== incomingBlock.blockType + ) { + result[fieldName][i] = { + blockType: incomingBlock.blockType, + } + } traverseFields({ apiRoute, @@ -73,11 +77,11 @@ export const traverseFields = ({ fieldSchema: incomingBlockJSON.fields, incomingData: incomingBlock, populationPromises, - result: block, + result: result[fieldName][i], serverURL, }) - return block + return result[fieldName][i] }) } else { result[fieldName] = [] @@ -94,7 +98,7 @@ export const traverseFields = ({ traverseFields({ apiRoute, depth, - fieldSchema: fieldJSON.fields, + fieldSchema: fieldSchema.fields, incomingData: incomingData[fieldName] || {}, populationPromises, result: result[fieldName], @@ -105,31 +109,35 @@ export const traverseFields = ({ case 'upload': case 'relationship': - if (fieldJSON.hasMany && Array.isArray(incomingData[fieldName])) { - const existingValue = Array.isArray(result[fieldName]) ? [...result[fieldName]] : [] - result[fieldName] = Array.isArray(result[fieldName]) - ? [...result[fieldName]].slice(0, incomingData[fieldName].length) - : [] + // Handle `hasMany` relationships + if (fieldSchema.hasMany && Array.isArray(incomingData[fieldName])) { + if (!result[fieldName]) { + result[fieldName] = [] + } - incomingData[fieldName].forEach((relation, i) => { + incomingData[fieldName].forEach((incomingRelation, i) => { // Handle `hasMany` polymorphic - if (Array.isArray(fieldJSON.relationTo)) { - const existingID = existingValue[i]?.value?.id - - if ( - existingID !== relation.value || - existingValue[i]?.relationTo !== relation.relationTo - ) { + if (Array.isArray(fieldSchema.relationTo)) { + // if the field doesn't exist on the result, create it + // the value will be populated later + if (!result[fieldName][i]) { result[fieldName][i] = { - relationTo: relation.relationTo, + relationTo: incomingRelation.relationTo, } + } + const oldID = result[fieldName][i]?.value?.id + const oldRelation = result[fieldName][i]?.relationTo + const newID = incomingRelation.value + const newRelation = incomingRelation.relationTo + + if (oldID !== newID || oldRelation !== newRelation) { populationPromises.push( promise({ - id: relation.value, + id: incomingRelation.value, accessor: 'value', apiRoute, - collection: relation.relationTo, + collection: newRelation, depth, ref: result[fieldName][i], serverURL, @@ -138,15 +146,13 @@ export const traverseFields = ({ } } else { // Handle `hasMany` monomorphic - const existingID = existingValue[i]?.id - - if (existingID !== relation) { + if (result[fieldName][i]?.id !== incomingRelation) { populationPromises.push( promise({ - id: relation, + id: incomingRelation, accessor: i, apiRoute, - collection: String(fieldJSON.relationTo), + collection: String(fieldSchema.relationTo), depth, ref: result[fieldName], serverURL, @@ -157,29 +163,49 @@ export const traverseFields = ({ }) } else { // Handle `hasOne` polymorphic - if (Array.isArray(fieldJSON.relationTo)) { + if (Array.isArray(fieldSchema.relationTo)) { + // if the field doesn't exist on the result, create it + // the value will be populated later + if (!result[fieldName]) { + result[fieldName] = { + relationTo: incomingData[fieldName]?.relationTo, + } + } + const hasNewValue = - typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null + incomingData[fieldName] && + typeof incomingData[fieldName] === 'object' && + incomingData[fieldName] !== null + const hasOldValue = - typeof result[fieldName] === 'object' && result[fieldName] !== null + result[fieldName] && + typeof result[fieldName] === 'object' && + result[fieldName] !== null + + const newID = hasNewValue + ? typeof incomingData[fieldName].value === 'object' + ? incomingData[fieldName].value.id + : incomingData[fieldName].value + : '' + + const oldID = hasOldValue + ? typeof result[fieldName].value === 'object' + ? result[fieldName].value.id + : result[fieldName].value + : '' - const newValue = hasNewValue ? incomingData[fieldName].value : '' const newRelation = hasNewValue ? incomingData[fieldName].relationTo : '' - - const oldValue = hasOldValue ? result[fieldName].value : '' const oldRelation = hasOldValue ? result[fieldName].relationTo : '' - if (newValue !== oldValue || newRelation !== oldRelation) { - if (newValue) { - if (!result[fieldName]) { - result[fieldName] = { - relationTo: newRelation, - } - } - + // if the new value/relation is different from the old value/relation + // populate the new value, otherwise leave it alone + if (newID !== oldID || newRelation !== oldRelation) { + // if the new value is not empty, populate it + // otherwise set the value to null + if (newID) { populationPromises.push( promise({ - id: newValue, + id: newID, accessor: 'value', apiRoute, collection: newRelation, @@ -188,34 +214,36 @@ export const traverseFields = ({ serverURL, }), ) + } else { + result[fieldName] = null } - } else { - result[fieldName] = null } } else { // Handle `hasOne` monomorphic - const newID: string = - (typeof incomingData[fieldName] === 'string' && incomingData[fieldName]) || - (typeof incomingData[fieldName] === 'object' && - incomingData[fieldName] !== null && + const newID: number | string | undefined = + (incomingData[fieldName] && + typeof incomingData[fieldName] === 'object' && incomingData[fieldName].id) || - '' + incomingData[fieldName] - const oldID: string = - (typeof result[fieldName] === 'string' && result[fieldName]) || - (typeof result[fieldName] === 'object' && - result[fieldName] !== null && + const oldID: number | string | undefined = + (result[fieldName] && + typeof result[fieldName] === 'object' && result[fieldName].id) || - '' + result[fieldName] + // if the new value is different from the old value + // populate the new value, otherwise leave it alone if (newID !== oldID) { + // if the new value is not empty, populate it + // otherwise set the value to null if (newID) { populationPromises.push( promise({ id: newID, accessor: fieldName, apiRoute, - collection: String(fieldJSON.relationTo), + collection: String(fieldSchema.relationTo), depth, ref: result as Record, serverURL, diff --git a/packages/payload/src/admin/components/views/LivePreview/Context/index.tsx b/packages/payload/src/admin/components/views/LivePreview/Context/index.tsx index a112fe9392..6a39cb7eb4 100644 --- a/packages/payload/src/admin/components/views/LivePreview/Context/index.tsx +++ b/packages/payload/src/admin/components/views/LivePreview/Context/index.tsx @@ -1,5 +1,5 @@ import { DndContext } from '@dnd-kit/core' -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import type { LivePreviewConfig } from '../../../../../exports/config' import type { Field } from '../../../../../fields/config/types' diff --git a/test/live-preview/collections/Pages.ts b/test/live-preview/collections/Pages.ts index 8730c60612..f04cd22046 100644 --- a/test/live-preview/collections/Pages.ts +++ b/test/live-preview/collections/Pages.ts @@ -71,5 +71,47 @@ export const Pages: CollectionConfig = { }, ], }, + // Hidden fields for testing purposes + { + name: 'relationshipPolyHasMany', + type: 'relationship', + relationTo: ['posts'], + hasMany: true, + admin: { + hidden: true, + }, + }, + { + name: 'relationshipMonoHasMany', + type: 'relationship', + relationTo: 'posts', + hasMany: true, + admin: { + hidden: true, + }, + }, + { + name: 'relationshipMonoHasOne', + type: 'relationship', + relationTo: 'posts', + admin: { + hidden: true, + }, + }, + { + name: 'arrayOfRelationships', + type: 'array', + admin: { + hidden: true, + disabled: true, + }, + fields: [ + { + name: 'relationshipWithinArray', + type: 'relationship', + relationTo: 'posts', + }, + ], + }, ], } diff --git a/test/live-preview/fields/link.ts b/test/live-preview/fields/link.ts index a1003842ea..ab6a909422 100644 --- a/test/live-preview/fields/link.ts +++ b/test/live-preview/fields/link.ts @@ -76,7 +76,7 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } = name: 'reference', label: 'Document to link to', type: 'relationship', - relationTo: ['pages'], + relationTo: ['posts', 'pages'], required: true, maxDepth: 1, admin: { diff --git a/test/live-preview/int.spec.ts b/test/live-preview/int.spec.ts index bc3230b231..2aa2ceb448 100644 --- a/test/live-preview/int.spec.ts +++ b/test/live-preview/int.spec.ts @@ -1,6 +1,6 @@ import path from 'path' -import type { Media, Page } from './payload-types' +import type { Media, Page, Post } from './payload-types' import { handleMessage } from '../../packages/live-preview/src/handleMessage' import { mergeData } from '../../packages/live-preview/src/mergeData' @@ -10,20 +10,21 @@ import { fieldSchemaToJSON } from '../../packages/payload/src/utilities/fieldSch import { initPayloadTest } from '../helpers/configHelpers' import { RESTClient } from '../helpers/rest' import { Pages } from './collections/Pages' +import { postsSlug } from './collections/Posts' import configPromise from './config' import { pagesSlug } from './shared' require('isomorphic-fetch') -let client -let serverURL - -let page: Page -let media: Media - const schemaJSON = fieldSchemaToJSON(Pages.fields) describe('Collections - Live Preview', () => { + let client + let serverURL + + let testPost: Post + let media: Media + beforeAll(async () => { const { serverURL: incomingServerURL } = await initPayloadTest({ __dirname, @@ -35,11 +36,11 @@ describe('Collections - Live Preview', () => { client = new RESTClient(config, { serverURL, defaultSlug: pagesSlug }) await client.login() - page = await payload.create({ - collection: pagesSlug, + testPost = await payload.create({ + collection: postsSlug, data: { - slug: 'home', - title: 'Test Page', + slug: 'post-1', + title: 'Test Post', }, }) @@ -63,18 +64,20 @@ describe('Collections - Live Preview', () => { event: { data: JSON.stringify({ data: { - title: 'Test Page (Change 1)', + title: 'Test Page (Changed)', }, fieldSchemaJSON: schemaJSON, type: 'payload-live-preview', }), origin: serverURL, } as MessageEvent, - initialData: page, + initialData: { + title: 'Test Page', + } as Page, serverURL, }) - expect(handledMessage.title).toEqual('Test Page (Change 1)') + expect(handledMessage.title).toEqual('Test Page (Changed)') }) it('caches `fieldSchemaJSON`', async () => { @@ -83,68 +86,89 @@ describe('Collections - Live Preview', () => { event: { data: JSON.stringify({ data: { - title: 'Test Page (Change 2)', + title: 'Test Page (Changed)', }, type: 'payload-live-preview', }), origin: serverURL, } as MessageEvent, - initialData: page, + initialData: { + title: 'Test Page', + } as Page, serverURL, }) - expect(handledMessage.title).toEqual('Test Page (Change 2)') + expect(handledMessage.title).toEqual('Test Page (Changed)') }) it('merges data', async () => { - expect(page?.id).toBeDefined() + const initialData: Partial = { + id: '123', + title: 'Test Page', + } - const mergedData = await mergeData({ - depth: 1, - fieldSchema: schemaJSON, - incomingData: page, - initialData: page, - serverURL, - }) - - expect(mergedData.id).toEqual(page.id) - }) - - it('merges strings', async () => { const mergedData = await mergeData({ depth: 1, fieldSchema: schemaJSON, incomingData: { - ...page, - title: 'Test Page (Change 3)', + title: 'Test Page (Merged)', }, - initialData: page, + initialData, serverURL, + returnNumberOfRequests: true, }) - expect(mergedData.title).toEqual('Test Page (Change 3)') + expect(mergedData.id).toEqual(initialData.id) + expect(mergedData._numberOfRequests).toEqual(0) + }) + + it('— strings - merges data', async () => { + const initialData: Partial = { + title: 'Test Page', + } + + const mergedData = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...initialData, + title: 'Test Page (Changed)', + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + expect(mergedData.title).toEqual('Test Page (Changed)') + expect(mergedData._numberOfRequests).toEqual(0) }) // TODO: this test is not working in Postgres // This is because of how relationships are handled in `mergeData` // This test passes in MongoDB, though - it.skip('adds and removes uploads', async () => { + it.skip('— uploads - adds and removes media', async () => { + const initialData: Partial = { + title: 'Test Page', + } + // Add upload const mergedData = await mergeData({ depth: 1, fieldSchema: schemaJSON, incomingData: { - ...page, + ...initialData, hero: { type: 'highImpact', media: media.id, }, }, - initialData: page, + initialData, serverURL, + returnNumberOfRequests: true, }) expect(mergedData.hero.media).toMatchObject(media) + expect(mergedData._numberOfRequests).toEqual(1) // Add upload const mergedDataWithoutUpload = await mergeData({ @@ -161,62 +185,244 @@ describe('Collections - Live Preview', () => { serverURL, }) - expect(mergedDataWithoutUpload.hero.media).toEqual(null) + expect(mergedDataWithoutUpload.hero.media).toBeFalsy() }) - it('add, reorder, and remove blocks', async () => { - // Add new blocks + it('— relationships - populates all types', async () => { + const initialData: Partial = { + title: 'Test Page', + } + const merge1 = await mergeData({ depth: 1, fieldSchema: schemaJSON, incomingData: { - ...page, - layout: [ - { - blockType: 'cta', - id: 'block-1', // use fake ID, this is a new block that is only assigned an ID on the client - richText: [ - { - type: 'paragraph', - text: 'Block 1 (Position 1)', - }, - ], - }, - { - blockType: 'cta', - id: 'block-2', // use fake ID, this is a new block that is only assigned an ID on the client - richText: [ - { - type: 'paragraph', - text: 'Block 2 (Position 2)', - }, - ], - }, - ], + ...initialData, + relationshipMonoHasOne: testPost.id, + relationshipMonoHasMany: [testPost.id], + relationshipPolyHasMany: [{ value: testPost.id, relationTo: postsSlug }], }, - initialData: page, + initialData, serverURL, + returnNumberOfRequests: true, }) - // Check that the blocks have been merged and are in the correct order - expect(merge1.layout).toHaveLength(2) - const block1 = merge1.layout[0] - expect(block1.id).toEqual('block-1') - expect(block1.richText[0].text).toEqual('Block 1 (Position 1)') - const block2 = merge1.layout[1] - expect(block2.id).toEqual('block-2') - expect(block2.richText[0].text).toEqual('Block 2 (Position 2)') + expect(merge1._numberOfRequests).toEqual(3) + expect(merge1.relationshipMonoHasOne).toMatchObject(testPost) + expect(merge1.relationshipMonoHasMany).toMatchObject([testPost]) + expect(merge1.relationshipPolyHasMany).toMatchObject([ + { value: testPost, relationTo: postsSlug }, + ]) - // Reorder the blocks using the same IDs from the previous merge + // Clear relationships const merge2 = await mergeData({ depth: 1, fieldSchema: schemaJSON, incomingData: { ...merge1, + relationshipMonoHasOne: null, + relationshipMonoHasMany: [], + relationshipPolyHasMany: [], + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + expect(merge2._numberOfRequests).toEqual(0) + expect(merge2.relationshipMonoHasOne).toBeFalsy() + expect(merge2.relationshipMonoHasMany).toEqual([]) + expect(merge2.relationshipPolyHasMany).toEqual([]) + + // Now populate the relationships again + const merge3 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...merge2, + relationshipMonoHasOne: testPost.id, + relationshipMonoHasMany: [testPost.id], + relationshipPolyHasMany: [{ value: testPost.id, relationTo: postsSlug }], + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + expect(merge3._numberOfRequests).toEqual(3) + expect(merge3.relationshipMonoHasOne).toMatchObject(testPost) + expect(merge3.relationshipMonoHasMany).toMatchObject([testPost]) + expect(merge3.relationshipPolyHasMany).toMatchObject([ + { value: testPost, relationTo: postsSlug }, + ]) + }) + + it('— relationships - populates within arrays', async () => { + const initialData: Partial = { + title: 'Test Page', + } + + const merge1 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...initialData, + arrayOfRelationships: [ + { + id: '123', + relationshipWithinArray: testPost.id, + }, + ], + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + expect(merge1._numberOfRequests).toEqual(1) + expect(merge1.arrayOfRelationships).toHaveLength(1) + expect(merge1.arrayOfRelationships).toMatchObject([ + { + id: '123', + relationshipWithinArray: testPost, + }, + ]) + + // Add a new block before the populated one, then check to see that the relationship is still populated + const merge2 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...merge1, + arrayOfRelationships: [ + { + id: '456', + relationshipWithinArray: undefined, + }, + { + id: '123', + relationshipWithinArray: testPost.id, + }, + ], + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + expect(merge2._numberOfRequests).toEqual(1) + expect(merge2.arrayOfRelationships).toHaveLength(2) + expect(merge2.arrayOfRelationships).toMatchObject([ + { + id: '456', + }, + { + id: '123', + relationshipWithinArray: testPost, + }, + ]) + }) + + it('— relationships - populates within blocks', async () => { + const block1 = (shallow?: boolean): Extract => ({ + blockType: 'cta', + id: '123', + links: [ + { + link: { + label: 'Link 1', + type: 'reference', + reference: { + relationTo: 'posts', + value: shallow ? testPost?.id : testPost, + }, + }, + }, + ], + }) + + const block2: Extract = { + blockType: 'content', + id: '456', + columns: [ + { + id: '789', + richText: [ + { + type: 'paragraph', + text: 'Column 1', + }, + ], + }, + ], + } + + const initialData: Partial = { + title: 'Test Page', + layout: [block1(), block2], + } + + // Add a new block before the populated one + // Then check to see that the relationship is still populated + const merge2 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...initialData, + layout: [block2, block1(true)], + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + // Check that the relationship on the first has been removed + // And that the relationship on the second has been populated + expect(merge2.layout[0].links).toBeUndefined() + expect(merge2.layout[1].links[0].link.reference.value).toMatchObject(testPost) + expect(merge2._numberOfRequests).toEqual(1) + }) + + it('— blocks - adds, reorders, and removes blocks', async () => { + const block1ID = '123' + const block2ID = '456' + + const initialData: Partial = { + title: 'Test Page', + layout: [ + { + blockType: 'cta', + id: block1ID, + richText: [ + { + type: 'paragraph', + text: 'Block 1 (Position 1)', + }, + ], + }, + { + blockType: 'cta', + id: block2ID, + richText: [ + { + type: 'paragraph', + text: 'Block 2 (Position 2)', + }, + ], + }, + ], + } + + // Reorder the blocks + const merge1 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...initialData, layout: [ { - id: block2.id, - blockType: 'content', + blockType: 'cta', + id: block2ID, richText: [ { type: 'paragraph', @@ -225,8 +431,8 @@ describe('Collections - Live Preview', () => { ], }, { - id: block1.id, - blockType: 'content', + blockType: 'cta', + id: block1ID, richText: [ { type: 'paragraph', @@ -236,27 +442,29 @@ describe('Collections - Live Preview', () => { }, ], }, - initialData: merge1, + initialData, serverURL, + returnNumberOfRequests: true, }) // Check that the blocks have been reordered - expect(merge2.layout).toHaveLength(2) - expect(merge2.layout[0].id).toEqual(block2.id) - expect(merge2.layout[1].id).toEqual(block1.id) - expect(merge2.layout[0].richText[0].text).toEqual('Block 2 (Position 1)') - expect(merge2.layout[1].richText[0].text).toEqual('Block 1 (Position 2)') + expect(merge1.layout).toHaveLength(2) + expect(merge1.layout[0].id).toEqual(block2ID) + expect(merge1.layout[1].id).toEqual(block1ID) + expect(merge1.layout[0].richText[0].text).toEqual('Block 2 (Position 1)') + expect(merge1.layout[1].richText[0].text).toEqual('Block 1 (Position 2)') + expect(merge1._numberOfRequests).toEqual(0) // Remove a block - const merge3 = await mergeData({ + const merge2 = await mergeData({ depth: 1, fieldSchema: schemaJSON, incomingData: { - ...merge2, + ...initialData, layout: [ { - id: block2.id, - blockType: 'content', + blockType: 'cta', + id: block2ID, richText: [ { type: 'paragraph', @@ -266,28 +474,32 @@ describe('Collections - Live Preview', () => { }, ], }, - initialData: merge2, + initialData, serverURL, + returnNumberOfRequests: true, }) // Check that the block has been removed - expect(merge3.layout).toHaveLength(1) - expect(merge3.layout[0].id).toEqual(block2.id) - expect(merge3.layout[0].richText[0].text).toEqual('Block 2 (Position 1)') + expect(merge2.layout).toHaveLength(1) + expect(merge2.layout[0].id).toEqual(block2ID) + expect(merge2.layout[0].richText[0].text).toEqual('Block 2 (Position 1)') + expect(merge2._numberOfRequests).toEqual(0) // Remove the last block to ensure that all blocks can be cleared - const merge4 = await mergeData({ + const merge3 = await mergeData({ depth: 1, fieldSchema: schemaJSON, incomingData: { - ...merge3, + ...initialData, layout: [], }, - initialData: merge3, + initialData, serverURL, + returnNumberOfRequests: true, }) // Check that the block has been removed - expect(merge4.layout).toHaveLength(0) + expect(merge3.layout).toHaveLength(0) + expect(merge3._numberOfRequests).toEqual(0) }) }) diff --git a/test/live-preview/next-app/app/_components/Blocks/index.tsx b/test/live-preview/next-app/app/_components/Blocks/index.tsx index f140f0a413..a051e37c77 100644 --- a/test/live-preview/next-app/app/_components/Blocks/index.tsx +++ b/test/live-preview/next-app/app/_components/Blocks/index.tsx @@ -19,7 +19,7 @@ const blockComponents = { } export const Blocks: React.FC<{ - blocks: (Page['layout'][0] | RelatedPostsProps)[] + blocks?: (Page['layout'][0] | RelatedPostsProps)[] disableTopPadding?: boolean }> = (props) => { const { disableTopPadding, blocks } = props diff --git a/test/live-preview/next-app/app/_components/Media/Image/index.tsx b/test/live-preview/next-app/app/_components/Media/Image/index.tsx index ef2d2690a8..b0531d4083 100644 --- a/test/live-preview/next-app/app/_components/Media/Image/index.tsx +++ b/test/live-preview/next-app/app/_components/Media/Image/index.tsx @@ -38,8 +38,8 @@ export const Image: React.FC = (props) => { alt: altFromResource, } = resource - width = fullWidth - height = fullHeight + width = fullWidth || undefined + height = fullHeight || undefined alt = altFromResource const filename = fullFilename diff --git a/test/live-preview/next-app/app/_heros/HighImpact/index.tsx b/test/live-preview/next-app/app/_heros/HighImpact/index.tsx index 95ead943ba..aed106a67d 100644 --- a/test/live-preview/next-app/app/_heros/HighImpact/index.tsx +++ b/test/live-preview/next-app/app/_heros/HighImpact/index.tsx @@ -1,32 +1,20 @@ import React, { Fragment } from 'react' -import { Page } from '../../../payload/payload-types' +import { Page } from '../../../payload-types' import { Gutter } from '../../_components/Gutter' -import { CMSLink } from '../../_components/Link' import { Media } from '../../_components/Media' import RichText from '../../_components/RichText' import classes from './index.module.scss' -export const HighImpactHero: React.FC = ({ richText, media, links }) => { +export const HighImpactHero: React.FC = ({ richText, media }) => { return (
- {Array.isArray(links) && links.length > 0 && ( -
    - {links.map(({ link }, i) => { - return ( -
  • - -
  • - ) - })} -
- )}
- {typeof media === 'object' && ( + {typeof media === 'object' && media !== null && ( = { slug: 'home', title: 'Home', - id: '', meta: { description: 'This is an example of live preview on a page.', }, @@ -94,4 +93,12 @@ export const home: Page = { ], }, ], + relationshipMonoHasMany: ['{{POST_1_ID}}'], + relationshipMonoHasOne: '{{POST_1_ID}}', + relationshipPolyHasMany: [{ relationTo: 'posts', value: '{{POST_1_ID}}' }], + arrayOfRelationships: [ + { + relationshipWithinArray: '{{POST_1_ID}}', + }, + ], } diff --git a/test/live-preview/seed/post-1.ts b/test/live-preview/seed/post-1.ts index 78f8d0b23a..920baec379 100644 --- a/test/live-preview/seed/post-1.ts +++ b/test/live-preview/seed/post-1.ts @@ -1,5 +1,5 @@ import type { Post } from '../payload-types' -export const post1: Partial = { +export const post1: Omit = { title: 'Post 1', slug: 'post-1', meta: { diff --git a/test/live-preview/seed/post-2.ts b/test/live-preview/seed/post-2.ts index 67901ca959..79d5e15950 100644 --- a/test/live-preview/seed/post-2.ts +++ b/test/live-preview/seed/post-2.ts @@ -1,6 +1,6 @@ import type { Post } from '../payload-types' -export const post2: Partial = { +export const post2: Omit = { title: 'Post 2', slug: 'post-2', meta: { diff --git a/test/live-preview/seed/post-3.ts b/test/live-preview/seed/post-3.ts index f7804de4cd..8e2db6c7d6 100644 --- a/test/live-preview/seed/post-3.ts +++ b/test/live-preview/seed/post-3.ts @@ -1,6 +1,6 @@ import type { Post } from '../payload-types' -export const post3: Partial = { +export const post3: Omit = { title: 'Post 3', slug: 'post-3', meta: {