From 381c158b0303b515164ae487b0ce7e555ae1a08d Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Wed, 29 Nov 2023 10:54:14 -0500 Subject: [PATCH] fix(live-preview): compounds merge results (#4306) --- packages/live-preview/src/handleMessage.ts | 8 +- packages/live-preview/src/traverseRichText.ts | 72 ++++--- test/live-preview/int.spec.ts | 203 +++++++++++++----- .../app/_components/RichText/serialize.tsx | 16 +- 4 files changed, 212 insertions(+), 87 deletions(-) diff --git a/packages/live-preview/src/handleMessage.ts b/packages/live-preview/src/handleMessage.ts index f7f0c54d377..b8fbb743524 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 625bd1dbf67..d1ec36891ee 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) => { + if (!result[key]) { + result[key] = incomingData[key] + } + + const isRelationship = key === 'value' && 'relationTo' in incomingData - Object.keys(incomingData).forEach((key) => { + 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 3a6f8a88f68..bdfc73d1221 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,28 +198,84 @@ describe('Collections - Live Preview', () => { incomingData: { ...initialData, relationshipMonoHasOne: testPost.id, - relationshipMonoHasMany: [testPost.id], - relationshipPolyHasOne: { value: testPost.id, relationTo: postsSlug }, - relationshipPolyHasMany: [{ value: testPost.id, relationTo: postsSlug }], }, initialData, serverURL, returnNumberOfRequests: true, }) - expect(merge1._numberOfRequests).toEqual(4) + 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, + serverURL, + returnNumberOfRequests: true, + }) + 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,9 +476,22 @@ describe('Collections - Live Preview', () => { expect(merge2.relationshipInRichText[0].type).toEqual('paragraph') }) - it('— rich text - merges rich text', async () => { + 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 @@ -432,19 +500,19 @@ describe('Collections - Live Preview', () => { fieldSchema: schemaJSON, incomingData: { ...initialData, - hero: { - type: 'lowImpact', - richText: [ - { - type: 'paragraph', - children: [ - { - text: 'Paragraph 1', - }, - ], + relationshipInRichText: [ + { + type: 'paragraph', + text: 'Paragraph 1', + }, + { + type: 'reference', + reference: { + relationTo: 'posts', + value: testPost.id, }, - ], - }, + }, + ], }, initialData, serverURL, @@ -452,37 +520,8 @@ describe('Collections - Live Preview', () => { }) expect(merge1._numberOfRequests).toEqual(0) - expect(merge1.hero.richText).toHaveLength(1) - expect(merge1.hero.richText[0].children[0].text).toEqual('Paragraph 1') - - // Update the rich text - const merge2 = await mergeData({ - depth: 1, - fieldSchema: schemaJSON, - incomingData: { - ...merge1, - hero: { - type: 'lowImpact', - richText: [ - { - type: 'paragraph', - children: [ - { - text: 'Paragraph 1 (Updated)', - }, - ], - }, - ], - }, - }, - initialData, - serverURL, - returnNumberOfRequests: true, - }) - - expect(merge2._numberOfRequests).toEqual(0) - expect(merge2.hero.richText).toHaveLength(1) - expect(merge2.hero.richText[0].children[0].text).toEqual('Paragraph 1 (Updated)') + expect(merge1.relationshipInRichText).toHaveLength(2) + expect(merge1.relationshipInRichText[1].reference.value).toMatchObject(testPost) }) it('— relationships - populates within blocks', async () => { @@ -545,6 +584,70 @@ describe('Collections - Live Preview', () => { expect(merge2._numberOfRequests).toEqual(1) }) + it('— rich text - merges rich text', async () => { + const initialData: Partial = { + title: 'Test Page', + } + + // Add a relationship + const merge1 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...initialData, + hero: { + type: 'lowImpact', + richText: [ + { + type: 'paragraph', + children: [ + { + text: 'Paragraph 1', + }, + ], + }, + ], + }, + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + expect(merge1._numberOfRequests).toEqual(0) + expect(merge1.hero.richText).toHaveLength(1) + expect(merge1.hero.richText[0].children[0].text).toEqual('Paragraph 1') + + // Update the rich text + const merge2 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...merge1, + hero: { + type: 'lowImpact', + richText: [ + { + type: 'paragraph', + children: [ + { + text: 'Paragraph 1 (Updated)', + }, + ], + }, + ], + }, + }, + initialData, + serverURL, + returnNumberOfRequests: true, + }) + + expect(merge2._numberOfRequests).toEqual(0) + expect(merge2.hero.richText).toHaveLength(1) + expect(merge2.hero.richText[0].children[0].text).toEqual('Paragraph 1 (Updated)') + }) + 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 3c8e3097019..571a47a0a1d 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: