From a13ec2ebc4858029c643f4530daa4ed49a7b024e Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Mon, 23 Oct 2023 14:30:51 -0400 Subject: [PATCH] fix: renders live preview for globals (#3801) --- .../elements/DocumentDrawer/DrawerContent.tsx | 2 + .../elements/DocumentFields/index.tsx | 13 +++- .../components/forms/RenderFields/index.tsx | 2 +- .../components/views/Account/Default.tsx | 9 ++- .../admin/components/views/Account/index.tsx | 31 ++++++--- .../admin/components/views/Global/Default.tsx | 15 +++-- .../components/views/Global/Default/index.tsx | 10 ++- .../views/Global/Routes/CustomComponent.tsx | 3 +- .../components/views/Global/Routes/index.tsx | 7 +- .../admin/components/views/Global/index.tsx | 6 +- .../components/views/LivePreview/index.tsx | 57 +++++++++++----- .../views/collections/Edit/Default.tsx | 17 +++-- .../views/collections/Edit/Default/index.tsx | 9 ++- .../views/collections/Edit/Routes/index.tsx | 7 +- .../views/collections/Edit/index.tsx | 6 +- test/live-preview/e2e.spec.ts | 67 ++++++++++--------- 16 files changed, 174 insertions(+), 87 deletions(-) diff --git a/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx b/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx index 3bcf28f9bb..54ff5d2d89 100644 --- a/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx +++ b/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx @@ -12,6 +12,7 @@ import { baseClass } from '.' import { getTranslation } from '../../../../utilities/getTranslation' import usePayloadAPI from '../../../hooks/usePayloadAPI' import buildStateFromSchema from '../../forms/Form/buildStateFromSchema' +import { fieldTypes } from '../../forms/field-types' import { useRelatedCollections } from '../../forms/field-types/Relationship/AddNew/useRelatedCollections' import X from '../../icons/X' import { useAuth } from '../../utilities/Auth' @@ -165,6 +166,7 @@ const Content: React.FC = ({ disableActions: true, disableLeaveWithoutSaving: true, disableRoutes: true, + fieldTypes, hasSavePermission, internalState, isEditing, diff --git a/packages/payload/src/admin/components/elements/DocumentFields/index.tsx b/packages/payload/src/admin/components/elements/DocumentFields/index.tsx index b83ea21f79..81517a4249 100644 --- a/packages/payload/src/admin/components/elements/DocumentFields/index.tsx +++ b/packages/payload/src/admin/components/elements/DocumentFields/index.tsx @@ -3,10 +3,10 @@ import React from 'react' import type { CollectionPermission, GlobalPermission } from '../../../../auth' import type { FieldWithPath } from '../../../../fields/config/types' import type { Description } from '../../forms/FieldDescription/types' +import type { FieldTypes } from '../../forms/field-types' import RenderFields from '../../forms/RenderFields' import { filterFields } from '../../forms/RenderFields/filterFields' -import { fieldTypes } from '../../forms/field-types' import { Gutter } from '../Gutter' import ViewDescription from '../ViewDescription' import './index.scss' @@ -17,11 +17,20 @@ export const DocumentFields: React.FC<{ AfterFields?: React.ReactNode BeforeFields?: React.ReactNode description?: Description + fieldTypes: FieldTypes fields: FieldWithPath[] hasSavePermission: boolean permissions: CollectionPermission | GlobalPermission }> = (props) => { - const { AfterFields, BeforeFields, description, fields, hasSavePermission, permissions } = props + const { + AfterFields, + BeforeFields, + description, + fieldTypes, + fields, + hasSavePermission, + permissions, + } = props const sidebarFields = filterFields({ fieldSchema: fields, diff --git a/packages/payload/src/admin/components/forms/RenderFields/index.tsx b/packages/payload/src/admin/components/forms/RenderFields/index.tsx index b1d2a455a6..851ef5ea0a 100644 --- a/packages/payload/src/admin/components/forms/RenderFields/index.tsx +++ b/packages/payload/src/admin/components/forms/RenderFields/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import type { Props } from './types' diff --git a/packages/payload/src/admin/components/views/Account/Default.tsx b/packages/payload/src/admin/components/views/Account/Default.tsx index f1c2ea98b0..58e2682bc7 100644 --- a/packages/payload/src/admin/components/views/Account/Default.tsx +++ b/packages/payload/src/admin/components/views/Account/Default.tsx @@ -1,6 +1,7 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import type { FieldTypes } from '../../forms/field-types' import type { CollectionEditViewProps } from '../types' import { DocumentControls } from '../../elements/DocumentControls' @@ -18,12 +19,17 @@ import './index.scss' const baseClass = 'account' -const DefaultAccount: React.FC = (props) => { +export type DefaultAccountViewProps = CollectionEditViewProps & { + fieldTypes: FieldTypes +} + +const DefaultAccount: React.FC = (props) => { const { action, apiURL, collection, data, + fieldTypes, hasSavePermission, initialState, isLoading, @@ -80,6 +86,7 @@ const DefaultAccount: React.FC = (props) => { useAPIKey={auth.useAPIKey} /> } + fieldTypes={fieldTypes} fields={fields} hasSavePermission={hasSavePermission} permissions={permissions} diff --git a/packages/payload/src/admin/components/views/Account/index.tsx b/packages/payload/src/admin/components/views/Account/index.tsx index d18ae0dccd..7266f5eaba 100644 --- a/packages/payload/src/admin/components/views/Account/index.tsx +++ b/packages/payload/src/admin/components/views/Account/index.tsx @@ -2,11 +2,14 @@ import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router-dom' +import type { CollectionPermission } from '../../../../auth' import type { Fields } from '../../forms/Form/types' +import type { DefaultAccountViewProps } from './Default' import usePayloadAPI from '../../../hooks/usePayloadAPI' import { useStepNav } from '../../elements/StepNav' import buildStateFromSchema from '../../forms/Form/buildStateFromSchema' +import { fieldTypes } from '../../forms/field-types' import { useAuth } from '../../utilities/Auth' import { useConfig } from '../../utilities/Config' import { useDocumentInfo } from '../../utilities/DocumentInfo' @@ -125,23 +128,29 @@ const AccountView: React.FC = () => { const isLoading = !internalState || !docPermissions || isLoadingData + const componentProps: DefaultAccountViewProps = { + id: id.toString(), + action, + apiURL, + collection, + data, + fieldTypes, + hasSavePermission, + initialState: internalState, + isLoading, + onSave, + permissions: docPermissions as CollectionPermission, + updatedAt: data?.updatedAt, + user, + } + return ( ) } diff --git a/packages/payload/src/admin/components/views/Global/Default.tsx b/packages/payload/src/admin/components/views/Global/Default.tsx index d5770cf669..6a13e45b70 100644 --- a/packages/payload/src/admin/components/views/Global/Default.tsx +++ b/packages/payload/src/admin/components/views/Global/Default.tsx @@ -1,6 +1,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import type { FieldTypes } from '../../forms/field-types' import type { GlobalEditViewProps } from '../types' import { getTranslation } from '../../../../utilities/getTranslation' @@ -15,11 +16,12 @@ import './index.scss' const baseClass = 'global-edit' -const DefaultGlobalView: React.FC< - GlobalEditViewProps & { - disableRoutes?: boolean - } -> = (props) => { +export type DefaultGlobalViewProps = GlobalEditViewProps & { + disableRoutes?: boolean + fieldTypes: FieldTypes +} + +const DefaultGlobalView: React.FC = (props) => { const { i18n } = useTranslation('general') const { @@ -27,6 +29,7 @@ const DefaultGlobalView: React.FC< apiURL, data, disableRoutes, + fieldTypes, global, initialState, isLoading, @@ -61,7 +64,7 @@ const DefaultGlobalView: React.FC< {disableRoutes ? ( ) : ( - + )} )} diff --git a/packages/payload/src/admin/components/views/Global/Default/index.tsx b/packages/payload/src/admin/components/views/Global/Default/index.tsx index f040b42325..0cefc90df7 100644 --- a/packages/payload/src/admin/components/views/Global/Default/index.tsx +++ b/packages/payload/src/admin/components/views/Global/Default/index.tsx @@ -1,6 +1,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import type { FieldTypes } from '../../../forms/field-types' import type { GlobalEditViewProps } from '../../types' import { getTranslation } from '../../../../../utilities/getTranslation' @@ -10,8 +11,12 @@ import { LeaveWithoutSaving } from '../../../modals/LeaveWithoutSaving' import Meta from '../../../utilities/Meta' import { SetStepNav } from '../../collections/Edit/SetStepNav' -export const DefaultGlobalEdit: React.FC = (props) => { - const { apiURL, data, global, permissions } = props +export const DefaultGlobalEdit: React.FC< + GlobalEditViewProps & { + fieldTypes: FieldTypes + } +> = (props) => { + const { apiURL, data, fieldTypes, global, permissions } = props const { i18n } = useTranslation() const { admin: { description } = {}, fields, label } = global @@ -37,6 +42,7 @@ export const DefaultGlobalEdit: React.FC = (props) => { /> import('../../Unauthorized')) -export const GlobalRoutes: React.FC = (props) => { +export const GlobalRoutes: React.FC< + GlobalEditViewProps & { + fieldTypes: FieldTypes + } +> = (props) => { const { global, permissions } = props const match = useRouteMatch() diff --git a/packages/payload/src/admin/components/views/Global/index.tsx b/packages/payload/src/admin/components/views/Global/index.tsx index c220d8b842..333fbf16d8 100644 --- a/packages/payload/src/admin/components/views/Global/index.tsx +++ b/packages/payload/src/admin/components/views/Global/index.tsx @@ -3,11 +3,12 @@ import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router-dom' import type { Fields } from '../../forms/Form/types' -import type { GlobalEditViewProps } from '../types' +import type { DefaultGlobalViewProps } from './Default' import type { IndexProps } from './types' import usePayloadAPI from '../../../hooks/usePayloadAPI' import buildStateFromSchema from '../../forms/Form/buildStateFromSchema' +import { fieldTypes } from '../../forms/field-types' import { useAuth } from '../../utilities/Auth' import { useConfig } from '../../utilities/Config' import { useDocumentInfo } from '../../utilities/DocumentInfo' @@ -104,13 +105,14 @@ const GlobalView: React.FC = (props) => { const isLoading = !initialState || !docPermissions || isLoadingData - const componentProps: GlobalEditViewProps = { + const componentProps: DefaultGlobalViewProps = { action: `${serverURL}${api}/globals/${slug}?locale=${locale}&fallback-locale=null`, apiURL: `${serverURL}${api}/globals/${slug}?locale=${locale}${ global.versions?.drafts ? '&draft=true' : '' }`, canAccessAdmin: permissions?.canAccessAdmin, data: dataToRender, + fieldTypes, global, initialState, isLoading, diff --git a/packages/payload/src/admin/components/views/LivePreview/index.tsx b/packages/payload/src/admin/components/views/LivePreview/index.tsx index 622acb3c79..6c50446bd6 100644 --- a/packages/payload/src/admin/components/views/LivePreview/index.tsx +++ b/packages/payload/src/admin/components/views/LivePreview/index.tsx @@ -1,8 +1,11 @@ import React, { Fragment } from 'react' import { useTranslation } from 'react-i18next' +import type { SanitizedCollectionConfig } from '../../../../collections/config/types' import type { LivePreviewConfig } from '../../../../exports/config' -import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../exports/types' +import type { Field } from '../../../../fields/config/types' +import type { SanitizedGlobalConfig } from '../../../../globals/config/types' +import type { FieldTypes } from '../../forms/field-types' import type { EditViewProps } from '../types' import { getTranslation } from '../../../../utilities/getTranslation' @@ -10,7 +13,6 @@ import { DocumentControls } from '../../elements/DocumentControls' import { Gutter } from '../../elements/Gutter' import RenderFields from '../../forms/RenderFields' import { filterFields } from '../../forms/RenderFields/filterFields' -import { fieldTypes } from '../../forms/field-types' import { LeaveWithoutSaving } from '../../modals/LeaveWithoutSaving' import { useConfig } from '../../utilities/Config' import { useDocumentInfo } from '../../utilities/DocumentInfo' @@ -25,11 +27,15 @@ import { usePopupWindow } from './usePopupWindow' const baseClass = 'live-preview' -const PreviewView: React.FC = (props) => { +const PreviewView: React.FC< + EditViewProps & { + fieldTypes: FieldTypes + } +> = (props) => { const { i18n, t } = useTranslation('general') const { previewWindowType } = useLivePreviewContext() - const { apiURL, data, permissions } = props + const { apiURL, data, fieldTypes, permissions } = props let collection: SanitizedCollectionConfig let global: SanitizedGlobalConfig @@ -38,6 +44,8 @@ const PreviewView: React.FC = (props) => { let hasSavePermission: boolean let isEditing: boolean let id: string + let fields: Field[] = [] + let label: SanitizedGlobalConfig['label'] if ('collection' in props) { collection = props?.collection @@ -46,14 +54,15 @@ const PreviewView: React.FC = (props) => { hasSavePermission = props?.hasSavePermission isEditing = props?.isEditing id = props?.id + fields = props?.collection?.fields } if ('global' in props) { global = props?.global + fields = props?.global?.fields + label = props?.global?.label } - const { fields } = collection - const sidebarFields = filterFields({ fieldSchema: fields, fieldTypes, @@ -64,6 +73,26 @@ const PreviewView: React.FC = (props) => { return ( + {collection && ( + + )} + {global && ( + + )} + {((collection && !(collection.versions?.drafts && collection.versions?.drafts?.autosave)) || + (global && !(global.versions?.drafts && global.versions?.drafts?.autosave))) && + !disableLeaveWithoutSaving && } = (props) => { .filter(Boolean) .join(' ')} > - - {!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && - !disableLeaveWithoutSaving && } = (props) => { ) } -export const LivePreviewView: React.FC = (props) => { +export const LivePreviewView: React.FC< + EditViewProps & { + fieldTypes: FieldTypes + } +> = (props) => { const config = useConfig() const documentInfo = useDocumentInfo() const locale = useLocale() diff --git a/packages/payload/src/admin/components/views/collections/Edit/Default.tsx b/packages/payload/src/admin/components/views/collections/Edit/Default.tsx index c25aa0b58e..af1781dfb6 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/Default.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/Default.tsx @@ -1,6 +1,7 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import type { FieldTypes } from '../../../forms/field-types' import type { CollectionEditViewProps } from '../../types' import { getTranslation } from '../../../../../utilities/getTranslation' @@ -15,12 +16,13 @@ import './index.scss' const baseClass = 'collection-edit' -const DefaultEditView: React.FC< - CollectionEditViewProps & { - customHeader?: React.ReactNode - disableRoutes?: boolean - } -> = (props) => { +export type DefaultEditViewProps = CollectionEditViewProps & { + customHeader?: React.ReactNode + disableRoutes?: boolean + fieldTypes: FieldTypes +} + +const DefaultEditView: React.FC = (props) => { const { i18n } = useTranslation('general') const { refreshCookieAsync, user } = useAuth() @@ -32,6 +34,7 @@ const DefaultEditView: React.FC< customHeader, data, disableRoutes, + fieldTypes, hasSavePermission, internalState, isEditing, @@ -96,7 +99,7 @@ const DefaultEditView: React.FC< {disableRoutes ? ( ) : ( - + )} )} diff --git a/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx b/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx index fe7a103848..84df02caac 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx @@ -1,6 +1,7 @@ import React, { Fragment } from 'react' import { useTranslation } from 'react-i18next' +import type { FieldTypes } from '../../../../forms/field-types' import type { CollectionEditViewProps } from '../../../types' import { getTranslation } from '../../../../../../utilities/getTranslation' @@ -15,7 +16,11 @@ import './index.scss' const baseClass = 'collection-default-edit' -export const DefaultCollectionEdit: React.FC = (props) => { +export const DefaultCollectionEdit: React.FC< + CollectionEditViewProps & { + fieldTypes: FieldTypes + } +> = (props) => { const { i18n, t } = useTranslation('general') const { @@ -25,6 +30,7 @@ export const DefaultCollectionEdit: React.FC = (props) data, disableActions, disableLeaveWithoutSaving, + fieldTypes, hasSavePermission, internalState, isEditing, @@ -79,6 +85,7 @@ export const DefaultCollectionEdit: React.FC = (props) {upload && } } + fieldTypes={fieldTypes} fields={fields} hasSavePermission={hasSavePermission} permissions={permissions} diff --git a/packages/payload/src/admin/components/views/collections/Edit/Routes/index.tsx b/packages/payload/src/admin/components/views/collections/Edit/Routes/index.tsx index da8cea08e5..77575415b1 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/Routes/index.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/Routes/index.tsx @@ -2,6 +2,7 @@ import { lazy } from 'react' import React from 'react' import { Route, Switch, useRouteMatch } from 'react-router-dom' +import type { FieldTypes } from '../../../../forms/field-types' import type { CollectionEditViewProps } from '../../../types' import { useAuth } from '../../../../utilities/Auth' @@ -13,7 +14,11 @@ import { collectionCustomRoutes } from './custom' // @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue const Unauthorized = lazy(() => import('../../../Unauthorized')) -export const CollectionRoutes: React.FC = (props) => { +export const CollectionRoutes: React.FC< + CollectionEditViewProps & { + fieldTypes: FieldTypes + } +> = (props) => { const { collection, permissions } = props const match = useRouteMatch() diff --git a/packages/payload/src/admin/components/views/collections/Edit/index.tsx b/packages/payload/src/admin/components/views/collections/Edit/index.tsx index 22a8cd7507..423fcb7a41 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/index.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/index.tsx @@ -6,11 +6,12 @@ import { useHistory, useRouteMatch } from 'react-router-dom' import type { CollectionPermission } from '../../../../../auth' import type { Fields } from '../../../forms/Form/types' import type { QueryParamTypes } from '../../../utilities/FormQueryParams' -import type { CollectionEditViewProps } from '../../types' +import type { DefaultEditViewProps } from './Default' import type { IndexProps } from './types' import usePayloadAPI from '../../../../hooks/usePayloadAPI' import buildStateFromSchema from '../../../forms/Form/buildStateFromSchema' +import { fieldTypes } from '../../../forms/field-types' import { useAuth } from '../../../utilities/Auth' import { useConfig } from '../../../utilities/Config' import { useDocumentInfo } from '../../../utilities/DocumentInfo' @@ -149,13 +150,14 @@ const EditView: React.FC = (props) => { const isLoading = !internalState || !docPermissions || isLoadingData - const componentProps: CollectionEditViewProps = { + const componentProps: DefaultEditViewProps = { id, action, apiURL, canAccessAdmin: permissions?.canAccessAdmin, collection, data, + fieldTypes, hasSavePermission, internalState, isEditing, diff --git a/test/live-preview/e2e.spec.ts b/test/live-preview/e2e.spec.ts index e6f0d37283..2d49e8e3fb 100644 --- a/test/live-preview/e2e.spec.ts +++ b/test/live-preview/e2e.spec.ts @@ -12,6 +12,13 @@ const { beforeAll, describe } = test let url: AdminUrlUtil let serverURL: string +const goToDoc = async (page: Page) => { + await page.goto(url.list) + const linkToDoc = page.locator('tbody tr:first-child .cell-id a').first() + expect(linkToDoc).toBeTruthy() + await linkToDoc.click() +} + describe('Live Preview', () => { let page: Page @@ -28,69 +35,65 @@ describe('Live Preview', () => { }) test('collection - has tab', async () => { - await page.goto(url.create) + await goToDoc(page) + const docURL = page.url() + const pathname = new URL(docURL).pathname const livePreviewTab = page.locator('.doc-tab', { hasText: exactText('Live Preview'), }) expect(livePreviewTab).toBeTruthy() + const href = await livePreviewTab.locator('a').first().getAttribute('href') + expect(href).toBe(`${pathname}/preview`) }) test('collection - has route', async () => { - await page.goto(url.create) - await page.locator('#field-title').fill('Title 1') - await page.locator('#field-slug').fill('slug-1') - - await saveDocAndAssert(page) + await goToDoc(page) const docURL = page.url() - - const livePreviewTab = page.locator('.doc-tab', { - hasText: exactText('Live Preview'), - }) - - expect(livePreviewTab).toBeTruthy() - await livePreviewTab.click() + await page.goto(`${docURL}/preview`) expect(page.url()).toBe(`${docURL}/preview`) }) + test('collection - renders iframe', async () => { + await goToDoc(page) + const docURL = page.url() + await page.goto(`${docURL}/preview`) + expect(page.url()).toBe(`${docURL}/preview`) + const iframe = page.locator('iframe.live-preview-iframe') + await expect(iframe).toBeVisible() + }) + test('global - has tab', async () => { const global = new AdminUrlUtil(serverURL, 'header') await page.goto(global.global('header')) + const docURL = page.url() + const pathname = new URL(docURL).pathname const livePreviewTab = page.locator('.doc-tab', { hasText: exactText('Live Preview'), }) expect(livePreviewTab).toBeTruthy() + const href = await livePreviewTab.locator('a').first().getAttribute('href') + expect(href).toBe(`${pathname}/preview`) }) test('global - has route', async () => { const global = new AdminUrlUtil(serverURL, 'header') - await page.goto(global.global('header')) - - const docURL = page.url() - - const livePreviewTab = page.locator('.doc-tab', { - hasText: exactText('Live Preview'), - }) - - expect(livePreviewTab).toBeTruthy() - await livePreviewTab.click() - expect(page.url()).toBe(`${docURL}/preview`) + const previewURL = `${global.global('header')}/preview` + await page.goto(previewURL) + expect(page.url()).toBe(previewURL) }) - test('renders preview iframe on the page', async () => { - await page.goto(url.create) - await page.locator('#field-title').fill('Title 2') - await page.locator('#field-slug').fill('slug-2') - - await saveDocAndAssert(page) + test('global - renders iframe', async () => { + const global = new AdminUrlUtil(serverURL, 'header') + await page.goto(global.global('header')) const docURL = page.url() await page.goto(`${docURL}/preview`) expect(page.url()).toBe(`${docURL}/preview`) - const iframe = page.locator('iframe') - expect(iframe).toBeTruthy() + const iframe = page.locator('iframe.live-preview-iframe') + await expect(iframe).toBeVisible() }) test('properly measures iframe and displays size', async () => {