Compare commits
14 Commits
create-pay
...
richtext-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ecfe3da28 | ||
|
|
3e3163e875 | ||
|
|
cfe1698dfd | ||
|
|
5722634660 | ||
|
|
cdbfc9132a | ||
|
|
dd0ac066ce | ||
|
|
8b8ceabbdd | ||
|
|
8da18d3496 | ||
|
|
6cd4df3dc4 | ||
|
|
e74dc8633b | ||
|
|
2f945919a3 | ||
|
|
52dc9177d7 | ||
|
|
cec757d098 | ||
|
|
80e57150a0 |
7
.github/workflows/main.yml
vendored
7
.github/workflows/main.yml
vendored
@@ -132,7 +132,12 @@ jobs:
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: E2E Tests
|
||||
run: pnpm test:e2e --part ${{ matrix.part }} --bail
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
retry_on: error
|
||||
max_attempts: 2
|
||||
timeout_minutes: 15
|
||||
command: pnpm test:e2e --part ${{ matrix.part }} --bail
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,3 +1,28 @@
|
||||
|
||||
|
||||
## [2.0.8](https://github.com/payloadcms/payload/compare/v2.0.7...v2.0.8) (2023-10-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allows filterOptions to return null ([c4cac99](https://github.com/payloadcms/payload/commit/c4cac998752730e7084598c92c77789da8c82e0d))
|
||||
* **live-preview:** caches field schema ([#3711](https://github.com/payloadcms/payload/issues/3711)) ([dd0ac06](https://github.com/payloadcms/payload/commit/dd0ac066ce2ed88b85025309303610a95b6089e1))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* autosave time shown minutes only ([#3492](https://github.com/payloadcms/payload/issues/3492)) ([e311e8f](https://github.com/payloadcms/payload/commit/e311e8fff9cd4264d7a71903f63c4fa825a3564d))
|
||||
* blocks within groups in postgres ([45a62ba](https://github.com/payloadcms/payload/commit/45a62ba949aca33b25e0808773a5c2f1cf4adf82))
|
||||
* bug with seeding ecommerce ([993568a](https://github.com/payloadcms/payload/commit/993568a1959ea10f960e35e4ed7a8e06af672a72))
|
||||
* corrects add block index ([#3681](https://github.com/payloadcms/payload/issues/3681)) ([3c50443](https://github.com/payloadcms/payload/commit/3c5044368d5b30c76a2ff20c25b9234ef89dc205))
|
||||
* misc upload crop/focal point updates ([#3580](https://github.com/payloadcms/payload/issues/3580)) ([d616772](https://github.com/payloadcms/payload/commit/d6167727401a01282345e63636560e029ae8e0f3))
|
||||
* renders mobile document controls ([#3695](https://github.com/payloadcms/payload/issues/3695)) ([1625ff2](https://github.com/payloadcms/payload/commit/1625ff244e6e81e6edc0357037c3abc1a3bf8ba7))
|
||||
* some local operations missing req.transactionID ([#3651](https://github.com/payloadcms/payload/issues/3651)) ([150799e](https://github.com/payloadcms/payload/commit/150799e10e580281d1af49388eb142ee9639a002))
|
||||
* **richtext-*:** extra fields not being iterated correctly ([#3693](https://github.com/payloadcms/payload/issues/3693)) ([b8a5866](https://github.com/payloadcms/payload/commit/b8a58666e70f604af1e1cf349bcb4f9add0147e7))
|
||||
* **richtext-*:** link drawer form receiving incorrect field schema ([#3696](https://github.com/payloadcms/payload/issues/3696)) ([cb39354](https://github.com/payloadcms/payload/commit/cb39354a9de3d20960110e453f62c4aa166d8448))
|
||||
* **richtext-lexical:** [#3682](https://github.com/payloadcms/payload/issues/3682) isolated editor container causing z-index issues ([24918fe](https://github.com/payloadcms/payload/commit/24918fe1d2ca251e211632765d370c214cef2a38))
|
||||
* **templates:** user access control ([#3712](https://github.com/payloadcms/payload/issues/3712)) ([8b8ceab](https://github.com/payloadcms/payload/commit/8b8ceabbdd6354761e7d744cacb1192cac3a2427))
|
||||
|
||||
|
||||
## [2.0.6](https://github.com/payloadcms/payload/compare/v2.0.5...v2.0.6) (2023-10-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { mergeData } from '.'
|
||||
|
||||
// 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
|
||||
|
||||
export const handleMessage = async <T>(args: {
|
||||
depth: number
|
||||
event: MessageEvent
|
||||
@@ -11,9 +17,13 @@ export const handleMessage = async <T>(args: {
|
||||
const eventData = JSON.parse(event?.data)
|
||||
|
||||
if (eventData.type === 'payload-live-preview') {
|
||||
if (!payloadLivePreviewFieldSchema && eventData.fieldSchemaJSON) {
|
||||
payloadLivePreviewFieldSchema = eventData.fieldSchemaJSON
|
||||
}
|
||||
|
||||
const mergedData = await mergeData<T>({
|
||||
depth,
|
||||
fieldSchema: eventData.fieldSchemaJSON,
|
||||
fieldSchema: payloadLivePreviewFieldSchema,
|
||||
incomingData: eventData.data,
|
||||
initialData,
|
||||
serverURL,
|
||||
|
||||
@@ -2,11 +2,14 @@ export const ready = (args: { serverURL: string }): void => {
|
||||
const { serverURL } = args
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// This subscription may have been from either an iframe `src` or `window.open()`
|
||||
// i.e. `window?.opener` || `window?.parent`
|
||||
window?.opener?.postMessage(
|
||||
// This subscription may have been from either an iframe or a popup
|
||||
// We need to report 'ready' to the parent window, whichever it may be
|
||||
// i.e. `window?.opener` for popups, `window?.parent` for iframes
|
||||
const windowToPostTo: Window = window?.opener || window?.parent
|
||||
|
||||
windowToPostTo?.postMessage(
|
||||
JSON.stringify({
|
||||
popupReady: true,
|
||||
ready: true,
|
||||
type: 'payload-live-preview',
|
||||
}),
|
||||
serverURL,
|
||||
|
||||
@@ -9,7 +9,7 @@ export const subscribe = <T>(args: {
|
||||
const { callback, depth, initialData, serverURL } = args
|
||||
|
||||
const onMessage = async (event: MessageEvent) => {
|
||||
const mergedData = await handleMessage({ depth, event, initialData, serverURL })
|
||||
const mergedData = await handleMessage<T>({ depth, event, initialData, serverURL })
|
||||
callback(mergedData)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ module.exports = {
|
||||
skipChecks: true,
|
||||
},
|
||||
hooks: {
|
||||
'before:init': ['pnpm install', 'pnpm clean', 'pnpm test'],
|
||||
'before:init': ['pnpm install', 'pnpm clean', 'pnpm build'], // Assume tests have already been run
|
||||
},
|
||||
plugins: {
|
||||
'@release-it/conventional-changelog': {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.8",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { Props } from './types'
|
||||
@@ -37,7 +37,7 @@ const RenderFields: React.FC<Props> = (props) => {
|
||||
|
||||
const { i18n, t } = useTranslation('general')
|
||||
const [hasRendered, setHasRendered] = useState(Boolean(forceRender))
|
||||
const [intersectionRef, entry] = useIntersect(intersectionObserverOptions)
|
||||
const [intersectionRef, entry] = useIntersect(intersectionObserverOptions, forceRender)
|
||||
|
||||
const isIntersecting = Boolean(entry?.isIntersecting)
|
||||
const isAboveViewport = entry?.boundingClientRect?.top < 0
|
||||
@@ -105,6 +105,7 @@ const RenderFields: React.FC<Props> = (props) => {
|
||||
readOnly,
|
||||
},
|
||||
fieldTypes,
|
||||
forceRender,
|
||||
indexPath:
|
||||
'indexPath' in props ? `${props?.indexPath}.${fieldIndex}` : `${fieldIndex}`,
|
||||
path: field.path || (isFieldAffectingData && 'name' in field ? field.name : ''),
|
||||
|
||||
@@ -24,6 +24,7 @@ type ArrayRowProps = UseDraggableSortableReturn &
|
||||
CustomRowLabel?: RowLabelType
|
||||
addRow: (rowIndex: number) => void
|
||||
duplicateRow: (rowIndex: number) => void
|
||||
forceRender?: boolean
|
||||
hasMaxRows?: boolean
|
||||
moveRow: (fromIndex: number, toIndex: number) => void
|
||||
readOnly?: boolean
|
||||
@@ -40,6 +41,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
||||
duplicateRow,
|
||||
fieldTypes,
|
||||
fields,
|
||||
forceRender = false,
|
||||
hasMaxRows,
|
||||
indexPath,
|
||||
labels,
|
||||
@@ -126,10 +128,11 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
||||
path: createNestedFieldPath(path, field),
|
||||
}))}
|
||||
fieldTypes={fieldTypes}
|
||||
forceRender={forceRender}
|
||||
indexPath={indexPath}
|
||||
margins="small"
|
||||
permissions={permissions?.fields}
|
||||
readOnly={readOnly}
|
||||
margins="small"
|
||||
/>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
admin: { className, components, condition, description, readOnly },
|
||||
fieldTypes,
|
||||
fields,
|
||||
forceRender = false,
|
||||
indexPath,
|
||||
localized,
|
||||
maxRows,
|
||||
@@ -234,6 +235,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
duplicateRow={duplicateRow}
|
||||
fieldTypes={fieldTypes}
|
||||
fields={fields}
|
||||
forceRender={forceRender}
|
||||
hasMaxRows={hasMaxRows}
|
||||
indexPath={indexPath}
|
||||
labels={labels}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ArrayField } from '../../../../../fields/config/types'
|
||||
|
||||
export type Props = Omit<ArrayField, 'type'> & {
|
||||
fieldTypes: FieldTypes
|
||||
forceRender?: boolean
|
||||
indexPath: string
|
||||
label: false | string
|
||||
path?: string
|
||||
|
||||
@@ -24,6 +24,7 @@ type BlockFieldProps = UseDraggableSortableReturn &
|
||||
addRow: (rowIndex: number, blockType: string) => void
|
||||
blockToRender: Block
|
||||
duplicateRow: (rowIndex: number) => void
|
||||
forceRender?: boolean
|
||||
hasMaxRows?: boolean
|
||||
moveRow: (fromIndex: number, toIndex: number) => void
|
||||
readOnly: boolean
|
||||
@@ -40,6 +41,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
||||
blocks,
|
||||
duplicateRow,
|
||||
fieldTypes,
|
||||
forceRender,
|
||||
hasMaxRows,
|
||||
indexPath,
|
||||
labels,
|
||||
@@ -130,6 +132,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
||||
path: createNestedFieldPath(path, field),
|
||||
}))}
|
||||
fieldTypes={fieldTypes}
|
||||
forceRender={forceRender}
|
||||
indexPath={indexPath}
|
||||
margins="small"
|
||||
permissions={permissions?.blocks?.[row.blockType]?.fields}
|
||||
|
||||
@@ -37,6 +37,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
admin: { className, condition, description, readOnly },
|
||||
blocks,
|
||||
fieldTypes,
|
||||
forceRender = false,
|
||||
indexPath,
|
||||
label,
|
||||
labels: labelsFromProps,
|
||||
@@ -238,6 +239,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
blocks={blocks}
|
||||
duplicateRow={duplicateRow}
|
||||
fieldTypes={fieldTypes}
|
||||
forceRender={forceRender}
|
||||
hasMaxRows={hasMaxRows}
|
||||
indexPath={indexPath}
|
||||
labels={labels}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { BlockField } from '../../../../../fields/config/types'
|
||||
|
||||
export type Props = Omit<BlockField, 'type'> & {
|
||||
fieldTypes: FieldTypes
|
||||
forceRender?: boolean
|
||||
indexPath: string
|
||||
path?: string
|
||||
permissions: FieldPermissions
|
||||
|
||||
@@ -14,9 +14,9 @@ import { WatchChildErrors } from '../../WatchChildErrors'
|
||||
import withCondition from '../../withCondition'
|
||||
import { useRow } from '../Row/provider'
|
||||
import { useTabs } from '../Tabs/provider'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
import { GroupProvider, useGroup } from './provider'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
|
||||
const baseClass = 'group-field'
|
||||
|
||||
@@ -26,6 +26,7 @@ const Group: React.FC<Props> = (props) => {
|
||||
admin: { className, description, hideGutter = false, readOnly, style, width },
|
||||
fieldTypes,
|
||||
fields,
|
||||
forceRender = false,
|
||||
indexPath,
|
||||
label,
|
||||
path: pathFromProps,
|
||||
@@ -88,6 +89,7 @@ const Group: React.FC<Props> = (props) => {
|
||||
path: createNestedFieldPath(path, subField),
|
||||
}))}
|
||||
fieldTypes={fieldTypes}
|
||||
forceRender={forceRender}
|
||||
indexPath={indexPath}
|
||||
margins="small"
|
||||
permissions={permissions?.fields}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { GroupField } from '../../../../../fields/config/types'
|
||||
|
||||
export type Props = Omit<GroupField, 'type'> & {
|
||||
fieldTypes: FieldTypes
|
||||
forceRender?: boolean
|
||||
indexPath: string
|
||||
path?: string
|
||||
permissions: FieldPermissions
|
||||
|
||||
@@ -14,6 +14,7 @@ const Row: React.FC<Props> = (props) => {
|
||||
admin: { className, readOnly },
|
||||
fieldTypes,
|
||||
fields,
|
||||
forceRender = false,
|
||||
indexPath,
|
||||
path,
|
||||
permissions,
|
||||
@@ -28,6 +29,7 @@ const Row: React.FC<Props> = (props) => {
|
||||
path: createNestedFieldPath(path, field),
|
||||
}))}
|
||||
fieldTypes={fieldTypes}
|
||||
forceRender={forceRender}
|
||||
indexPath={indexPath}
|
||||
permissions={permissions}
|
||||
readOnly={readOnly}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { RowField } from '../../../../../fields/config/types'
|
||||
|
||||
export type Props = Omit<RowField, 'type'> & {
|
||||
fieldTypes: FieldTypes
|
||||
forceRender?: boolean
|
||||
indexPath: string
|
||||
path?: string
|
||||
permissions: FieldPermissions
|
||||
|
||||
@@ -18,9 +18,9 @@ import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
|
||||
import RenderFields from '../../RenderFields'
|
||||
import { WatchChildErrors } from '../../WatchChildErrors'
|
||||
import withCondition from '../../withCondition'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
import { TabsProvider } from './provider'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
|
||||
const baseClass = 'tabs-field'
|
||||
|
||||
@@ -72,6 +72,7 @@ const TabsField: React.FC<Props> = (props) => {
|
||||
const {
|
||||
admin: { className, readOnly },
|
||||
fieldTypes,
|
||||
forceRender = false,
|
||||
indexPath,
|
||||
path,
|
||||
permissions,
|
||||
@@ -188,7 +189,7 @@ const TabsField: React.FC<Props> = (props) => {
|
||||
}
|
||||
})}
|
||||
fieldTypes={fieldTypes}
|
||||
forceRender
|
||||
forceRender={forceRender}
|
||||
indexPath={indexPath}
|
||||
key={String(activeTabConfig.label)}
|
||||
margins="small"
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { TabsField } from '../../../../../fields/config/types'
|
||||
|
||||
export type Props = Omit<TabsField, 'type'> & {
|
||||
fieldTypes: FieldTypes
|
||||
forceRender?: boolean
|
||||
indexPath: string
|
||||
path?: string
|
||||
permissions: FieldPermissions
|
||||
|
||||
@@ -3,21 +3,31 @@ import type { Dispatch } from 'react'
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
import type { LivePreviewConfig } from '../../../../../exports/config'
|
||||
import type { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
|
||||
import type { usePopupWindow } from '../usePopupWindow'
|
||||
import type { SizeReducerAction } from './sizeReducer'
|
||||
|
||||
export interface LivePreviewContextType {
|
||||
appIsReady: boolean
|
||||
breakpoint: LivePreviewConfig['breakpoints'][number]['name']
|
||||
breakpoints: LivePreviewConfig['breakpoints']
|
||||
fieldSchemaJSON?: ReturnType<typeof fieldSchemaToJSON>
|
||||
iframeHasLoaded: boolean
|
||||
iframeRef: React.RefObject<HTMLIFrameElement>
|
||||
isPopupOpen: boolean
|
||||
measuredDeviceSize: {
|
||||
height: number
|
||||
width: number
|
||||
}
|
||||
openPopupWindow: ReturnType<typeof usePopupWindow>['openPopupWindow']
|
||||
popupRef?: React.MutableRefObject<Window | null>
|
||||
previewWindowType: 'iframe' | 'popup'
|
||||
setAppIsReady: (appIsReady: boolean) => void
|
||||
setBreakpoint: (breakpoint: LivePreviewConfig['breakpoints'][number]['name']) => void
|
||||
setHeight: (height: number) => void
|
||||
setIframeHasLoaded: (loaded: boolean) => void
|
||||
setMeasuredDeviceSize: (size: { height: number; width: number }) => void
|
||||
setPreviewWindowType: (previewWindowType: 'iframe' | 'popup') => void
|
||||
setSize: Dispatch<SizeReducerAction>
|
||||
setToolbarPosition: (position: { x: number; y: number }) => void
|
||||
setWidth: (width: number) => void
|
||||
@@ -30,22 +40,31 @@ export interface LivePreviewContextType {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
url: string | undefined
|
||||
zoom: number
|
||||
}
|
||||
|
||||
export const LivePreviewContext = createContext<LivePreviewContextType>({
|
||||
appIsReady: false,
|
||||
breakpoint: undefined,
|
||||
breakpoints: undefined,
|
||||
fieldSchemaJSON: undefined,
|
||||
iframeHasLoaded: false,
|
||||
iframeRef: undefined,
|
||||
isPopupOpen: false,
|
||||
measuredDeviceSize: {
|
||||
height: 0,
|
||||
width: 0,
|
||||
},
|
||||
openPopupWindow: () => {},
|
||||
popupRef: undefined,
|
||||
previewWindowType: 'iframe',
|
||||
setAppIsReady: () => {},
|
||||
setBreakpoint: () => {},
|
||||
setHeight: () => {},
|
||||
setIframeHasLoaded: () => {},
|
||||
setMeasuredDeviceSize: () => {},
|
||||
setPreviewWindowType: () => {},
|
||||
setSize: () => {},
|
||||
setToolbarPosition: () => {},
|
||||
setWidth: () => {},
|
||||
@@ -58,6 +77,7 @@ export const LivePreviewContext = createContext<LivePreviewContextType>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
url: undefined,
|
||||
zoom: 1,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,39 +1,48 @@
|
||||
import { DndContext } from '@dnd-kit/core'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import type { LivePreviewConfig } from '../../../../../exports/config'
|
||||
import type { Field } from '../../../../../fields/config/types'
|
||||
import type { EditViewProps } from '../../types'
|
||||
import type { usePopupWindow } from '../usePopupWindow'
|
||||
|
||||
import { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
|
||||
import { customCollisionDetection } from './collisionDetection'
|
||||
import { LivePreviewContext } from './context'
|
||||
import { sizeReducer } from './sizeReducer'
|
||||
|
||||
export type ToolbarProviderProps = EditViewProps & {
|
||||
export type LivePreviewProviderProps = EditViewProps & {
|
||||
appIsReady?: boolean
|
||||
breakpoints?: LivePreviewConfig['breakpoints']
|
||||
children: React.ReactNode
|
||||
deviceSize?: {
|
||||
height: number
|
||||
width: number
|
||||
}
|
||||
popupState: ReturnType<typeof usePopupWindow>
|
||||
isPopupOpen?: boolean
|
||||
openPopupWindow?: ReturnType<typeof usePopupWindow>['openPopupWindow']
|
||||
popupRef?: React.MutableRefObject<Window>
|
||||
url?: string
|
||||
}
|
||||
|
||||
export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
|
||||
const { breakpoints, children } = props
|
||||
export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = (props) => {
|
||||
const { breakpoints, children, isPopupOpen, openPopupWindow, popupRef, url } = props
|
||||
|
||||
const [previewWindowType, setPreviewWindowType] = useState<'iframe' | 'popup'>('iframe')
|
||||
|
||||
const [appIsReady, setAppIsReady] = useState(false)
|
||||
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null)
|
||||
|
||||
const [iframeHasLoaded, setIframeHasLoaded] = React.useState(false)
|
||||
const [iframeHasLoaded, setIframeHasLoaded] = useState(false)
|
||||
|
||||
const [zoom, setZoom] = React.useState(1)
|
||||
const [zoom, setZoom] = useState(1)
|
||||
|
||||
const [position, setPosition] = React.useState({ x: 0, y: 0 })
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||
|
||||
const [size, setSize] = React.useReducer(sizeReducer, { height: 0, width: 0 })
|
||||
|
||||
const [measuredDeviceSize, setMeasuredDeviceSize] = React.useState({
|
||||
const [measuredDeviceSize, setMeasuredDeviceSize] = useState({
|
||||
height: 0,
|
||||
width: 0,
|
||||
})
|
||||
@@ -41,6 +50,22 @@ export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
|
||||
const [breakpoint, setBreakpoint] =
|
||||
React.useState<LivePreviewConfig['breakpoints'][0]['name']>('responsive')
|
||||
|
||||
const [fieldSchemaJSON] = useState(() => {
|
||||
let fields: Field[]
|
||||
|
||||
if ('collection' in props) {
|
||||
const { collection } = props
|
||||
fields = collection.fields
|
||||
}
|
||||
|
||||
if ('global' in props) {
|
||||
const { global } = props
|
||||
fields = global.fields
|
||||
}
|
||||
|
||||
return fieldSchemaToJSON(fields)
|
||||
})
|
||||
|
||||
// The toolbar needs to freely drag and drop around the page
|
||||
const handleDragEnd = (ev) => {
|
||||
// only update position if the toolbar is completely within the preview area
|
||||
@@ -94,24 +119,70 @@ export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
|
||||
}
|
||||
}, [breakpoint, breakpoints])
|
||||
|
||||
// Receive the `ready` message from the popup window
|
||||
// This indicates that the app is ready to receive `window.postMessage` events
|
||||
// This is also the only cross-origin way of detecting when a popup window has loaded
|
||||
// Unlike iframe elements which have an `onLoad` handler, there is no way to access `window.open` on popups
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data)
|
||||
|
||||
if (url.startsWith(event.origin) && data.type === 'payload-live-preview' && data.ready) {
|
||||
setAppIsReady(true)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
}
|
||||
}, [url])
|
||||
|
||||
const handleWindowChange = useCallback(
|
||||
(type: 'iframe' | 'popup') => {
|
||||
setAppIsReady(false)
|
||||
setPreviewWindowType(type)
|
||||
if (type === 'popup') openPopupWindow()
|
||||
},
|
||||
[openPopupWindow],
|
||||
)
|
||||
|
||||
// when the user closes the popup window, switch back to the iframe
|
||||
// the `usePopupWindow` reports the `isPopupOpen` state for us to use here
|
||||
useEffect(() => {
|
||||
if (!isPopupOpen) {
|
||||
handleWindowChange('iframe')
|
||||
}
|
||||
}, [isPopupOpen, handleWindowChange])
|
||||
|
||||
return (
|
||||
<LivePreviewContext.Provider
|
||||
value={{
|
||||
appIsReady,
|
||||
breakpoint,
|
||||
breakpoints,
|
||||
fieldSchemaJSON,
|
||||
iframeHasLoaded,
|
||||
iframeRef,
|
||||
isPopupOpen,
|
||||
measuredDeviceSize,
|
||||
openPopupWindow,
|
||||
popupRef,
|
||||
previewWindowType,
|
||||
setAppIsReady,
|
||||
setBreakpoint,
|
||||
setHeight,
|
||||
setIframeHasLoaded,
|
||||
setMeasuredDeviceSize,
|
||||
setPreviewWindowType: handleWindowChange,
|
||||
setSize,
|
||||
setToolbarPosition: setPosition,
|
||||
setWidth,
|
||||
setZoom,
|
||||
size,
|
||||
toolbarPosition: position,
|
||||
url,
|
||||
zoom,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.live-preview-iframe {
|
||||
background-color: white;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.live-preview-window {
|
||||
background-color: var(--theme-bg);
|
||||
width: 60%;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
import type { LivePreviewConfig } from '../../../../../exports/config'
|
||||
import type { Field } from '../../../../../fields/config/types'
|
||||
import type { EditViewProps } from '../../types'
|
||||
import type { usePopupWindow } from '../usePopupWindow'
|
||||
|
||||
import { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
|
||||
import { useAllFormFields } from '../../../forms/Form/context'
|
||||
import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues'
|
||||
import { LivePreviewProvider } from '../Context'
|
||||
import { useLivePreviewContext } from '../Context/context'
|
||||
import { DeviceContainer } from '../Device'
|
||||
import { IFrame } from '../IFrame'
|
||||
@@ -17,93 +12,81 @@ import './index.scss'
|
||||
|
||||
const baseClass = 'live-preview-window'
|
||||
|
||||
const Preview: React.FC<
|
||||
EditViewProps & {
|
||||
popupState: ReturnType<typeof usePopupWindow>
|
||||
url?: string
|
||||
}
|
||||
> = (props) => {
|
||||
export const LivePreview: React.FC<EditViewProps> = (props) => {
|
||||
const {
|
||||
popupState: { isPopupOpen, popupHasLoaded, popupRef },
|
||||
appIsReady,
|
||||
iframeHasLoaded,
|
||||
iframeRef,
|
||||
popupRef,
|
||||
previewWindowType,
|
||||
setIframeHasLoaded,
|
||||
url,
|
||||
} = props
|
||||
} = useLivePreviewContext()
|
||||
|
||||
const { iframeHasLoaded, iframeRef, setIframeHasLoaded } = useLivePreviewContext()
|
||||
const { breakpoint, fieldSchemaJSON } = useLivePreviewContext()
|
||||
|
||||
const { breakpoint } = useLivePreviewContext()
|
||||
const prevWindowType =
|
||||
React.useRef<ReturnType<typeof useLivePreviewContext>['previewWindowType']>()
|
||||
|
||||
const [fields] = useAllFormFields()
|
||||
|
||||
const [fieldSchemaJSON] = useState(() => {
|
||||
let fields: Field[]
|
||||
|
||||
if ('collection' in props) {
|
||||
const { collection } = props
|
||||
fields = collection.fields
|
||||
}
|
||||
|
||||
if ('global' in props) {
|
||||
const { global } = props
|
||||
fields = global.fields
|
||||
}
|
||||
|
||||
return fieldSchemaToJSON(fields)
|
||||
})
|
||||
|
||||
// The preview could either be an iframe embedded on the page
|
||||
// Or it could be a separate popup window
|
||||
// We need to transmit data to both accordingly
|
||||
useEffect(() => {
|
||||
if (fields && window && 'postMessage' in window) {
|
||||
// For performance, do no reduce fields to values until after the iframe or popup has loaded
|
||||
if (fields && window && 'postMessage' in window && appIsReady) {
|
||||
const values = reduceFieldsToValues(fields, true)
|
||||
|
||||
// TODO: only send `fieldSchemaToJSON` one time
|
||||
// To reduce on large `postMessage` payloads, only send `fieldSchemaToJSON` one time
|
||||
// To do this, the underlying JS function maintains a cache of this value
|
||||
// So we need to send it through each time the window type changes
|
||||
// But only once per window type change, not on every render, because this is a potentially large obj
|
||||
const shouldSendSchema =
|
||||
!prevWindowType.current || prevWindowType.current !== previewWindowType
|
||||
|
||||
prevWindowType.current = previewWindowType
|
||||
|
||||
const message = JSON.stringify({
|
||||
data: values,
|
||||
fieldSchemaJSON,
|
||||
fieldSchemaJSON: shouldSendSchema ? fieldSchemaJSON : undefined,
|
||||
type: 'payload-live-preview',
|
||||
})
|
||||
|
||||
// external window
|
||||
if (isPopupOpen) {
|
||||
setIframeHasLoaded(false)
|
||||
if (popupRef.current) {
|
||||
popupRef.current.postMessage(message, url)
|
||||
}
|
||||
// Post message to external popup window
|
||||
if (previewWindowType === 'popup' && popupRef.current) {
|
||||
popupRef.current.postMessage(message, url)
|
||||
}
|
||||
|
||||
// embedded iframe
|
||||
if (!isPopupOpen) {
|
||||
if (iframeHasLoaded && iframeRef.current) {
|
||||
iframeRef.current.contentWindow?.postMessage(message, url)
|
||||
}
|
||||
// Post message to embedded iframe
|
||||
if (previewWindowType === 'iframe' && iframeRef.current) {
|
||||
iframeRef.current.contentWindow?.postMessage(message, url)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
fields,
|
||||
url,
|
||||
iframeHasLoaded,
|
||||
isPopupOpen,
|
||||
previewWindowType,
|
||||
popupRef,
|
||||
popupHasLoaded,
|
||||
appIsReady,
|
||||
iframeRef,
|
||||
setIframeHasLoaded,
|
||||
fieldSchemaJSON,
|
||||
])
|
||||
|
||||
if (!isPopupOpen) {
|
||||
if (previewWindowType === 'iframe') {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
baseClass,
|
||||
isPopupOpen && `${baseClass}--popup-open`,
|
||||
breakpoint && breakpoint !== 'responsive' && `${baseClass}--has-breakpoint`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<LivePreviewToolbar {...props} iframeRef={iframeRef} url={url} />
|
||||
<LivePreviewToolbar {...props} />
|
||||
<div className={`${baseClass}__main`}>
|
||||
<DeviceContainer>
|
||||
<IFrame ref={iframeRef} setIframeHasLoaded={setIframeHasLoaded} url={url} />
|
||||
@@ -114,29 +97,3 @@ const Preview: React.FC<
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const LivePreview: React.FC<
|
||||
EditViewProps & {
|
||||
livePreviewConfig?: LivePreviewConfig
|
||||
popupState: ReturnType<typeof usePopupWindow>
|
||||
url?: string
|
||||
}
|
||||
> = (props) => {
|
||||
const { livePreviewConfig, url } = props
|
||||
|
||||
const breakpoints: LivePreviewConfig['breakpoints'] = [
|
||||
...(livePreviewConfig?.breakpoints || []),
|
||||
{
|
||||
name: 'responsive',
|
||||
height: '100%',
|
||||
label: 'Responsive',
|
||||
width: '100%',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<LivePreviewProvider {...props} breakpoints={breakpoints} url={url}>
|
||||
<Preview {...props} />
|
||||
</LivePreviewProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { LivePreviewToolbarProps } from '..'
|
||||
import type { EditViewProps } from '../../../types'
|
||||
|
||||
import { X } from '../../../..'
|
||||
import { ExternalLinkIcon } from '../../../../graphics/ExternalLink'
|
||||
@@ -10,13 +10,9 @@ import './index.scss'
|
||||
|
||||
const baseClass = 'live-preview-toolbar-controls'
|
||||
|
||||
export const ToolbarControls: React.FC<LivePreviewToolbarProps> = (props) => {
|
||||
const { breakpoint, breakpoints, setBreakpoint, setZoom, zoom } = useLivePreviewContext()
|
||||
|
||||
const {
|
||||
popupState: { openPopupWindow },
|
||||
url,
|
||||
} = props
|
||||
export const ToolbarControls: React.FC<EditViewProps> = () => {
|
||||
const { breakpoint, breakpoints, setBreakpoint, setPreviewWindowType, setZoom, url, zoom } =
|
||||
useLivePreviewContext()
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -57,7 +53,15 @@ export const ToolbarControls: React.FC<LivePreviewToolbarProps> = (props) => {
|
||||
<option value={150}>150%</option>
|
||||
<option value={200}>200%</option>
|
||||
</select>
|
||||
<a className={`${baseClass}__external`} href={url} onClick={openPopupWindow} type="button">
|
||||
<a
|
||||
className={`${baseClass}__external`}
|
||||
href={url}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setPreviewWindowType('popup')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<ExternalLinkIcon />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useDraggable } from '@dnd-kit/core'
|
||||
import React from 'react'
|
||||
|
||||
import type { ToolbarProviderProps } from '../Context'
|
||||
import type { EditViewProps } from '../../types'
|
||||
|
||||
import DragHandle from '../../../icons/Drag'
|
||||
import { useLivePreviewContext } from '../Context/context'
|
||||
@@ -10,11 +10,7 @@ import './index.scss'
|
||||
|
||||
const baseClass = 'live-preview-toolbar'
|
||||
|
||||
export type LivePreviewToolbarProps = Omit<ToolbarProviderProps, 'children'> & {
|
||||
iframeRef: React.RefObject<HTMLIFrameElement>
|
||||
}
|
||||
|
||||
const DraggableToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
|
||||
const DraggableToolbar: React.FC<EditViewProps> = (props) => {
|
||||
const { toolbarPosition } = useLivePreviewContext()
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
@@ -50,7 +46,7 @@ const DraggableToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
const StaticToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
|
||||
const StaticToolbar: React.FC<EditViewProps> = (props) => {
|
||||
return (
|
||||
<div className={[baseClass, `${baseClass}--static`].join(' ')}>
|
||||
<ToolbarControls {...props} />
|
||||
@@ -59,7 +55,7 @@ const StaticToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
|
||||
}
|
||||
|
||||
export const LivePreviewToolbar: React.FC<
|
||||
LivePreviewToolbarProps & {
|
||||
EditViewProps & {
|
||||
draggable?: boolean
|
||||
}
|
||||
> = (props) => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
.live-preview {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
--gradient: linear-gradient(to left, rgba(0, 0, 0, 0.04) 0%, transparent 100%);
|
||||
|
||||
[dir='rtl'] & {
|
||||
flex-direction: row-reverse;
|
||||
@@ -34,7 +35,7 @@
|
||||
right: 0;
|
||||
width: calc(var(--base) * 2);
|
||||
height: 100%;
|
||||
background: linear-gradient(to left, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
background: var(--gradient);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
@@ -58,6 +59,10 @@
|
||||
&__main {
|
||||
min-height: initial;
|
||||
width: 100%;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
@@ -77,3 +82,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='dark'] {
|
||||
.live-preview {
|
||||
--gradient: linear-gradient(to left, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,47 +17,17 @@ import { useDocumentInfo } from '../../utilities/DocumentInfo'
|
||||
import { useLocale } from '../../utilities/Locale'
|
||||
import Meta from '../../utilities/Meta'
|
||||
import { SetStepNav } from '../collections/Edit/SetStepNav'
|
||||
import { LivePreviewProvider } from './Context'
|
||||
import { useLivePreviewContext } from './Context/context'
|
||||
import { LivePreview } from './Preview'
|
||||
import './index.scss'
|
||||
import { usePopupWindow } from './usePopupWindow'
|
||||
|
||||
const baseClass = 'live-preview'
|
||||
|
||||
export const LivePreviewView: React.FC<EditViewProps> = (props) => {
|
||||
const PreviewView: React.FC<EditViewProps> = (props) => {
|
||||
const { i18n, t } = useTranslation('general')
|
||||
const config = useConfig()
|
||||
const documentInfo = useDocumentInfo()
|
||||
const locale = useLocale()
|
||||
|
||||
let livePreviewConfig: LivePreviewConfig = config?.admin?.livePreview
|
||||
|
||||
if ('collection' in props) {
|
||||
livePreviewConfig = {
|
||||
...(livePreviewConfig || {}),
|
||||
...(props?.collection.admin.livePreview || {}),
|
||||
}
|
||||
}
|
||||
|
||||
if ('global' in props) {
|
||||
livePreviewConfig = {
|
||||
...(livePreviewConfig || {}),
|
||||
...(props?.global.admin.livePreview || {}),
|
||||
}
|
||||
}
|
||||
|
||||
const url =
|
||||
typeof livePreviewConfig?.url === 'function'
|
||||
? livePreviewConfig?.url({
|
||||
data: props?.data,
|
||||
documentInfo,
|
||||
locale,
|
||||
})
|
||||
: livePreviewConfig?.url
|
||||
|
||||
const popupState = usePopupWindow({
|
||||
eventType: 'payload-live-preview',
|
||||
url,
|
||||
})
|
||||
const { previewWindowType } = useLivePreviewContext()
|
||||
|
||||
const { apiURL, data, permissions } = props
|
||||
|
||||
@@ -113,14 +83,14 @@ export const LivePreviewView: React.FC<EditViewProps> = (props) => {
|
||||
permissions={permissions}
|
||||
/>
|
||||
<div
|
||||
className={[baseClass, popupState?.isPopupOpen && `${baseClass}--detached`]
|
||||
className={[baseClass, previewWindowType === 'popup' && `${baseClass}--detached`]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
`${baseClass}__main`,
|
||||
popupState?.isPopupOpen && `${baseClass}__main--popup-open`,
|
||||
previewWindowType === 'popup' && `${baseClass}__main--popup-open`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
@@ -148,13 +118,67 @@ export const LivePreviewView: React.FC<EditViewProps> = (props) => {
|
||||
)}
|
||||
</Gutter>
|
||||
</div>
|
||||
<LivePreview
|
||||
{...props}
|
||||
livePreviewConfig={livePreviewConfig}
|
||||
popupState={popupState}
|
||||
url={url}
|
||||
/>
|
||||
<LivePreview {...props} />
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export const LivePreviewView: React.FC<EditViewProps> = (props) => {
|
||||
const config = useConfig()
|
||||
const documentInfo = useDocumentInfo()
|
||||
const locale = useLocale()
|
||||
|
||||
let livePreviewConfig: LivePreviewConfig = config?.admin?.livePreview
|
||||
|
||||
if ('collection' in props) {
|
||||
livePreviewConfig = {
|
||||
...(livePreviewConfig || {}),
|
||||
...(props?.collection.admin.livePreview || {}),
|
||||
}
|
||||
}
|
||||
|
||||
if ('global' in props) {
|
||||
livePreviewConfig = {
|
||||
...(livePreviewConfig || {}),
|
||||
...(props?.global.admin.livePreview || {}),
|
||||
}
|
||||
}
|
||||
|
||||
const url =
|
||||
typeof livePreviewConfig?.url === 'function'
|
||||
? livePreviewConfig?.url({
|
||||
data: props?.data,
|
||||
documentInfo,
|
||||
locale,
|
||||
})
|
||||
: livePreviewConfig?.url
|
||||
|
||||
const breakpoints: LivePreviewConfig['breakpoints'] = [
|
||||
...(livePreviewConfig?.breakpoints || []),
|
||||
{
|
||||
name: 'responsive',
|
||||
height: '100%',
|
||||
label: 'Responsive',
|
||||
width: '100%',
|
||||
},
|
||||
]
|
||||
|
||||
const { isPopupOpen, openPopupWindow, popupRef } = usePopupWindow({
|
||||
eventType: 'payload-live-preview',
|
||||
url,
|
||||
})
|
||||
|
||||
return (
|
||||
<LivePreviewProvider
|
||||
{...props}
|
||||
breakpoints={breakpoints}
|
||||
isPopupOpen={isPopupOpen}
|
||||
openPopupWindow={openPopupWindow}
|
||||
popupRef={popupRef}
|
||||
url={url}
|
||||
>
|
||||
<PreviewView {...props} />
|
||||
</LivePreviewProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,17 +19,14 @@ export const usePopupWindow = (props: {
|
||||
url: string
|
||||
}): {
|
||||
isPopupOpen: boolean
|
||||
openPopupWindow: (e: React.MouseEvent<HTMLAnchorElement>) => void
|
||||
popupHasLoaded: boolean
|
||||
openPopupWindow: () => void
|
||||
popupRef?: React.MutableRefObject<Window | null>
|
||||
} => {
|
||||
const { eventType, onMessage, url } = props
|
||||
const isReceivingMessage = useRef(false)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [popupHasLoaded, setPopupHasLoaded] = useState(false)
|
||||
const { serverURL } = useConfig()
|
||||
const popupRef = useRef<Window | null>(null)
|
||||
const hasAttachedMessageListener = useRef(false)
|
||||
|
||||
// Optionally broadcast messages back out to the parent component
|
||||
useEffect(() => {
|
||||
@@ -65,8 +62,10 @@ export const usePopupWindow = (props: {
|
||||
|
||||
// Customize the size, position, and style of the popup window
|
||||
const openPopupWindow = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
(e?: MouseEvent) => {
|
||||
if (e) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const features = {
|
||||
height: 700,
|
||||
@@ -106,27 +105,6 @@ export const usePopupWindow = (props: {
|
||||
[url],
|
||||
)
|
||||
|
||||
// the only cross-origin way of detecting when a popup window has loaded
|
||||
// we catch a message event that the site rendered within the popup window fires
|
||||
// there is no way in js to add an event listener to a popup window across domains
|
||||
useEffect(() => {
|
||||
if (hasAttachedMessageListener.current) return
|
||||
hasAttachedMessageListener.current = true
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
|
||||
if (
|
||||
url.startsWith(event.origin) &&
|
||||
data.type === eventType &&
|
||||
data.popupReady &&
|
||||
!popupHasLoaded
|
||||
) {
|
||||
setPopupHasLoaded(true)
|
||||
}
|
||||
})
|
||||
}, [url, eventType, popupHasLoaded])
|
||||
|
||||
// this is the most stable and widely supported way to check if a popup window is no longer open
|
||||
// we poll its ref every x ms and use the popup window's `closed` property
|
||||
useEffect(() => {
|
||||
@@ -137,7 +115,6 @@ export const usePopupWindow = (props: {
|
||||
if (popupRef.current.closed) {
|
||||
clearInterval(timer)
|
||||
setIsOpen(false)
|
||||
setPopupHasLoaded(false)
|
||||
}
|
||||
}, 1000)
|
||||
} else {
|
||||
@@ -154,7 +131,6 @@ export const usePopupWindow = (props: {
|
||||
return {
|
||||
isPopupOpen: isOpen,
|
||||
openPopupWindow,
|
||||
popupHasLoaded,
|
||||
popupRef,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
type Intersect = [setNode: React.Dispatch<Element>, entry: IntersectionObserverEntry]
|
||||
|
||||
const useIntersect = ({ root = null, rootMargin = '0px', threshold = 0 } = {}): Intersect => {
|
||||
const useIntersect = (
|
||||
{ root = null, rootMargin = '0px', threshold = 0 } = {},
|
||||
disable?: boolean,
|
||||
): Intersect => {
|
||||
const [entry, updateEntry] = useState<IntersectionObserverEntry>()
|
||||
const [node, setNode] = useState(null)
|
||||
|
||||
@@ -16,13 +19,16 @@ const useIntersect = ({ root = null, rootMargin = '0px', threshold = 0 } = {}):
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (disable) {
|
||||
return
|
||||
}
|
||||
const { current: currentObserver } = observer
|
||||
currentObserver.disconnect()
|
||||
|
||||
if (node) currentObserver.observe(node)
|
||||
|
||||
return () => currentObserver.disconnect()
|
||||
}, [node])
|
||||
}, [node, disable])
|
||||
|
||||
return [setNode, entry]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.13",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -163,6 +163,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
path: createNestedFieldPath(null, field),
|
||||
}))}
|
||||
fieldTypes={field.fieldTypes}
|
||||
forceRender
|
||||
margins="small"
|
||||
permissions={field.permissions?.blocks?.[fields?.data?.blockType]?.fields}
|
||||
readOnly={field.admin.readOnly}
|
||||
|
||||
@@ -81,8 +81,11 @@ ${packagesToRelease
|
||||
|
||||
try {
|
||||
console.log(chalk.bold(`\n\nPublishing ${shortName}...\n\n`))
|
||||
|
||||
execSync(`npm --no-git-tag-version --prefix ${packagePath} version ${bump}`, execOpts)
|
||||
let npmVersionCmd = `npm --no-git-tag-version --prefix ${packagePath} version ${bump}`
|
||||
if (tag !== 'latest') {
|
||||
npmVersionCmd += ` --preid ${tag}`
|
||||
}
|
||||
execSync(npmVersionCmd, execOpts)
|
||||
execSync(`git add ${packagePath}/package.json`, execOpts)
|
||||
|
||||
const packageObj = await fse.readJson(`${packagePath}/package.json`)
|
||||
@@ -91,7 +94,13 @@ ${packagesToRelease
|
||||
const tagName = `${shortName}/${newVersion}`
|
||||
execSync(`git commit -m "chore(release): ${tagName}"`, execOpts)
|
||||
execSync(`git tag -a ${tagName} -m "${tagName}"`, execOpts)
|
||||
execSync(`pnpm publish -C ${packagePath} --no-git-checks`, execOpts)
|
||||
|
||||
let publishCmd = `pnpm publish -C ${packagePath} --no-git-checks`
|
||||
if (tag !== 'latest') {
|
||||
publishCmd += ` --tag ${tag}`
|
||||
}
|
||||
execSync(publishCmd, execOpts)
|
||||
|
||||
results.push({ name: shortName, success: true })
|
||||
} catch (error) {
|
||||
console.error(chalk.bold.red(`ERROR: ${error.message}`))
|
||||
|
||||
@@ -9,7 +9,9 @@ const adminsAndUser: Access = ({ req: { user } }) => {
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
id: {
|
||||
equals: user.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ const adminsAndUser: Access = ({ req: { user } }) => {
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
id: {
|
||||
equals: user.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
10
test/live-preview/collections/Users.ts
Normal file
10
test/live-preview/collections/Users.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [],
|
||||
}
|
||||
@@ -1,20 +1,12 @@
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
|
||||
import { devUser } from '../credentials'
|
||||
import Categories from './collections/Categories'
|
||||
import { Media } from './collections/Media'
|
||||
import { Pages } from './collections/Pages'
|
||||
import { Posts, postsSlug } from './collections/Posts'
|
||||
import { Posts } from './collections/Posts'
|
||||
import { Users } from './collections/Users'
|
||||
import { Footer } from './globals/Footer'
|
||||
import { Header } from './globals/Header'
|
||||
import { footer } from './seed/footer'
|
||||
import { header } from './seed/header'
|
||||
import { home } from './seed/home'
|
||||
import { post1 } from './seed/post-1'
|
||||
import { post2 } from './seed/post-2'
|
||||
import { post3 } from './seed/post-3'
|
||||
import { postsPage } from './seed/posts-page'
|
||||
import { seed } from './seed'
|
||||
|
||||
export const pagesSlug = 'pages'
|
||||
|
||||
@@ -40,92 +32,7 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
cors: ['http://localhost:3001'],
|
||||
csrf: ['http://localhost:3001'],
|
||||
collections: [
|
||||
{
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [],
|
||||
},
|
||||
Pages,
|
||||
Posts,
|
||||
Categories,
|
||||
Media,
|
||||
],
|
||||
collections: [Users, Pages, Posts, Categories, Media],
|
||||
globals: [Header, Footer],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
const media = await payload.create({
|
||||
collection: 'media',
|
||||
filePath: path.resolve(__dirname, 'image-1.jpg'),
|
||||
data: {
|
||||
alt: 'Image 1',
|
||||
},
|
||||
})
|
||||
|
||||
const mediaID = payload.db.defaultIDType === 'number' ? media.id : `"${media.id}"`
|
||||
|
||||
const [post1Doc, post2Doc, post3Doc] = await Promise.all([
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: JSON.parse(JSON.stringify(post1).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
|
||||
}),
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: JSON.parse(JSON.stringify(post2).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
|
||||
}),
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: JSON.parse(JSON.stringify(post3).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
|
||||
}),
|
||||
])
|
||||
|
||||
const postsPageDoc = await payload.create({
|
||||
collection: pagesSlug,
|
||||
data: JSON.parse(JSON.stringify(postsPage).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
|
||||
})
|
||||
|
||||
let postsPageDocID = postsPageDoc.id
|
||||
let post1DocID = post1Doc.id
|
||||
let post2DocID = post2Doc.id
|
||||
let post3DocID = post3Doc.id
|
||||
|
||||
if (payload.db.defaultIDType !== 'number') {
|
||||
postsPageDocID = `"${postsPageDoc.id}"`
|
||||
post1DocID = `"${post1Doc.id}"`
|
||||
post2DocID = `"${post2Doc.id}"`
|
||||
post3DocID = `"${post3Doc.id}"`
|
||||
}
|
||||
|
||||
await payload.create({
|
||||
collection: pagesSlug,
|
||||
data: JSON.parse(
|
||||
JSON.stringify(home)
|
||||
.replace(/"\{\{MEDIA_ID\}\}"/g, mediaID)
|
||||
.replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)
|
||||
.replace(/"\{\{POST_1_ID\}\}"/g, post1DocID)
|
||||
.replace(/"\{\{POST_2_ID\}\}"/g, post2DocID)
|
||||
.replace(/"\{\{POST_3_ID\}\}"/g, post3DocID),
|
||||
),
|
||||
})
|
||||
|
||||
await payload.updateGlobal({
|
||||
slug: 'header',
|
||||
data: JSON.parse(JSON.stringify(header).replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)),
|
||||
})
|
||||
|
||||
await payload.updateGlobal({
|
||||
slug: 'footer',
|
||||
data: JSON.parse(JSON.stringify(footer)),
|
||||
})
|
||||
},
|
||||
onInit: seed,
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
88
test/live-preview/seed/index.ts
Normal file
88
test/live-preview/seed/index.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import path from 'path'
|
||||
|
||||
import type { Config } from '../../../packages/payload/src/config/types'
|
||||
|
||||
import { devUser } from '../../credentials'
|
||||
import { postsSlug } from '../collections/Posts'
|
||||
import { pagesSlug } from '../config'
|
||||
import { footer } from './footer'
|
||||
import { header } from './header'
|
||||
import { home } from './home'
|
||||
import { post1 } from './post-1'
|
||||
import { post2 } from './post-2'
|
||||
import { post3 } from './post-3'
|
||||
import { postsPage } from './posts-page'
|
||||
|
||||
export const seed: Config['onInit'] = async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
const media = await payload.create({
|
||||
collection: 'media',
|
||||
filePath: path.resolve(__dirname, 'image-1.jpg'),
|
||||
data: {
|
||||
alt: 'Image 1',
|
||||
},
|
||||
})
|
||||
|
||||
const mediaID = payload.db.defaultIDType === 'number' ? media.id : `"${media.id}"`
|
||||
|
||||
const [post1Doc, post2Doc, post3Doc] = await Promise.all([
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: JSON.parse(JSON.stringify(post1).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
|
||||
}),
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: JSON.parse(JSON.stringify(post2).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
|
||||
}),
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: JSON.parse(JSON.stringify(post3).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
|
||||
}),
|
||||
])
|
||||
|
||||
const postsPageDoc = await payload.create({
|
||||
collection: pagesSlug,
|
||||
data: JSON.parse(JSON.stringify(postsPage).replace(/"\{\{IMAGE\}\}"/g, mediaID)),
|
||||
})
|
||||
|
||||
let postsPageDocID = postsPageDoc.id
|
||||
let post1DocID = post1Doc.id
|
||||
let post2DocID = post2Doc.id
|
||||
let post3DocID = post3Doc.id
|
||||
|
||||
if (payload.db.defaultIDType !== 'number') {
|
||||
postsPageDocID = `"${postsPageDoc.id}"`
|
||||
post1DocID = `"${post1Doc.id}"`
|
||||
post2DocID = `"${post2Doc.id}"`
|
||||
post3DocID = `"${post3Doc.id}"`
|
||||
}
|
||||
|
||||
await payload.create({
|
||||
collection: pagesSlug,
|
||||
data: JSON.parse(
|
||||
JSON.stringify(home)
|
||||
.replace(/"\{\{MEDIA_ID\}\}"/g, mediaID)
|
||||
.replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)
|
||||
.replace(/"\{\{POST_1_ID\}\}"/g, post1DocID)
|
||||
.replace(/"\{\{POST_2_ID\}\}"/g, post2DocID)
|
||||
.replace(/"\{\{POST_3_ID\}\}"/g, post3DocID),
|
||||
),
|
||||
})
|
||||
|
||||
await payload.updateGlobal({
|
||||
slug: 'header',
|
||||
data: JSON.parse(JSON.stringify(header).replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)),
|
||||
})
|
||||
|
||||
await payload.updateGlobal({
|
||||
slug: 'footer',
|
||||
data: JSON.parse(JSON.stringify(footer)),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user