fix(live-preview): blocks field (#3753)

This commit is contained in:
Jacob Fletcher
2023-10-19 16:40:16 -04:00
committed by GitHub
parent 01245b07f8
commit 7fcb972dfa
12 changed files with 242 additions and 95 deletions

View File

@@ -6,7 +6,7 @@ export type MergeLiveDataArgs<T> = {
apiRoute?: string
depth: number
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: T
incomingData: Partial<T>
initialData: T
serverURL: string
}

View File

@@ -53,34 +53,36 @@ export const traverseFields = <T>({
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':

View File

@@ -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'],

View File

@@ -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<Page>): Promise<Page> => {
return await mergeLivePreviewData<Page>({
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({
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({
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({
// 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)
})
})

View File

@@ -0,0 +1,4 @@
export default {
readdirSync: () => {},
rmSync: () => {},
}

View File

@@ -1,3 +1,6 @@
import type { Page } from '../../../payload-types'
export type ArchiveBlockProps = Extract<Page['layout'][0], { blockType: 'archive' }>
export type ArchiveBlockProps = Extract<
Exclude<Page['layout'], undefined>[0],
{ blockType: 'archive' }
>

View File

@@ -8,7 +8,7 @@ import { VerticalPadding } from '../../_components/VerticalPadding'
import classes from './index.module.scss'
type Props = Extract<Page['layout'][0], { blockType: 'cta' }>
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'cta' }>
export const CallToActionBlock: React.FC<
Props & {

View File

@@ -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<Page['layout'][0], { blockType: 'content' }>
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'content' }>
export const ContentBlock: React.FC<
Props & {
@@ -19,9 +19,9 @@ export const ContentBlock: React.FC<
return (
<Gutter className={classes.content}>
<div className={classes.grid}>
{columns &&
columns.length > 0 &&
columns.map((col, index) => {
{columns && columns.length > 0 ? (
<Fragment>
{columns.map((col, index) => {
const { enableLink, richText, link, size } = col
return (
@@ -31,6 +31,8 @@ export const ContentBlock: React.FC<
</div>
)
})}
</Fragment>
) : null}
</div>
</Gutter>
)

View File

@@ -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<Page['layout'][0], { blockType: 'mediaBlock' }> & {
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'mediaBlock' }> & {
staticImage?: StaticImageData
id?: string
}

View File

@@ -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 {}
}

View File

@@ -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 {}
}

View File

@@ -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: {