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.
This commit is contained in:
@@ -1,33 +1,40 @@
|
||||
import type { LivePreviewMessageEvent } from './types.js'
|
||||
|
||||
import { isLivePreviewEvent } from './isLivePreviewEvent.js'
|
||||
import { mergeData } from './mergeData.js'
|
||||
|
||||
// For performance reasons, `fieldSchemaJSON` will only be sent once on the initial message
|
||||
// We need to cache this value so that it can be used across subsequent messages
|
||||
// To do this, save `fieldSchemaJSON` when it arrives as a global variable
|
||||
// Send this cached value to `mergeData`, instead of `eventData.fieldSchemaJSON` directly
|
||||
let payloadLivePreviewFieldSchema = undefined // TODO: type this from `fieldSchemaToJSON` return type
|
||||
|
||||
// Each time the data is merged, cache the result as a `previousData` variable
|
||||
// This will ensure changes compound overtop of each other
|
||||
let payloadLivePreviewPreviousData = undefined
|
||||
const _payloadLivePreview = {
|
||||
/**
|
||||
* For performance reasons, `fieldSchemaJSON` will only be sent once on the initial message
|
||||
* We need to cache this value so that it can be used across subsequent messages
|
||||
* To do this, save `fieldSchemaJSON` when it arrives as a global variable
|
||||
* Send this cached value to `mergeData`, instead of `eventData.fieldSchemaJSON` directly
|
||||
*/
|
||||
fieldSchema: undefined,
|
||||
/**
|
||||
* Each time the data is merged, cache the result as a `previousData` variable
|
||||
* This will ensure changes compound overtop of each other
|
||||
*/
|
||||
previousData: undefined,
|
||||
}
|
||||
|
||||
export const handleMessage = async <T>(args: {
|
||||
apiRoute?: string
|
||||
depth?: number
|
||||
event: MessageEvent
|
||||
event: LivePreviewMessageEvent<T>
|
||||
initialData: T
|
||||
serverURL: string
|
||||
}): Promise<T> => {
|
||||
const { apiRoute, depth, event, initialData, serverURL } = args
|
||||
|
||||
if (isLivePreviewEvent(event, serverURL)) {
|
||||
const { data, externallyUpdatedRelationship, fieldSchemaJSON } = event.data
|
||||
const { data, externallyUpdatedRelationship, fieldSchemaJSON, locale } = event.data
|
||||
|
||||
if (!payloadLivePreviewFieldSchema && fieldSchemaJSON) {
|
||||
payloadLivePreviewFieldSchema = fieldSchemaJSON
|
||||
if (!_payloadLivePreview?.fieldSchema && fieldSchemaJSON) {
|
||||
_payloadLivePreview.fieldSchema = fieldSchemaJSON
|
||||
}
|
||||
|
||||
if (!payloadLivePreviewFieldSchema) {
|
||||
if (!_payloadLivePreview?.fieldSchema) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'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: {
|
||||
apiRoute,
|
||||
depth,
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: payloadLivePreviewFieldSchema,
|
||||
fieldSchema: _payloadLivePreview.fieldSchema,
|
||||
incomingData: data,
|
||||
initialData: payloadLivePreviewPreviousData || initialData,
|
||||
locale: event.data.locale,
|
||||
initialData: _payloadLivePreview?.previousData || initialData,
|
||||
locale,
|
||||
serverURL,
|
||||
})
|
||||
|
||||
payloadLivePreviewPreviousData = mergedData
|
||||
_payloadLivePreview.previousData = mergedData
|
||||
|
||||
return mergedData
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ export { mergeData } from './mergeData.js'
|
||||
export { ready } from './ready.js'
|
||||
export { subscribe } from './subscribe.js'
|
||||
export { traverseRichText } from './traverseRichText.js'
|
||||
export type { LivePreviewMessageEvent } from './types.js'
|
||||
export { unsubscribe } from './unsubscribe.js'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { PaginatedDocs } from 'payload'
|
||||
import type { fieldSchemaToJSON } from 'payload/shared'
|
||||
import type { DocumentEvent, FieldSchemaJSON, PaginatedDocs } from 'payload'
|
||||
|
||||
import type { PopulationsByCollection, UpdatedDocument } from './types.js'
|
||||
import type { PopulationsByCollection } from './types.js'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
@@ -32,8 +31,8 @@ export const mergeData = async <T>(args: {
|
||||
serverURL: string
|
||||
}) => Promise<Response>
|
||||
depth?: number
|
||||
externallyUpdatedRelationship?: UpdatedDocument
|
||||
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
fieldSchema: FieldSchemaJSON
|
||||
incomingData: Partial<T>
|
||||
initialData: T
|
||||
locale?: string
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { DocumentEvent } from 'payload'
|
||||
import type { fieldSchemaToJSON } from 'payload/shared'
|
||||
|
||||
import type { PopulationsByCollection, UpdatedDocument } from './types.js'
|
||||
import type { PopulationsByCollection } from './types.js'
|
||||
|
||||
import { traverseRichText } from './traverseRichText.js'
|
||||
|
||||
export const traverseFields = <T>(args: {
|
||||
externallyUpdatedRelationship?: UpdatedDocument
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||
incomingData: T
|
||||
localeChanged: boolean
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { PopulationsByCollection, UpdatedDocument } from './types.js'
|
||||
import type { DocumentEvent } from 'payload'
|
||||
|
||||
import type { PopulationsByCollection } from './types.js'
|
||||
|
||||
export const traverseRichText = ({
|
||||
externallyUpdatedRelationship,
|
||||
@@ -6,7 +8,7 @@ export const traverseRichText = ({
|
||||
populationsByCollection,
|
||||
result,
|
||||
}: {
|
||||
externallyUpdatedRelationship?: UpdatedDocument
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
incomingData: any
|
||||
populationsByCollection: PopulationsByCollection
|
||||
result: any
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
|
||||
|
||||
export type LivePreviewArgs = {}
|
||||
|
||||
export type LivePreview = void
|
||||
@@ -10,9 +12,10 @@ export type PopulationsByCollection = {
|
||||
}>
|
||||
}
|
||||
|
||||
// TODO: import this from `payload/admin/components/utilities/DocumentEvents/types.ts`
|
||||
export type UpdatedDocument = {
|
||||
entitySlug: string
|
||||
id?: number | string
|
||||
updatedAt: string
|
||||
}
|
||||
export type LivePreviewMessageEvent<T> = MessageEvent<{
|
||||
data: T
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
fieldSchemaJSON: FieldSchemaJSON
|
||||
locale?: string
|
||||
type: 'payload-live-preview'
|
||||
}>
|
||||
|
||||
@@ -507,3 +507,9 @@ export type ClientFieldSchemaMap = Map<
|
||||
| ClientField
|
||||
| ClientTab
|
||||
>
|
||||
|
||||
export type DocumentEvent = {
|
||||
entitySlug: string
|
||||
id?: number | string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
@@ -1338,6 +1338,7 @@ export {
|
||||
type CustomVersionParser,
|
||||
} from './utilities/dependencies/dependencyChecker.js'
|
||||
export { getDependencies } from './utilities/dependencies/getDependencies.js'
|
||||
export type { FieldSchemaJSON } from './utilities/fieldSchemaToJSON.js'
|
||||
export {
|
||||
findUp,
|
||||
findUpSync,
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
'use client'
|
||||
import React, { createContext, useContext, useState } from 'react'
|
||||
import type { DocumentEvent } from 'payload'
|
||||
|
||||
export type UpdatedDocument = {
|
||||
entitySlug: string
|
||||
id?: number | string
|
||||
updatedAt: string
|
||||
}
|
||||
import React, { createContext, useContext, useState } from 'react'
|
||||
|
||||
const Context = createContext({
|
||||
mostRecentUpdate: null,
|
||||
reportUpdate: (doc: UpdatedDocument) => null,
|
||||
reportUpdate: (doc: DocumentEvent) => null,
|
||||
})
|
||||
|
||||
export const DocumentEventsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [mostRecentUpdate, reportUpdate] = useState<UpdatedDocument>(null)
|
||||
const [mostRecentUpdate, reportUpdate] = useState<DocumentEvent>(null)
|
||||
|
||||
return <Context.Provider value={{ mostRecentUpdate, reportUpdate }}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { handleMessage, mergeData, traverseRichText } from '@payloadcms/live-preview'
|
||||
import {
|
||||
handleMessage,
|
||||
type LivePreviewMessageEvent,
|
||||
mergeData,
|
||||
traverseRichText,
|
||||
} from '@payloadcms/live-preview'
|
||||
import path from 'path'
|
||||
import { getFileByPath } from 'payload'
|
||||
import { fieldSchemaToJSON } from 'payload/shared'
|
||||
@@ -97,7 +102,7 @@ describe('Collections - Live Preview', () => {
|
||||
type: 'payload-live-preview',
|
||||
},
|
||||
origin: serverURL,
|
||||
} as MessageEvent,
|
||||
} as MessageEvent as LivePreviewMessageEvent<Page>,
|
||||
initialData: {
|
||||
title: 'Test Page',
|
||||
} as Page,
|
||||
@@ -118,7 +123,7 @@ describe('Collections - Live Preview', () => {
|
||||
type: 'payload-live-preview',
|
||||
},
|
||||
origin: serverURL,
|
||||
} as MessageEvent,
|
||||
} as MessageEvent as LivePreviewMessageEvent<Page>,
|
||||
initialData: {
|
||||
title: 'Test Page',
|
||||
} as Page,
|
||||
|
||||
@@ -135,9 +135,6 @@ export interface Page {
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
/**
|
||||
* Choose how the link should be rendered.
|
||||
*/
|
||||
appearance?: ('primary' | 'secondary') | null;
|
||||
};
|
||||
id?: string | null;
|
||||
@@ -172,9 +169,6 @@ export interface Page {
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
/**
|
||||
* Choose how the link should be rendered.
|
||||
*/
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null;
|
||||
};
|
||||
id?: string | null;
|
||||
@@ -208,18 +202,12 @@ export interface Page {
|
||||
value: string | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* This field is auto-populated after-read
|
||||
*/
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* This field is auto-populated after-read
|
||||
*/
|
||||
populatedDocsTotal?: number | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
@@ -379,9 +367,6 @@ export interface Post {
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
/**
|
||||
* Choose how the link should be rendered.
|
||||
*/
|
||||
appearance?: ('primary' | 'secondary') | null;
|
||||
};
|
||||
id?: string | null;
|
||||
@@ -416,9 +401,6 @@ export interface Post {
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
/**
|
||||
* Choose how the link should be rendered.
|
||||
*/
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null;
|
||||
};
|
||||
id?: string | null;
|
||||
@@ -452,18 +434,12 @@ export interface Post {
|
||||
value: string | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* This field is auto-populated after-read
|
||||
*/
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* This field is auto-populated after-read
|
||||
*/
|
||||
populatedDocsTotal?: number | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
@@ -534,9 +510,6 @@ export interface Ssr {
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
/**
|
||||
* Choose how the link should be rendered.
|
||||
*/
|
||||
appearance?: ('primary' | 'secondary') | null;
|
||||
};
|
||||
id?: string | null;
|
||||
@@ -571,9 +544,6 @@ export interface Ssr {
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
/**
|
||||
* Choose how the link should be rendered.
|
||||
*/
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null;
|
||||
};
|
||||
id?: string | null;
|
||||
@@ -607,18 +577,12 @@ export interface Ssr {
|
||||
value: string | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* This field is auto-populated after-read
|
||||
*/
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* This field is auto-populated after-read
|
||||
*/
|
||||
populatedDocsTotal?: number | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
@@ -677,9 +641,6 @@ export interface SsrAutosave {
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
/**
|
||||
* Choose how the link should be rendered.
|
||||
*/
|
||||
appearance?: ('primary' | 'secondary') | null;
|
||||
};
|
||||
id?: string | null;
|
||||
@@ -714,9 +675,6 @@ export interface SsrAutosave {
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
/**
|
||||
* Choose how the link should be rendered.
|
||||
*/
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null;
|
||||
};
|
||||
id?: string | null;
|
||||
@@ -750,18 +708,12 @@ export interface SsrAutosave {
|
||||
value: string | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* This field is auto-populated after-read
|
||||
*/
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* This field is auto-populated after-read
|
||||
*/
|
||||
populatedDocsTotal?: number | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
@@ -1393,9 +1345,6 @@ export interface Header {
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
/**
|
||||
* Choose how the link should be rendered.
|
||||
*/
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null;
|
||||
};
|
||||
id?: string | null;
|
||||
@@ -1426,9 +1375,6 @@ export interface Footer {
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
/**
|
||||
* Choose how the link should be rendered.
|
||||
*/
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null;
|
||||
};
|
||||
id?: string | null;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": ["./test/database/config.ts"],
|
||||
"@payload-config": ["./test/live-preview/config.ts"],
|
||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user