fix(live-preview): re-populates externally updated relationships (#4287)

This commit is contained in:
Jacob Fletcher
2023-11-30 09:47:56 -05:00
committed by GitHub
parent 9da9b1fc50
commit 57fc211674
7 changed files with 148 additions and 25 deletions

View File

@@ -39,6 +39,7 @@ export const handleMessage = async <T>(args: {
const mergedData = await mergeData<T>({
apiRoute,
depth,
externallyUpdatedRelationship: eventData.externallyUpdatedRelationship,
fieldSchema: payloadLivePreviewFieldSchema,
incomingData: eventData.data,
initialData: payloadLivePreviewPreviousData || initialData,

View File

@@ -1,13 +1,14 @@
import type { PaginatedDocs } from 'payload/database'
import type { fieldSchemaToJSON } from 'payload/utilities'
import type { PopulationsByCollection } from './types'
import type { PopulationsByCollection, RecentUpdate } from './types'
import { traverseFields } from './traverseFields'
export const mergeData = async <T>(args: {
apiRoute?: string
depth?: number
externallyUpdatedRelationship?: RecentUpdate
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: Partial<T>
initialData: T
@@ -21,6 +22,7 @@ export const mergeData = async <T>(args: {
const {
apiRoute,
depth,
externallyUpdatedRelationship,
fieldSchema,
incomingData,
initialData,
@@ -33,6 +35,7 @@ export const mergeData = async <T>(args: {
const populationsByCollection: PopulationsByCollection = {}
traverseFields({
externallyUpdatedRelationship,
fieldSchema,
incomingData,
populationsByCollection,

View File

@@ -1,16 +1,23 @@
import type { fieldSchemaToJSON } from 'payload/utilities'
import type { PopulationsByCollection } from './types'
import type { PopulationsByCollection, RecentUpdate } from './types'
import { traverseRichText } from './traverseRichText'
export const traverseFields = <T>(args: {
externallyUpdatedRelationship?: RecentUpdate
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: T
populationsByCollection: PopulationsByCollection
result: T
}): void => {
const { fieldSchema: fieldSchemas, incomingData, populationsByCollection, result } = args
const {
externallyUpdatedRelationship,
fieldSchema: fieldSchemas,
incomingData,
populationsByCollection,
result,
} = args
fieldSchemas.forEach((fieldSchema) => {
if ('name' in fieldSchema && typeof fieldSchema.name === 'string') {
@@ -19,6 +26,7 @@ export const traverseFields = <T>(args: {
switch (fieldSchema.type) {
case 'richText':
result[fieldName] = traverseRichText({
externallyUpdatedRelationship,
incomingData: incomingData[fieldName],
populationsByCollection,
result: result[fieldName],
@@ -38,6 +46,7 @@ export const traverseFields = <T>(args: {
}
traverseFields({
externallyUpdatedRelationship,
fieldSchema: fieldSchema.fields,
incomingData: incomingRow,
populationsByCollection,
@@ -70,6 +79,7 @@ export const traverseFields = <T>(args: {
}
traverseFields({
externallyUpdatedRelationship,
fieldSchema: incomingBlockJSON.fields,
incomingData: incomingBlock,
populationsByCollection,
@@ -91,6 +101,7 @@ export const traverseFields = <T>(args: {
}
traverseFields({
externallyUpdatedRelationship,
fieldSchema: fieldSchema.fields,
incomingData: incomingData[fieldName] || {},
populationsByCollection,
@@ -123,7 +134,12 @@ export const traverseFields = <T>(args: {
const newID = incomingRelation.value
const newRelation = incomingRelation.relationTo
if (oldID !== newID || oldRelation !== newRelation) {
const hasChanged = newID !== oldID || newRelation !== oldRelation
const hasUpdated =
newRelation === externallyUpdatedRelationship?.entitySlug &&
newID === externallyUpdatedRelationship?.id
if (hasChanged || hasUpdated) {
if (!populationsByCollection[newRelation]) {
populationsByCollection[newRelation] = []
}
@@ -136,7 +152,12 @@ export const traverseFields = <T>(args: {
}
} else {
// Handle `hasMany` monomorphic
if (result[fieldName][i]?.id !== incomingRelation) {
const hasChanged = incomingRelation !== result[fieldName][i]?.id
const hasUpdated =
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
incomingRelation === externallyUpdatedRelationship?.id
if (hasChanged || hasUpdated) {
if (!populationsByCollection[fieldSchema.relationTo]) {
populationsByCollection[fieldSchema.relationTo] = []
}
@@ -185,9 +206,14 @@ export const traverseFields = <T>(args: {
const newRelation = hasNewValue ? incomingData[fieldName].relationTo : ''
const oldRelation = hasOldValue ? result[fieldName].relationTo : ''
const hasChanged = newID !== oldID || newRelation !== oldRelation
const hasUpdated =
newRelation === externallyUpdatedRelationship?.entitySlug &&
newID === externallyUpdatedRelationship?.id
// if the new value/relation is different from the old value/relation
// populate the new value, otherwise leave it alone
if (newID !== oldID || newRelation !== oldRelation) {
if (hasChanged || hasUpdated) {
// if the new value is not empty, populate it
// otherwise set the value to null
if (newID) {
@@ -218,9 +244,14 @@ export const traverseFields = <T>(args: {
result[fieldName].id) ||
result[fieldName]
const hasChanged = newID !== oldID
const hasUpdated =
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
newID === externallyUpdatedRelationship?.id
// if the new value is different from the old value
// populate the new value, otherwise leave it alone
if (newID !== oldID) {
if (hasChanged || hasUpdated) {
// if the new value is not empty, populate it
// otherwise set the value to null
if (newID) {

View File

@@ -1,10 +1,12 @@
import type { PopulationsByCollection } from './types'
import type { PopulationsByCollection, RecentUpdate } from './types'
export const traverseRichText = ({
externallyUpdatedRelationship,
incomingData,
populationsByCollection,
result,
}: {
externallyUpdatedRelationship?: RecentUpdate
incomingData: any
populationsByCollection: PopulationsByCollection
result: any
@@ -20,6 +22,7 @@ export const traverseRichText = ({
}
return traverseRichText({
externallyUpdatedRelationship,
incomingData: item,
populationsByCollection,
result: result[index],
@@ -57,8 +60,12 @@ export const traverseRichText = ({
if (isRelationship) {
const needsPopulation = !result.value || typeof result.value !== 'object'
const hasChanged =
result &&
typeof result === 'object' &&
result.value.id === externallyUpdatedRelationship?.id
if (needsPopulation) {
if (needsPopulation || hasChanged) {
if (!populationsByCollection[incomingData.relationTo]) {
populationsByCollection[incomingData.relationTo] = []
}
@@ -71,6 +78,7 @@ export const traverseRichText = ({
}
} else {
result[key] = traverseRichText({
externallyUpdatedRelationship,
incomingData: incomingData[key],
populationsByCollection,
result: result[key],

View File

@@ -9,3 +9,10 @@ export type PopulationsByCollection = {
ref: Record<string, unknown>
}>
}
// TODO: import this from `payload/utilities`
export type RecentUpdate = {
entitySlug: string
id?: string
updatedAt: string
}

View File

@@ -4,6 +4,7 @@ import type { EditViewProps } from '../../types'
import { useAllFormFields } from '../../../forms/Form/context'
import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues'
import { useDocumentEvents } from '../../../utilities/DocumentEvents'
import { useLivePreviewContext } from '../Context/context'
import { DeviceContainer } from '../Device'
import { IFrame } from '../IFrame'
@@ -23,6 +24,8 @@ export const LivePreview: React.FC<EditViewProps> = (props) => {
url,
} = useLivePreviewContext()
const { mostRecentUpdate } = useDocumentEvents()
const { breakpoint, fieldSchemaJSON } = useLivePreviewContext()
const prevWindowType =
@@ -49,6 +52,7 @@ export const LivePreview: React.FC<EditViewProps> = (props) => {
const message = JSON.stringify({
data: values,
externallyUpdatedRelationship: mostRecentUpdate,
fieldSchemaJSON: shouldSendSchema ? fieldSchemaJSON : undefined,
type: 'payload-live-preview',
})
@@ -73,6 +77,7 @@ export const LivePreview: React.FC<EditViewProps> = (props) => {
iframeRef,
setIframeHasLoaded,
fieldSchemaJSON,
mostRecentUpdate,
])
if (previewWindowType === 'iframe') {

View File

@@ -586,11 +586,89 @@ describe('Collections - Live Preview', () => {
expect(merge2._numberOfRequests).toEqual(1)
})
it('— relationships - re-populates externally updated relationships', async () => {
const initialData: Partial<Page> = {
title: 'Test Page',
}
// Populate the relationships
const merge1 = await mergeData({
depth: 1,
fieldSchema: schemaJSON,
incomingData: {
title: 'Test Page',
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(1)
expect(merge1.relationshipMonoHasOne).toMatchObject(testPost)
expect(merge1.relationshipMonoHasMany).toMatchObject([testPost])
expect(merge1.relationshipPolyHasOne).toMatchObject({
value: testPost,
relationTo: postsSlug,
})
expect(merge1.relationshipPolyHasMany).toMatchObject([
{ value: testPost, relationTo: postsSlug },
])
// Update the test post
const updatedTestPost = await payload.update({
collection: postsSlug,
id: testPost.id,
data: {
title: 'Test Post (Recently Updated)',
},
})
const externallyUpdatedRelationship = {
id: updatedTestPost.id.toString(), // TODO: don't cast to string once the types are fixed
entitySlug: postsSlug,
updatedAt: updatedTestPost.updatedAt as string,
}
// Merge again using the `externallyUpdatedRelationship` argument
const merge2 = await mergeData({
depth: 1,
fieldSchema: schemaJSON,
incomingData: {
title: 'Test Page',
relationshipMonoHasOne: testPost.id,
relationshipMonoHasMany: [testPost.id],
relationshipPolyHasOne: { value: testPost.id, relationTo: postsSlug },
relationshipPolyHasMany: [{ value: testPost.id, relationTo: postsSlug }],
},
initialData: merge1,
externallyUpdatedRelationship,
serverURL,
returnNumberOfRequests: true,
})
expect(merge2._numberOfRequests).toEqual(1)
expect(merge2.relationshipMonoHasOne).toMatchObject(updatedTestPost)
expect(merge2.relationshipMonoHasMany).toMatchObject([updatedTestPost])
expect(merge2.relationshipPolyHasOne).toMatchObject({
value: updatedTestPost,
relationTo: postsSlug,
})
expect(merge2.relationshipPolyHasMany).toMatchObject([
{ value: updatedTestPost, relationTo: postsSlug },
])
})
it('— rich text - merges text changes', async () => {
// Add a relationship
const merge1 = await traverseRichText({
depth: 1,
apiRoute: undefined,
incomingData: [
{
type: 'paragraph',
@@ -602,8 +680,7 @@ describe('Collections - Live Preview', () => {
},
],
result: [],
populationPromises: [],
serverURL,
populationsByCollection: {},
})
expect(merge1).toHaveLength(1)
@@ -611,8 +688,6 @@ describe('Collections - Live Preview', () => {
// Update the rich text
const merge2 = await traverseRichText({
depth: 1,
apiRoute: undefined,
incomingData: [
{
type: 'paragraph',
@@ -623,9 +698,8 @@ describe('Collections - Live Preview', () => {
],
},
],
populationPromises: [],
populationsByCollection: {},
result: merge1,
serverURL,
})
expect(merge2).toHaveLength(1)
@@ -635,8 +709,6 @@ describe('Collections - Live Preview', () => {
it('— rich text - can reset heading type', async () => {
// Add a heading with an H1 type
const merge1 = await traverseRichText({
depth: 1,
apiRoute: undefined,
incomingData: [
{
type: 'h1',
@@ -647,9 +719,8 @@ describe('Collections - Live Preview', () => {
],
},
],
populationPromises: [],
populationsByCollection: {},
result: [],
serverURL,
})
expect(merge1).toHaveLength(1)
@@ -657,8 +728,6 @@ describe('Collections - Live Preview', () => {
// Update the rich text to remove the heading type
const merge2 = await traverseRichText({
depth: 1,
apiRoute: undefined,
incomingData: [
{
children: [
@@ -668,9 +737,8 @@ describe('Collections - Live Preview', () => {
],
},
],
populationPromises: [],
populationsByCollection: {},
result: merge1,
serverURL,
})
expect(merge2).toHaveLength(1)