Skip to content

Commit 466f109

Browse files
authored
chore(live-preview): strongly types message events (#10148)
Live Preview message events were typed with the generic `MessageEvent` interface without passing any of the Live Preview specific properties, leading to unknown types upon use. To fix this, there is a new `LivePreviewMessageEvent` which properly extends the underlying `MessageEvent` interface, providing much needed type safety to these functions. In the same vein, the `UpdatedDocument` type was not being properly shared across packages, leading to multiple independent definitions of this type. This type is now exported from `payload` itself and renamed to `DocumentEvent` for improved semantics. Same with the `FieldSchemaJSON` type. This PR also adjusts where globally scoped variables are set, putting them within the shared `_payloadLivePreview` namespace instead of setting them individually at the top-level.
1 parent 0588394 commit 466f109

File tree

12 files changed

+66
-99
lines changed

12 files changed

+66
-99
lines changed

packages/live-preview/src/handleMessage.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,40 @@
1+
import type { LivePreviewMessageEvent } from './types.js'
2+
13
import { isLivePreviewEvent } from './isLivePreviewEvent.js'
24
import { mergeData } from './mergeData.js'
35

4-
// For performance reasons, `fieldSchemaJSON` will only be sent once on the initial message
5-
// We need to cache this value so that it can be used across subsequent messages
6-
// To do this, save `fieldSchemaJSON` when it arrives as a global variable
7-
// Send this cached value to `mergeData`, instead of `eventData.fieldSchemaJSON` directly
8-
let payloadLivePreviewFieldSchema = undefined // TODO: type this from `fieldSchemaToJSON` return type
9-
10-
// Each time the data is merged, cache the result as a `previousData` variable
11-
// This will ensure changes compound overtop of each other
12-
let payloadLivePreviewPreviousData = undefined
6+
const _payloadLivePreview = {
7+
/**
8+
* For performance reasons, `fieldSchemaJSON` will only be sent once on the initial message
9+
* We need to cache this value so that it can be used across subsequent messages
10+
* To do this, save `fieldSchemaJSON` when it arrives as a global variable
11+
* Send this cached value to `mergeData`, instead of `eventData.fieldSchemaJSON` directly
12+
*/
13+
fieldSchema: undefined,
14+
/**
15+
* Each time the data is merged, cache the result as a `previousData` variable
16+
* This will ensure changes compound overtop of each other
17+
*/
18+
previousData: undefined,
19+
}
1320

1421
export const handleMessage = async <T>(args: {
1522
apiRoute?: string
1623
depth?: number
17-
event: MessageEvent
24+
event: LivePreviewMessageEvent<T>
1825
initialData: T
1926
serverURL: string
2027
}): Promise<T> => {
2128
const { apiRoute, depth, event, initialData, serverURL } = args
2229

2330
if (isLivePreviewEvent(event, serverURL)) {
24-
const { data, externallyUpdatedRelationship, fieldSchemaJSON } = event.data
31+
const { data, externallyUpdatedRelationship, fieldSchemaJSON, locale } = event.data
2532

26-
if (!payloadLivePreviewFieldSchema && fieldSchemaJSON) {
27-
payloadLivePreviewFieldSchema = fieldSchemaJSON
33+
if (!_payloadLivePreview?.fieldSchema && fieldSchemaJSON) {
34+
_payloadLivePreview.fieldSchema = fieldSchemaJSON
2835
}
2936

30-
if (!payloadLivePreviewFieldSchema) {
37+
if (!_payloadLivePreview?.fieldSchema) {
3138
// eslint-disable-next-line no-console
3239
console.warn(
3340
'Payload Live Preview: No `fieldSchemaJSON` was received from the parent window. Unable to merge data.',
@@ -40,14 +47,14 @@ export const handleMessage = async <T>(args: {
4047
apiRoute,
4148
depth,
4249
externallyUpdatedRelationship,
43-
fieldSchema: payloadLivePreviewFieldSchema,
50+
fieldSchema: _payloadLivePreview.fieldSchema,
4451
incomingData: data,
45-
initialData: payloadLivePreviewPreviousData || initialData,
46-
locale: event.data.locale,
52+
initialData: _payloadLivePreview?.previousData || initialData,
53+
locale,
4754
serverURL,
4855
})
4956

50-
payloadLivePreviewPreviousData = mergedData
57+
_payloadLivePreview.previousData = mergedData
5158

5259
return mergedData
5360
}

packages/live-preview/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export { mergeData } from './mergeData.js'
55
export { ready } from './ready.js'
66
export { subscribe } from './subscribe.js'
77
export { traverseRichText } from './traverseRichText.js'
8+
export type { LivePreviewMessageEvent } from './types.js'
89
export { unsubscribe } from './unsubscribe.js'

packages/live-preview/src/mergeData.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import type { PaginatedDocs } from 'payload'
2-
import type { fieldSchemaToJSON } from 'payload/shared'
1+
import type { DocumentEvent, FieldSchemaJSON, PaginatedDocs } from 'payload'
32

4-
import type { PopulationsByCollection, UpdatedDocument } from './types.js'
3+
import type { PopulationsByCollection } from './types.js'
54

65
import { traverseFields } from './traverseFields.js'
76

@@ -32,8 +31,8 @@ export const mergeData = async <T>(args: {
3231
serverURL: string
3332
}) => Promise<Response>
3433
depth?: number
35-
externallyUpdatedRelationship?: UpdatedDocument
36-
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
34+
externallyUpdatedRelationship?: DocumentEvent
35+
fieldSchema: FieldSchemaJSON
3736
incomingData: Partial<T>
3837
initialData: T
3938
locale?: string

packages/live-preview/src/traverseFields.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import type { DocumentEvent } from 'payload'
12
import type { fieldSchemaToJSON } from 'payload/shared'
23

3-
import type { PopulationsByCollection, UpdatedDocument } from './types.js'
4+
import type { PopulationsByCollection } from './types.js'
45

56
import { traverseRichText } from './traverseRichText.js'
67

78
export const traverseFields = <T>(args: {
8-
externallyUpdatedRelationship?: UpdatedDocument
9+
externallyUpdatedRelationship?: DocumentEvent
910
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
1011
incomingData: T
1112
localeChanged: boolean

packages/live-preview/src/traverseRichText.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import type { PopulationsByCollection, UpdatedDocument } from './types.js'
1+
import type { DocumentEvent } from 'payload'
2+
3+
import type { PopulationsByCollection } from './types.js'
24

35
export const traverseRichText = ({
46
externallyUpdatedRelationship,
57
incomingData,
68
populationsByCollection,
79
result,
810
}: {
9-
externallyUpdatedRelationship?: UpdatedDocument
11+
externallyUpdatedRelationship?: DocumentEvent
1012
incomingData: any
1113
populationsByCollection: PopulationsByCollection
1214
result: any

packages/live-preview/src/types.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
2+
13
export type LivePreviewArgs = {}
24

35
export type LivePreview = void
@@ -10,9 +12,10 @@ export type PopulationsByCollection = {
1012
}>
1113
}
1214

13-
// TODO: import this from `payload/admin/components/utilities/DocumentEvents/types.ts`
14-
export type UpdatedDocument = {
15-
entitySlug: string
16-
id?: number | string
17-
updatedAt: string
18-
}
15+
export type LivePreviewMessageEvent<T> = MessageEvent<{
16+
data: T
17+
externallyUpdatedRelationship?: DocumentEvent
18+
fieldSchemaJSON: FieldSchemaJSON
19+
locale?: string
20+
type: 'payload-live-preview'
21+
}>

packages/payload/src/admin/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,3 +507,9 @@ export type ClientFieldSchemaMap = Map<
507507
| ClientField
508508
| ClientTab
509509
>
510+
511+
export type DocumentEvent = {
512+
entitySlug: string
513+
id?: number | string
514+
updatedAt: string
515+
}

packages/payload/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,7 @@ export {
13381338
type CustomVersionParser,
13391339
} from './utilities/dependencies/dependencyChecker.js'
13401340
export { getDependencies } from './utilities/dependencies/getDependencies.js'
1341+
export type { FieldSchemaJSON } from './utilities/fieldSchemaToJSON.js'
13411342
export {
13421343
findUp,
13431344
findUpSync,

packages/ui/src/providers/DocumentEvents/index.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
'use client'
2-
import React, { createContext, useContext, useState } from 'react'
2+
import type { DocumentEvent } from 'payload'
33

4-
export type UpdatedDocument = {
5-
entitySlug: string
6-
id?: number | string
7-
updatedAt: string
8-
}
4+
import React, { createContext, useContext, useState } from 'react'
95

106
const Context = createContext({
117
mostRecentUpdate: null,
12-
reportUpdate: (doc: UpdatedDocument) => null,
8+
reportUpdate: (doc: DocumentEvent) => null,
139
})
1410

1511
export const DocumentEventsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
16-
const [mostRecentUpdate, reportUpdate] = useState<UpdatedDocument>(null)
12+
const [mostRecentUpdate, reportUpdate] = useState<DocumentEvent>(null)
1713

1814
return <Context.Provider value={{ mostRecentUpdate, reportUpdate }}>{children}</Context.Provider>
1915
}

test/live-preview/int.spec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { Payload } from 'payload'
22

3-
import { handleMessage, mergeData, traverseRichText } from '@payloadcms/live-preview'
3+
import {
4+
handleMessage,
5+
type LivePreviewMessageEvent,
6+
mergeData,
7+
traverseRichText,
8+
} from '@payloadcms/live-preview'
49
import path from 'path'
510
import { getFileByPath } from 'payload'
611
import { fieldSchemaToJSON } from 'payload/shared'
@@ -97,7 +102,7 @@ describe('Collections - Live Preview', () => {
97102
type: 'payload-live-preview',
98103
},
99104
origin: serverURL,
100-
} as MessageEvent,
105+
} as MessageEvent as LivePreviewMessageEvent<Page>,
101106
initialData: {
102107
title: 'Test Page',
103108
} as Page,
@@ -118,7 +123,7 @@ describe('Collections - Live Preview', () => {
118123
type: 'payload-live-preview',
119124
},
120125
origin: serverURL,
121-
} as MessageEvent,
126+
} as MessageEvent as LivePreviewMessageEvent<Page>,
122127
initialData: {
123128
title: 'Test Page',
124129
} as Page,

0 commit comments

Comments
 (0)