fix(live-preview): re-populates externally updated relationships (#4287)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user