fix(live-preview): blocks field (#3753)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
4
test/live-preview/mocks/mockFSModule.js
Normal file
4
test/live-preview/mocks/mockFSModule.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
readdirSync: () => {},
|
||||
rmSync: () => {},
|
||||
}
|
||||
@@ -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' }
|
||||
>
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user