diff --git a/packages/live-preview/src/handleMessage.ts b/packages/live-preview/src/handleMessage.ts index f7f0c54d3..b8fbb7435 100644 --- a/packages/live-preview/src/handleMessage.ts +++ b/packages/live-preview/src/handleMessage.ts @@ -6,6 +6,10 @@ import { mergeData } from '.' // Send this cached value to `mergeData`, instead of `eventData.fieldSchemaJSON` directly let payloadLivePreviewFieldSchema = undefined // TODO: type this from `fieldSchemaToJSON` return type +// Each time the data is merged, cache the result as a `previousData` variable +// This will ensure changes compound overtop of each other +let payloadLivePreviewPreviousData = undefined + export const handleMessage = async (args: { apiRoute?: string depth?: number @@ -37,10 +41,12 @@ export const handleMessage = async (args: { depth, fieldSchema: payloadLivePreviewFieldSchema, incomingData: eventData.data, - initialData, + initialData: payloadLivePreviewPreviousData || initialData, serverURL, }) + payloadLivePreviewPreviousData = mergedData + return mergedData } } diff --git a/packages/live-preview/src/traverseRichText.ts b/packages/live-preview/src/traverseRichText.ts index 625bd1dbf..d1ec36891 100644 --- a/packages/live-preview/src/traverseRichText.ts +++ b/packages/live-preview/src/traverseRichText.ts @@ -11,50 +11,68 @@ export const traverseRichText = ({ apiRoute: string depth: number incomingData: any - populationPromises: Promise[] + populationPromises: Promise[] result: any serverURL: string }): any => { if (Array.isArray(incomingData)) { - result = incomingData.map((incomingRow) => - traverseRichText({ + if (!result) { + result = [] + } + + result = incomingData.map((item, index) => { + if (!result[index]) { + result[index] = item + } + + return traverseRichText({ apiRoute, depth, - incomingData: incomingRow, + incomingData: item, populationPromises, - result, + result: result[index], serverURL, - }), - ) - } else if (typeof incomingData === 'object' && incomingData !== null) { - result = incomingData - - if ('relationTo' in incomingData && 'value' in incomingData && incomingData.value) { - populationPromises.push( - promise({ - id: typeof incomingData.value === 'object' ? incomingData.value.id : incomingData.value, - accessor: 'value', - apiRoute, - collection: String(incomingData.relationTo), - depth, - ref: result, - serverURL, - }), - ) - } else { + }) + }) + } else if (incomingData && typeof incomingData === 'object') { + if (!result) { result = {} + } - Object.keys(incomingData).forEach((key) => { + Object.keys(incomingData).forEach((key) => { + if (!result[key]) { + result[key] = incomingData[key] + } + + const isRelationship = key === 'value' && 'relationTo' in incomingData + + if (isRelationship) { + const needsPopulation = !result.value || typeof result.value !== 'object' + + if (needsPopulation) { + populationPromises.push( + promise({ + id: incomingData[key], + accessor: 'value', + apiRoute, + collection: incomingData.relationTo, + depth, + ref: result, + serverURL, + }), + ) + } + } else { result[key] = traverseRichText({ apiRoute, depth, incomingData: incomingData[key], populationPromises, - result, + result: result[key], serverURL, }) - }) - } + } + }) } else { result = incomingData } diff --git a/test/live-preview/int.spec.ts b/test/live-preview/int.spec.ts index 3a6f8a88f..bdfc73d12 100644 --- a/test/live-preview/int.spec.ts +++ b/test/live-preview/int.spec.ts @@ -187,8 +187,7 @@ describe('Collections - Live Preview', () => { expect(mergedDataWithoutUpload.hero.media).toBeFalsy() }) - - it('— relationships - populates all types', async () => { + it('— relationships - populates monomorphic has one relationships', async () => { const initialData: Partial = { title: 'Test Page', } @@ -199,8 +198,71 @@ describe('Collections - Live Preview', () => { incomingData: { ...initialData, relationshipMonoHasOne: testPost.id, + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + expect(merge1._numberOfRequests).toEqual(1) + expect(merge1.relationshipMonoHasOne).toMatchObject(testPost) + }) + + it('— relationships - populates monomorphic has many relationships', async () => { + const initialData: Partial = { + title: 'Test Page', + } + + const merge1 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...initialData, relationshipMonoHasMany: [testPost.id], + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + expect(merge1._numberOfRequests).toEqual(1) + expect(merge1.relationshipMonoHasMany).toMatchObject([testPost]) + }) + + it('— relationships - populates polymorphic has one relationships', async () => { + const initialData: Partial = { + title: 'Test Page', + } + + const merge1 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...initialData, relationshipPolyHasOne: { value: testPost.id, relationTo: postsSlug }, + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + expect(merge1._numberOfRequests).toEqual(1) + expect(merge1.relationshipPolyHasOne).toMatchObject({ + value: testPost, + relationTo: postsSlug, + }) + }) + + it('— relationships - populates polymorphic has many relationships', async () => { + const initialData: Partial = { + title: 'Test Page', + } + + const merge1 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...initialData, relationshipPolyHasMany: [{ value: testPost.id, relationTo: postsSlug }], }, initialData, @@ -208,19 +270,12 @@ describe('Collections - Live Preview', () => { returnNumberOfRequests: true, }) - expect(merge1._numberOfRequests).toEqual(4) - expect(merge1.relationshipMonoHasOne).toMatchObject(testPost) - expect(merge1.relationshipMonoHasMany).toMatchObject([testPost]) - - expect(merge1.relationshipPolyHasOne).toMatchObject({ - value: testPost, - relationTo: postsSlug, - }) - + expect(merge1._numberOfRequests).toEqual(1) expect(merge1.relationshipPolyHasMany).toMatchObject([ { value: testPost, relationTo: postsSlug }, ]) }) + it('— relationships - can clear relationships', async () => { const initialData: Partial = { title: 'Test Page', @@ -421,6 +476,114 @@ describe('Collections - Live Preview', () => { expect(merge2.relationshipInRichText[0].type).toEqual('paragraph') }) + it('— relationships - does not re-populate existing rich text relationships', async () => { + const initialData: Partial = { + title: 'Test Page', + relationshipInRichText: [ + { + type: 'paragraph', + text: 'Paragraph 1', + }, + { + type: 'reference', + reference: { + relationTo: 'posts', + value: testPost, + }, + }, + ], + } + + // Add a relationship + const merge1 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...initialData, + relationshipInRichText: [ + { + type: 'paragraph', + text: 'Paragraph 1', + }, + { + type: 'reference', + reference: { + relationTo: 'posts', + value: testPost.id, + }, + }, + ], + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + expect(merge1._numberOfRequests).toEqual(0) + expect(merge1.relationshipInRichText).toHaveLength(2) + expect(merge1.relationshipInRichText[1].reference.value).toMatchObject(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('— rich text - merges rich text', async () => { const initialData: Partial = { title: 'Test Page', @@ -485,66 +648,6 @@ describe('Collections - Live Preview', () => { expect(merge2.hero.richText[0].children[0].text).toEqual('Paragraph 1 (Updated)') }) - 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' diff --git a/test/live-preview/next-app/app/_components/RichText/serialize.tsx b/test/live-preview/next-app/app/_components/RichText/serialize.tsx index 3c8e30970..571a47a0a 100644 --- a/test/live-preview/next-app/app/_components/RichText/serialize.tsx +++ b/test/live-preview/next-app/app/_components/RichText/serialize.tsx @@ -2,6 +2,7 @@ import React, { Fragment } from 'react' import escapeHTML from 'escape-html' import Link from 'next/link' import { Text } from 'slate' +import { CMSLink } from '../Link' // eslint-disable-next-line no-use-before-define type Children = Leaf[] @@ -85,18 +86,15 @@ const serialize = (children?: Children): React.ReactNode[] => ) case 'link': return ( - {serialize(node?.children)} - + ) default: