fix: renders live preview for globals (#3801)

This commit is contained in:
Jacob Fletcher
2023-10-23 14:30:51 -04:00
committed by GitHub
parent eaef0e7395
commit a13ec2ebc4
16 changed files with 174 additions and 87 deletions

View File

@@ -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<DocumentDrawerProps> = ({
disableActions: true,
disableLeaveWithoutSaving: true,
disableRoutes: true,
fieldTypes,
hasSavePermission,
internalState,
isEditing,

View File

@@ -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,

View File

@@ -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'

View File

@@ -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<CollectionEditViewProps> = (props) => {
export type DefaultAccountViewProps = CollectionEditViewProps & {
fieldTypes: FieldTypes
}
const DefaultAccount: React.FC<DefaultAccountViewProps> = (props) => {
const {
action,
apiURL,
collection,
data,
fieldTypes,
hasSavePermission,
initialState,
isLoading,
@@ -80,6 +86,7 @@ const DefaultAccount: React.FC<CollectionEditViewProps> = (props) => {
useAPIKey={auth.useAPIKey}
/>
}
fieldTypes={fieldTypes}
fields={fields}
hasSavePermission={hasSavePermission}
permissions={permissions}

View File

@@ -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 (
<RenderCustomComponent
CustomComponent={
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined
}
DefaultComponent={DefaultAccount}
componentProps={{
action,
apiURL,
collection,
data,
hasSavePermission,
initialState: internalState,
isLoading,
onSave,
permissions: docPermissions,
}}
componentProps={componentProps}
/>
)
}

View File

@@ -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<DefaultGlobalViewProps> = (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 ? (
<CustomGlobalComponent view="Default" {...props} />
) : (
<GlobalRoutes {...props} />
<GlobalRoutes {...props} fieldTypes={fieldTypes} />
)}
</React.Fragment>
)}

View File

@@ -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<GlobalEditViewProps> = (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<GlobalEditViewProps> = (props) => {
/>
<DocumentFields
description={description}
fieldTypes={fieldTypes}
fields={fields}
hasSavePermission={hasSavePermission}
permissions={permissions}

View File

@@ -3,6 +3,7 @@ import React from 'react'
import type { GlobalEditViewProps } from '../../types'
import { API } from '../../API'
import { LivePreviewView } from '../../LivePreview'
import VersionView from '../../Version/Version'
import VersionsView from '../../Versions'
import { DefaultGlobalEdit } from '../Default/index'
@@ -21,7 +22,7 @@ export const defaultGlobalViews: {
} = {
API,
Default: DefaultGlobalEdit,
LivePreview: null,
LivePreview: LivePreviewView,
References: null,
Relationships: null,
Version: VersionView,

View File

@@ -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 { GlobalEditViewProps } from '../../types'
import { useAuth } from '../../../utilities/Auth'
@@ -13,7 +14,11 @@ import { globalCustomRoutes } from './custom'
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const Unauthorized = lazy(() => import('../../Unauthorized'))
export const GlobalRoutes: React.FC<GlobalEditViewProps> = (props) => {
export const GlobalRoutes: React.FC<
GlobalEditViewProps & {
fieldTypes: FieldTypes
}
> = (props) => {
const { global, permissions } = props
const match = useRouteMatch()

View File

@@ -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<IndexProps> = (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,

View File

@@ -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<EditViewProps> = (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<EditViewProps> = (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<EditViewProps> = (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<EditViewProps> = (props) => {
return (
<Fragment>
{collection && (
<Meta
description={t('editing')}
keywords={`${getTranslation(collection.labels.singular, i18n)}, Payload, CMS`}
title={`${isEditing ? t('editing') : t('creating')} - ${getTranslation(
collection.labels.singular,
i18n,
)}`}
/>
)}
{global && (
<Meta
description={getTranslation(label, i18n)}
keywords={`${getTranslation(label, i18n)}, Payload, CMS`}
title={getTranslation(label, i18n)}
/>
)}
{((collection && !(collection.versions?.drafts && collection.versions?.drafts?.autosave)) ||
(global && !(global.versions?.drafts && global.versions?.drafts?.autosave))) &&
!disableLeaveWithoutSaving && <LeaveWithoutSaving />}
<SetStepNav
collection={collection}
global={global}
@@ -95,16 +124,6 @@ const PreviewView: React.FC<EditViewProps> = (props) => {
.filter(Boolean)
.join(' ')}
>
<Meta
description={t('editing')}
keywords={`${getTranslation(collection.labels.singular, i18n)}, Payload, CMS`}
title={`${isEditing ? t('editing') : t('creating')} - ${getTranslation(
collection.labels.singular,
i18n,
)}`}
/>
{!(collection.versions?.drafts && collection.versions?.drafts?.autosave) &&
!disableLeaveWithoutSaving && <LeaveWithoutSaving />}
<Gutter className={`${baseClass}__edit`}>
<RenderFields
fieldSchema={fields}
@@ -124,7 +143,11 @@ const PreviewView: React.FC<EditViewProps> = (props) => {
)
}
export const LivePreviewView: React.FC<EditViewProps> = (props) => {
export const LivePreviewView: React.FC<
EditViewProps & {
fieldTypes: FieldTypes
}
> = (props) => {
const config = useConfig()
const documentInfo = useDocumentInfo()
const locale = useLocale()

View File

@@ -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<DefaultEditViewProps> = (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 ? (
<CustomCollectionComponent view="Default" {...props} />
) : (
<CollectionRoutes {...props} />
<CollectionRoutes {...props} fieldTypes={fieldTypes} />
)}
</React.Fragment>
)}

View File

@@ -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<CollectionEditViewProps> = (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<CollectionEditViewProps> = (props)
data,
disableActions,
disableLeaveWithoutSaving,
fieldTypes,
hasSavePermission,
internalState,
isEditing,
@@ -79,6 +85,7 @@ export const DefaultCollectionEdit: React.FC<CollectionEditViewProps> = (props)
{upload && <Upload collection={collection} internalState={internalState} />}
</Fragment>
}
fieldTypes={fieldTypes}
fields={fields}
hasSavePermission={hasSavePermission}
permissions={permissions}

View File

@@ -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<CollectionEditViewProps> = (props) => {
export const CollectionRoutes: React.FC<
CollectionEditViewProps & {
fieldTypes: FieldTypes
}
> = (props) => {
const { collection, permissions } = props
const match = useRouteMatch()

View File

@@ -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<IndexProps> = (props) => {
const isLoading = !internalState || !docPermissions || isLoadingData
const componentProps: CollectionEditViewProps = {
const componentProps: DefaultEditViewProps = {
id,
action,
apiURL,
canAccessAdmin: permissions?.canAccessAdmin,
collection,
data,
fieldTypes,
hasSavePermission,
internalState,
isEditing,

View File

@@ -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 () => {