From 7fcb972dfa9715b4d6c1dc915f6c6f4666298b38 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Thu, 19 Oct 2023 16:40:16 -0400 Subject: [PATCH] fix(live-preview): blocks field (#3753) --- packages/live-preview/src/mergeData.ts | 2 +- packages/live-preview/src/traverseFields.ts | 28 +-- test/live-preview/config.ts | 14 ++ test/live-preview/int.spec.ts | 200 +++++++++++++++--- test/live-preview/mocks/mockFSModule.js | 4 + .../app/_blocks/ArchiveBlock/types.ts | 5 +- .../app/_blocks/CallToAction/index.tsx | 2 +- .../next-app/app/_blocks/Content/index.tsx | 30 +-- .../next-app/app/_blocks/MediaBlock/index.tsx | 4 +- test/live-preview/next-app/payload-types.ts | 22 +- test/live-preview/payload-types.ts | 22 +- test/live-preview/seed/index.ts | 4 + 12 files changed, 242 insertions(+), 95 deletions(-) create mode 100644 test/live-preview/mocks/mockFSModule.js diff --git a/packages/live-preview/src/mergeData.ts b/packages/live-preview/src/mergeData.ts index c1083f6a93..d522311559 100644 --- a/packages/live-preview/src/mergeData.ts +++ b/packages/live-preview/src/mergeData.ts @@ -6,7 +6,7 @@ export type MergeLiveDataArgs = { apiRoute?: string depth: number fieldSchema: ReturnType - incomingData: T + incomingData: Partial initialData: T serverURL: string } diff --git a/packages/live-preview/src/traverseFields.ts b/packages/live-preview/src/traverseFields.ts index ab46a7c913..dfe9a9421a 100644 --- a/packages/live-preview/src/traverseFields.ts +++ b/packages/live-preview/src/traverseFields.ts @@ -53,34 +53,36 @@ export const traverseFields = ({ case 'blocks': if (Array.isArray(incomingData[fieldName])) { - result[fieldName] = incomingData[fieldName].map((row, i) => { - const matchedBlock = fieldJSON.blocks[row.blockType] + result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => { + const incomingBlockJSON = fieldJSON.blocks[incomingBlock.blockType] - const hasExistingRow = + // 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].blockType === row.blockType + result[fieldName][i].id === incomingBlock.id - const newRow = hasExistingRow - ? { ...result[fieldName][i] } - : { - blockType: matchedBlock.slug, - } + const block = isExistingBlock ? result[fieldName][i] : incomingBlock traverseFields({ apiRoute, depth, - fieldSchema: matchedBlock.fields, - incomingData: row, + fieldSchema: incomingBlockJSON.fields, + incomingData: incomingBlock, populationPromises, - result: newRow, + result: block, serverURL, }) - return newRow + return block }) + } else { + result[fieldName] = [] } + break case 'tabs': diff --git a/test/live-preview/config.ts b/test/live-preview/config.ts index a9a1515080..f1cf03a2ba 100644 --- a/test/live-preview/config.ts +++ b/test/live-preview/config.ts @@ -1,3 +1,5 @@ +import path from 'path' + import { buildConfigWithDefaults } from '../buildConfigWithDefaults' import Categories from './collections/Categories' import { Media } from './collections/Media' @@ -17,6 +19,8 @@ export const mobileBreakpoint = { height: 667, } +const mockModulePath = path.resolve(__dirname, './mocks/mockFSModule.js') + export default buildConfigWithDefaults({ admin: { livePreview: { @@ -30,6 +34,16 @@ export default buildConfigWithDefaults({ collections: ['pages', 'posts'], globals: ['header', 'footer'], }, + webpack: (config) => ({ + ...config, + resolve: { + ...config.resolve, + alias: { + ...config?.resolve?.alias, + fs: mockModulePath, + }, + }, + }), }, cors: ['http://localhost:3001'], csrf: ['http://localhost:3001'], diff --git a/test/live-preview/int.spec.ts b/test/live-preview/int.spec.ts index e5bf1f6516..f500cb5673 100644 --- a/test/live-preview/int.spec.ts +++ b/test/live-preview/int.spec.ts @@ -3,7 +3,7 @@ import path from 'path' import type { Media, Page } from './payload-types' import { handleMessage } from '../../packages/live-preview/src/handleMessage' -import { mergeData as mergeLivePreviewData } from '../../packages/live-preview/src/mergeData' +import { mergeData } from '../../packages/live-preview/src/mergeData' import payload from '../../packages/payload/src' import getFileByPath from '../../packages/payload/src/uploads/getFileByPath' import { fieldSchemaToJSON } from '../../packages/payload/src/utilities/fieldSchemaToJSON' @@ -20,18 +20,7 @@ let serverURL let page: Page let media: Media -let mergedData: Page - -// create a util so we don't have to rewrite the args on every test -const mergeData = async (edits: Partial): Promise => { - return await mergeLivePreviewData({ - depth: 1, - fieldSchema: fieldSchemaToJSON(Pages.fields), - incomingData: edits, - initialData: page, - serverURL, - }) -} +const schemaJSON = fieldSchemaToJSON(Pages.fields) describe('Collections - Live Preview', () => { beforeAll(async () => { @@ -75,7 +64,7 @@ describe('Collections - Live Preview', () => { data: { title: 'Test Page (Change 1)', }, - fieldSchemaJSON: fieldSchemaToJSON(Pages.fields), + fieldSchemaJSON: schemaJSON, type: 'payload-live-preview', }), origin: serverURL, @@ -108,14 +97,30 @@ describe('Collections - Live Preview', () => { it('merges data', async () => { expect(page?.id).toBeDefined() - mergedData = await mergeData({}) + + const mergedData = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: page, + initialData: page, + serverURL, + }) + expect(mergedData.id).toEqual(page.id) }) it('merges strings', async () => { - mergedData = await mergeData({ - title: 'Test Page (Change 3)', + const mergedData = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...page, + title: 'Test Page (Change 3)', + }, + initialData: page, + serverURL, }) + expect(mergedData.title).toEqual('Test Page (Change 3)') }) @@ -124,23 +129,164 @@ describe('Collections - Live Preview', () => { // This test passes in MongoDB, though it.skip('adds and removes uploads', async () => { // Add upload - mergedData = await mergeData({ - hero: { - type: 'highImpact', - media: media.id, + const mergedData = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...page, + hero: { + type: 'highImpact', + media: media.id, + }, }, + initialData: page, + serverURL, }) expect(mergedData.hero.media).toMatchObject(media) - // Remove upload - mergedData = await mergeData({ - hero: { - type: 'highImpact', - media: null, + // Add upload + const mergedDataWithoutUpload = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...mergedData, + hero: { + type: 'highImpact', + media: null, + }, }, + initialData: mergedData, + serverURL, }) - expect(mergedData.hero.media).toEqual(null) + expect(mergedDataWithoutUpload.hero.media).toEqual(null) + }) + + it('add, reorder, and remove blocks', async () => { + // Add new blocks + 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: page, + serverURL, + }) + + // 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)') + + // Reorder the blocks using the same IDs from the previous merge + const merge2 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...merge1, + layout: [ + { + id: block2.id, + blockType: 'content', + richText: [ + { + type: 'paragraph', + text: 'Block 2 (Position 1)', + }, + ], + }, + { + id: block1.id, + blockType: 'content', + richText: [ + { + type: 'paragraph', + text: 'Block 1 (Position 2)', + }, + ], + }, + ], + }, + initialData: merge1, + serverURL, + }) + + // 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)') + + // Remove a block + const merge3 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...merge2, + layout: [ + { + id: block2.id, + blockType: 'content', + richText: [ + { + type: 'paragraph', + text: 'Block 2 (Position 1)', + }, + ], + }, + ], + }, + initialData: merge2, + serverURL, + }) + + // 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)') + + // Remove the last block to ensure that all blocks can be cleared + const merge4 = await mergeData({ + depth: 1, + fieldSchema: schemaJSON, + incomingData: { + ...merge3, + layout: [], + }, + initialData: merge3, + serverURL, + }) + + // Check that the block has been removed + expect(merge4.layout).toHaveLength(0) }) }) diff --git a/test/live-preview/mocks/mockFSModule.js b/test/live-preview/mocks/mockFSModule.js new file mode 100644 index 0000000000..275cdd3dd0 --- /dev/null +++ b/test/live-preview/mocks/mockFSModule.js @@ -0,0 +1,4 @@ +export default { + readdirSync: () => {}, + rmSync: () => {}, +} diff --git a/test/live-preview/next-app/app/_blocks/ArchiveBlock/types.ts b/test/live-preview/next-app/app/_blocks/ArchiveBlock/types.ts index f775d2fd5b..01abca460d 100644 --- a/test/live-preview/next-app/app/_blocks/ArchiveBlock/types.ts +++ b/test/live-preview/next-app/app/_blocks/ArchiveBlock/types.ts @@ -1,3 +1,6 @@ import type { Page } from '../../../payload-types' -export type ArchiveBlockProps = Extract +export type ArchiveBlockProps = Extract< + Exclude[0], + { blockType: 'archive' } +> diff --git a/test/live-preview/next-app/app/_blocks/CallToAction/index.tsx b/test/live-preview/next-app/app/_blocks/CallToAction/index.tsx index 5854bac792..95687c15d5 100644 --- a/test/live-preview/next-app/app/_blocks/CallToAction/index.tsx +++ b/test/live-preview/next-app/app/_blocks/CallToAction/index.tsx @@ -8,7 +8,7 @@ import { VerticalPadding } from '../../_components/VerticalPadding' import classes from './index.module.scss' -type Props = Extract +type Props = Extract[0], { blockType: 'cta' }> export const CallToActionBlock: React.FC< Props & { diff --git a/test/live-preview/next-app/app/_blocks/Content/index.tsx b/test/live-preview/next-app/app/_blocks/Content/index.tsx index bcccedeb30..9ab5a7a19d 100644 --- a/test/live-preview/next-app/app/_blocks/Content/index.tsx +++ b/test/live-preview/next-app/app/_blocks/Content/index.tsx @@ -1,13 +1,13 @@ -import React from 'react' +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 RichText from '../../_components/RichText' import classes from './index.module.scss' -type Props = Extract +type Props = Extract[0], { blockType: 'content' }> export const ContentBlock: React.FC< Props & { @@ -19,18 +19,20 @@ export const ContentBlock: React.FC< return (
- {columns && - columns.length > 0 && - columns.map((col, index) => { - const { enableLink, richText, link, size } = col + {columns && columns.length > 0 ? ( + + {columns.map((col, index) => { + const { enableLink, richText, link, size } = col - return ( -
- - {enableLink && } -
- ) - })} + return ( +
+ + {enableLink && } +
+ ) + })} +
+ ) : null}
) diff --git a/test/live-preview/next-app/app/_blocks/MediaBlock/index.tsx b/test/live-preview/next-app/app/_blocks/MediaBlock/index.tsx index b8fff784bc..6d4289ecba 100644 --- a/test/live-preview/next-app/app/_blocks/MediaBlock/index.tsx +++ b/test/live-preview/next-app/app/_blocks/MediaBlock/index.tsx @@ -1,14 +1,14 @@ import React from 'react' import { StaticImageData } from 'next/image' -import { Page } from '../../../payload/payload-types' +import { Page } from '../../../payload-types' import { Gutter } from '../../_components/Gutter' import { Media } from '../../_components/Media' import RichText from '../../_components/RichText' import classes from './index.module.scss' -type Props = Extract & { +type Props = Extract[0], { blockType: 'mediaBlock' }> & { staticImage?: StaticImageData id?: string } diff --git a/test/live-preview/next-app/payload-types.ts b/test/live-preview/next-app/payload-types.ts index 0ea84d9f9e..d713e7274e 100644 --- a/test/live-preview/next-app/payload-types.ts +++ b/test/live-preview/next-app/payload-types.ts @@ -32,7 +32,7 @@ export interface User { hash?: string loginAttempts?: number lockUntil?: string - password?: string + password: string } export interface Page { id: string @@ -45,7 +45,7 @@ export interface Page { }[] media: string | Media } - layout: ( + layout?: ( | { invertBackground?: boolean richText?: { @@ -174,7 +174,7 @@ export interface Post { }[] media: string | Media } - layout: ( + layout?: ( | { invertBackground?: boolean richText?: { @@ -338,19 +338,5 @@ export interface Footer { } declare module 'payload' { - export interface GeneratedTypes { - collections: { - users: User - pages: Page - posts: Post - categories: Category - media: Media - 'payload-preferences': PayloadPreference - 'payload-migrations': PayloadMigration - } - globals: { - header: Header - footer: Footer - } - } + export interface GeneratedTypes extends Config {} } diff --git a/test/live-preview/payload-types.ts b/test/live-preview/payload-types.ts index 0ea84d9f9e..d713e7274e 100644 --- a/test/live-preview/payload-types.ts +++ b/test/live-preview/payload-types.ts @@ -32,7 +32,7 @@ export interface User { hash?: string loginAttempts?: number lockUntil?: string - password?: string + password: string } export interface Page { id: string @@ -45,7 +45,7 @@ export interface Page { }[] media: string | Media } - layout: ( + layout?: ( | { invertBackground?: boolean richText?: { @@ -174,7 +174,7 @@ export interface Post { }[] media: string | Media } - layout: ( + layout?: ( | { invertBackground?: boolean richText?: { @@ -338,19 +338,5 @@ export interface Footer { } declare module 'payload' { - export interface GeneratedTypes { - collections: { - users: User - pages: Page - posts: Post - categories: Category - media: Media - 'payload-preferences': PayloadPreference - 'payload-migrations': PayloadMigration - } - globals: { - header: Header - footer: Footer - } - } + export interface GeneratedTypes extends Config {} } diff --git a/test/live-preview/seed/index.ts b/test/live-preview/seed/index.ts index 487cb1784a..552f764065 100644 --- a/test/live-preview/seed/index.ts +++ b/test/live-preview/seed/index.ts @@ -3,6 +3,7 @@ import path from 'path' import type { Config } from '../../../packages/payload/src/config/types' import { devUser } from '../../credentials' +import removeFiles from '../../helpers/removeFiles' import { postsSlug } from '../collections/Posts' import { pagesSlug } from '../config' import { footer } from './footer' @@ -14,6 +15,9 @@ import { post3 } from './post-3' import { postsPage } from './posts-page' export const seed: Config['onInit'] = async (payload) => { + const uploadsDir = path.resolve(__dirname, './media') + removeFiles(path.normalize(uploadsDir)) + await payload.create({ collection: 'users', data: {