From 466f109152bc6bf9a67e7d7bafd38c7d57a881de Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Mon, 23 Dec 2024 13:19:52 -0500 Subject: [PATCH] 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. --- packages/live-preview/src/handleMessage.ts | 43 ++++++++------- packages/live-preview/src/index.ts | 1 + packages/live-preview/src/mergeData.ts | 9 ++-- packages/live-preview/src/traverseFields.ts | 5 +- packages/live-preview/src/traverseRichText.ts | 6 ++- packages/live-preview/src/types.ts | 15 +++--- packages/payload/src/admin/types.ts | 6 +++ packages/payload/src/index.ts | 1 + .../ui/src/providers/DocumentEvents/index.tsx | 12 ++--- test/live-preview/int.spec.ts | 11 ++-- test/live-preview/payload-types.ts | 54 ------------------- tsconfig.base.json | 2 +- 12 files changed, 66 insertions(+), 99 deletions(-) diff --git a/packages/live-preview/src/handleMessage.ts b/packages/live-preview/src/handleMessage.ts index fdfc92248a..dbf517f7a9 100644 --- a/packages/live-preview/src/handleMessage.ts +++ b/packages/live-preview/src/handleMessage.ts @@ -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 (args: { apiRoute?: string depth?: number - event: MessageEvent + event: LivePreviewMessageEvent initialData: T serverURL: string }): Promise => { 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 (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 } diff --git a/packages/live-preview/src/index.ts b/packages/live-preview/src/index.ts index 350e0cca0f..32046da2d0 100644 --- a/packages/live-preview/src/index.ts +++ b/packages/live-preview/src/index.ts @@ -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' diff --git a/packages/live-preview/src/mergeData.ts b/packages/live-preview/src/mergeData.ts index 59f43ad42f..ee7b7cf38d 100644 --- a/packages/live-preview/src/mergeData.ts +++ b/packages/live-preview/src/mergeData.ts @@ -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 (args: { serverURL: string }) => Promise depth?: number - externallyUpdatedRelationship?: UpdatedDocument - fieldSchema: ReturnType + externallyUpdatedRelationship?: DocumentEvent + fieldSchema: FieldSchemaJSON incomingData: Partial initialData: T locale?: string diff --git a/packages/live-preview/src/traverseFields.ts b/packages/live-preview/src/traverseFields.ts index 5c1b55452c..d9f9bbb528 100644 --- a/packages/live-preview/src/traverseFields.ts +++ b/packages/live-preview/src/traverseFields.ts @@ -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 = (args: { - externallyUpdatedRelationship?: UpdatedDocument + externallyUpdatedRelationship?: DocumentEvent fieldSchema: ReturnType incomingData: T localeChanged: boolean diff --git a/packages/live-preview/src/traverseRichText.ts b/packages/live-preview/src/traverseRichText.ts index 5ed9afefe3..5eca16b5a4 100644 --- a/packages/live-preview/src/traverseRichText.ts +++ b/packages/live-preview/src/traverseRichText.ts @@ -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 diff --git a/packages/live-preview/src/types.ts b/packages/live-preview/src/types.ts index 45df208133..94ae0ba447 100644 --- a/packages/live-preview/src/types.ts +++ b/packages/live-preview/src/types.ts @@ -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 = MessageEvent<{ + data: T + externallyUpdatedRelationship?: DocumentEvent + fieldSchemaJSON: FieldSchemaJSON + locale?: string + type: 'payload-live-preview' +}> diff --git a/packages/payload/src/admin/types.ts b/packages/payload/src/admin/types.ts index 0a158308a9..315c99b74a 100644 --- a/packages/payload/src/admin/types.ts +++ b/packages/payload/src/admin/types.ts @@ -507,3 +507,9 @@ export type ClientFieldSchemaMap = Map< | ClientField | ClientTab > + +export type DocumentEvent = { + entitySlug: string + id?: number | string + updatedAt: string +} diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 19e2defc64..6800330c9e 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -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, diff --git a/packages/ui/src/providers/DocumentEvents/index.tsx b/packages/ui/src/providers/DocumentEvents/index.tsx index 4cd7c502e2..e472726b98 100644 --- a/packages/ui/src/providers/DocumentEvents/index.tsx +++ b/packages/ui/src/providers/DocumentEvents/index.tsx @@ -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(null) + const [mostRecentUpdate, reportUpdate] = useState(null) return {children} } diff --git a/test/live-preview/int.spec.ts b/test/live-preview/int.spec.ts index c609519196..d9826fd8de 100644 --- a/test/live-preview/int.spec.ts +++ b/test/live-preview/int.spec.ts @@ -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, 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, initialData: { title: 'Test Page', } as Page, diff --git a/test/live-preview/payload-types.ts b/test/live-preview/payload-types.ts index 1792c3981a..6a9157119b 100644 --- a/test/live-preview/payload-types.ts +++ b/test/live-preview/payload-types.ts @@ -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; diff --git a/tsconfig.base.json b/tsconfig.base.json index 0ea64097ce..027d375b53 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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"],