Compare commits

..

6 Commits

Author SHA1 Message Date
Elliot DeNolf
8b95218577 chore(release): v3.0.0-beta.43 [skip ci] 2024-06-07 17:45:28 -04:00
Jarrod Flesch
a79d23c631 chore: adjusts test config for draft validation (#6678) 2024-06-07 16:01:03 -04:00
Jarrod Flesch
52c81ad525 feat: adds draft validation option (#6677)
## Description

Allows draft validation to be enabled at the config level.

You can enable this by:
```ts
// ...collectionConfig
versions: {
  drafts: {
    validate: true // defaults to false
  }
}
```
2024-06-07 15:22:03 -04:00
Paul
8ec836737e chore: add turbo resolveAlias mock alias to hide webpack warnings (#6676) 2024-06-07 17:23:35 +00:00
Paul
e4a90294ea feat(plugin-redirects)!: update fields overrides to use a function (#6675)
## Description

Updates the `fields` override in plugin redirects to allow for
overriding

```ts
// before
overrides: {
  fields: [
    {
      type: 'text',
      name: 'customField',
    },
  ],
},

// current
overrides: {
  fields: ({ defaultFields }) => {
    return [
      ...defaultFields,
      {
        type: 'text',
        name: 'customField',
      },
    ]
  },
},
```


## Type of change

- [x] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
2024-06-07 14:41:09 +00:00
Jacob Fletcher
7c8d562f03 fix(next): live preview device position when using zoom (#6665) 2024-06-07 10:17:49 -04:00
64 changed files with 628 additions and 282 deletions

7
.vscode/launch.json vendored
View File

@@ -111,6 +111,13 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js field-error-states",
"cwd": "${workspaceFolder}",
"name": "Run Dev Field Error States",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run test:int live-preview",
"cwd": "${workspaceFolder}",

View File

@@ -22,6 +22,7 @@ Collections and Globals both support the same options for configuring drafts. Yo
| Draft Option | Description |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `autosave` | Enable `autosave` to automatically save progress while documents are edited. To enable, set to `true` or pass an object with [options](/docs/versions/autosave). |
| `validate` | Set `validate` to `true` to validate draft documents when saved. Default is `false`. |
## Database changes

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "The official live preview React SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -151,8 +151,10 @@ export const Document: React.FC<AdminViewProps> = async ({
hasSavePermission &&
((collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave))
const validateDraftData =
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate
if (shouldAutosave && !id && collectionSlug) {
if (shouldAutosave && !validateDraftData && !id && collectionSlug) {
const doc = await payload.create({
collection: collectionSlug,
data: {},

View File

@@ -10,16 +10,20 @@ export const DeviceContainer: React.FC<{
const { children } = props
const deviceFrameRef = React.useRef<HTMLDivElement>(null)
const outerFrameRef = React.useRef<HTMLDivElement>(null)
const { breakpoint, setMeasuredDeviceSize, size, zoom } = useLivePreviewContext()
const { breakpoint, setMeasuredDeviceSize, size: desiredSize, zoom } = useLivePreviewContext()
// Keep an accurate measurement of the actual device size as it is truly rendered
// This is helpful when `sizes` are non-number units like percentages, etc.
const { size: measuredDeviceSize } = useResize(deviceFrameRef)
const { size: measuredDeviceSize } = useResize(deviceFrameRef.current)
const { size: outerFrameSize } = useResize(outerFrameRef.current)
let deviceIsLargerThanFrame: boolean = false
// Sync the measured device size with the context so that other components can use it
// This happens from the bottom up so that as this component mounts and unmounts,
// Its size is freshly populated again upon re-mounting, i.e. going from iframe->popup->iframe
// its size is freshly populated again upon re-mounting, i.e. going from iframe->popup->iframe
useEffect(() => {
if (measuredDeviceSize) {
setMeasuredDeviceSize(measuredDeviceSize)
@@ -34,13 +38,34 @@ export const DeviceContainer: React.FC<{
if (
typeof zoom === 'number' &&
typeof size.width === 'number' &&
typeof size.height === 'number'
typeof desiredSize.width === 'number' &&
typeof desiredSize.height === 'number' &&
typeof measuredDeviceSize.width === 'number' &&
typeof measuredDeviceSize.height === 'number'
) {
const scaledWidth = size.width / zoom
const difference = scaledWidth - size.width
x = `${difference / 2}px`
margin = '0 auto'
const scaledDesiredWidth = desiredSize.width / zoom
const scaledDeviceWidth = measuredDeviceSize.width * zoom
const scaledDeviceDifferencePixels = scaledDesiredWidth - desiredSize.width
deviceIsLargerThanFrame = scaledDeviceWidth > outerFrameSize.width
if (deviceIsLargerThanFrame) {
if (zoom > 1) {
const differenceFromDeviceToFrame = measuredDeviceSize.width - outerFrameSize.width
if (differenceFromDeviceToFrame < 0) x = `${differenceFromDeviceToFrame / 2}px`
else x = '0'
} else {
x = '0'
}
} else {
if (zoom >= 1) {
x = `${scaledDeviceDifferencePixels / 2}px`
} else {
const differenceFromDeviceToFrame = outerFrameSize.width - scaledDeviceWidth
x = `${differenceFromDeviceToFrame / 2}px`
margin = '0'
}
}
}
}
@@ -48,21 +73,29 @@ export const DeviceContainer: React.FC<{
let height = zoom ? `${100 / zoom}%` : '100%'
if (breakpoint !== 'responsive') {
width = `${size?.width / (typeof zoom === 'number' ? zoom : 1)}px`
height = `${size?.height / (typeof zoom === 'number' ? zoom : 1)}px`
width = `${desiredSize?.width / (typeof zoom === 'number' ? zoom : 1)}px`
height = `${desiredSize?.height / (typeof zoom === 'number' ? zoom : 1)}px`
}
return (
<div
ref={deviceFrameRef}
ref={outerFrameRef}
style={{
height,
margin,
transform: `translate3d(${x}, 0, 0)`,
width,
height: '100%',
width: '100%',
}}
>
{children}
<div
ref={deviceFrameRef}
style={{
height,
margin,
transform: `translate3d(${x}, 0, 0)`,
width,
}}
>
{children}
</div>
</div>
)
}

View File

@@ -28,6 +28,13 @@ export const withPayload = (nextConfig = {}) => {
'libsql',
],
},
turbo: {
...(nextConfig?.experimental?.turbo || {}),
resolveAlias: {
...(nextConfig?.experimental?.turbo?.resolveAlias || {}),
'payload-mock-package': 'payload-mock-package',
},
},
},
headers: async () => {
const headersFromConfig = 'headers' in nextConfig ? await nextConfig.headers() : []

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",

View File

@@ -84,6 +84,7 @@ export const sanitizeCollection = async (
if (sanitized.versions.drafts === true) {
sanitized.versions.drafts = {
autosave: false,
validate: false,
}
}
@@ -93,6 +94,10 @@ export const sanitizeCollection = async (
}
}
if (sanitized.versions.drafts.validate === undefined) {
sanitized.versions.drafts.validate = false
}
sanitized.fields = mergeBaseFields(sanitized.fields, baseVersionFields)
}
}

View File

@@ -221,6 +221,7 @@ const collectionSchema = joi.object().keys({
interval: joi.number(),
}),
),
validate: joi.boolean(),
}),
joi.boolean(),
),

View File

@@ -165,14 +165,6 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
Promise.resolve(),
)
// /////////////////////////////////////
// Write files to local storage
// /////////////////////////////////////
// if (!collectionConfig.upload.disableLocalStorage) {
// await uploadFiles(payload, filesToUpload, req.t)
// }
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
@@ -203,7 +195,10 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
global: null,
operation: 'create',
req,
skipValidation: shouldSaveDraft,
skipValidation:
shouldSaveDraft &&
collectionConfig.versions.drafts &&
!collectionConfig.versions.drafts.validate,
})
// /////////////////////////////////////

View File

@@ -205,7 +205,10 @@ export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['col
global: null,
operation,
req,
skipValidation: shouldSaveDraft,
skipValidation:
shouldSaveDraft &&
collectionConfig.versions.drafts &&
!collectionConfig.versions.drafts.validate,
})
// set req.locale back to the original locale

View File

@@ -270,7 +270,10 @@ export const updateOperation = async <TSlug extends keyof GeneratedTypes['collec
operation: 'update',
req,
skipValidation:
Boolean(collectionConfig.versions?.drafts) && data._status !== 'published',
shouldSaveDraft &&
collectionConfig.versions.drafts &&
!collectionConfig.versions.drafts.validate &&
data._status !== 'published',
})
// /////////////////////////////////////

View File

@@ -242,7 +242,11 @@ export const updateByIDOperation = async <TSlug extends keyof GeneratedTypes['co
global: null,
operation: 'update',
req,
skipValidation: Boolean(collectionConfig.versions?.drafts) && data._status !== 'published',
skipValidation:
shouldSaveDraft &&
collectionConfig.versions.drafts &&
!collectionConfig.versions.drafts.validate &&
data._status !== 'published',
})
// /////////////////////////////////////

View File

@@ -4,10 +4,12 @@ export type Autosave = {
export type IncomingDrafts = {
autosave?: Autosave | boolean
validate?: boolean
}
export type SanitizedDrafts = {
autosave: Autosave | false
validate: boolean
}
export type IncomingCollectionVersions = {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud-storage",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "The official cloud storage plugin for Payload CMS",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "The official Nested Docs plugin for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-redirects",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,34 +0,0 @@
// @ts-nocheck
/**
* Simple object check.
* @param item
* @returns {boolean}
*/
export function isObject(item: unknown): boolean {
return item && typeof item === 'object' && !Array.isArray(item)
}
/**
* Deep merge two objects.
* @param target
* @param ...sources
*/
export default function deepMerge<T, R>(target: T, source: R): T {
const output = { ...target }
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach((key) => {
if (isObject(source[key])) {
if (!(key in target)) {
Object.assign(output, { [key]: source[key] })
} else {
output[key] = deepMerge(target[key], source[key])
}
} else {
Object.assign(output, { [key]: source[key] })
}
})
}
return output
}

View File

@@ -1,80 +1,85 @@
import type { Config } from 'payload/config'
import type { CollectionConfig, Field } from 'payload/types'
import type { RedirectsPluginConfig } from './types.js'
import deepMerge from './deepMerge.js'
export const redirectsPlugin =
(pluginConfig: RedirectsPluginConfig) =>
(incomingConfig: Config): Config => ({
...incomingConfig,
collections: [
...(incomingConfig?.collections || []),
deepMerge(
{
slug: 'redirects',
access: {
read: (): boolean => true,
},
admin: {
defaultColumns: ['from', 'to.type', 'createdAt'],
},
fields: [
{
name: 'from',
type: 'text',
index: true,
label: 'From URL',
required: true,
(incomingConfig: Config): Config => {
const defaultFields: Field[] = [
{
name: 'from',
type: 'text',
index: true,
label: 'From URL',
required: true,
},
{
name: 'to',
type: 'group',
fields: [
{
name: 'type',
type: 'radio',
admin: {
layout: 'horizontal',
},
{
name: 'to',
type: 'group',
fields: [
{
name: 'type',
type: 'radio',
admin: {
layout: 'horizontal',
},
defaultValue: 'reference',
label: 'To URL Type',
options: [
{
label: 'Internal link',
value: 'reference',
},
{
label: 'Custom URL',
value: 'custom',
},
],
},
{
name: 'reference',
type: 'relationship',
admin: {
condition: (_, siblingData) => siblingData?.type === 'reference',
},
label: 'Document to redirect to',
relationTo: pluginConfig?.collections || [],
required: true,
},
{
name: 'url',
type: 'text',
admin: {
condition: (_, siblingData) => siblingData?.type === 'custom',
},
label: 'Custom URL',
required: true,
},
],
label: false,
defaultValue: 'reference',
label: 'To URL Type',
options: [
{
label: 'Internal link',
value: 'reference',
},
{
label: 'Custom URL',
value: 'custom',
},
],
},
{
name: 'reference',
type: 'relationship',
admin: {
condition: (_, siblingData) => siblingData?.type === 'reference',
},
],
},
pluginConfig?.overrides || {},
),
],
})
label: 'Document to redirect to',
relationTo: pluginConfig?.collections || [],
required: true,
},
{
name: 'url',
type: 'text',
admin: {
condition: (_, siblingData) => siblingData?.type === 'custom',
},
label: 'Custom URL',
required: true,
},
],
label: false,
},
]
const redirectsCollection: CollectionConfig = {
...(pluginConfig?.overrides || {}),
slug: pluginConfig?.overrides?.slug || 'redirects',
access: {
read: () => true,
...(pluginConfig?.overrides?.access || {}),
},
admin: {
defaultColumns: ['from', 'to.type', 'createdAt'],
...(pluginConfig?.overrides?.admin || {}),
},
fields:
pluginConfig?.overrides?.fields && typeof pluginConfig?.overrides?.fields === 'function'
? pluginConfig?.overrides.fields({ defaultFields })
: defaultFields,
}
return {
...incomingConfig,
collections: [...(incomingConfig?.collections || []), redirectsCollection],
}
}

View File

@@ -1,6 +1,8 @@
import type { CollectionConfig } from 'payload/types'
import type { CollectionConfig, Field } from 'payload/types'
export type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]
export type RedirectsPluginConfig = {
collections?: string[]
overrides?: Partial<CollectionConfig>
overrides?: Partial<Omit<CollectionConfig, 'fields'>> & { fields: FieldsOverride }
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-relationship-object-ids",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "A Payload plugin to store all relationship IDs as ObjectIDs",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Search plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "SEO plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-stripe",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Stripe plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "The officially supported Lexical richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "The officially supported Slate richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-azure",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Payload storage adapter for Azure Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-gcs",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Payload storage adapter for Google Cloud Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-s3",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Payload storage adapter for Amazon S3",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-uploadthing",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Payload storage adapter for uploadthing",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-vercel-blob",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"description": "Payload storage adapter for Vercel Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/translations",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/ui",
"version": "3.0.0-beta.42",
"version": "3.0.0-beta.43",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -3,9 +3,9 @@
import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload/types'
import React, { useEffect, useRef, useState } from 'react'
import { toast } from 'react-toastify'
import { useAllFormFields, useFormModified } from '../../forms/Form/context.js'
import { useDebounce } from '../../hooks/useDebounce.js'
import { useAllFormFields, useForm, useFormModified } from '../../forms/Form/context.js'
import { useConfig } from '../../providers/Config/index.js'
import { useDocumentEvents } from '../../providers/DocumentEvents/index.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
@@ -36,6 +36,7 @@ export const Autosave: React.FC<Props> = ({
} = useConfig()
const { docConfig, getVersions, versions } = useDocumentInfo()
const { reportUpdate } = useDocumentEvents()
const { dispatchFields, setSubmitted } = useForm()
const versionsConfig = docConfig?.versions
const [fields] = useAllFormFields()
@@ -49,7 +50,6 @@ export const Autosave: React.FC<Props> = ({
const [saving, setSaving] = useState(false)
const [lastSaved, setLastSaved] = useState<number>()
const debouncedFields = useDebounce(fields, interval)
const fieldRef = useRef(fields)
const modifiedRef = useRef(modified)
const localeRef = useRef(locale)
@@ -117,26 +117,77 @@ export const Autosave: React.FC<Props> = ({
})
void getVersions()
}
if (
versionsConfig?.drafts &&
versionsConfig?.drafts?.validate &&
res.status === 400
) {
const json = await res.json()
if (Array.isArray(json.errors)) {
const [fieldErrors, nonFieldErrors] = json.errors.reduce(
([fieldErrs, nonFieldErrs], err) => {
const newFieldErrs = []
const newNonFieldErrs = []
if (err?.message) {
newNonFieldErrs.push(err)
}
if (Array.isArray(err?.data)) {
err.data.forEach((dataError) => {
if (dataError?.field) {
newFieldErrs.push(dataError)
} else {
newNonFieldErrs.push(dataError)
}
})
}
return [
[...fieldErrs, ...newFieldErrs],
[...nonFieldErrs, ...newNonFieldErrs],
]
},
[[], []],
)
dispatchFields({
type: 'ADD_SERVER_ERRORS',
errors: fieldErrors,
})
nonFieldErrors.forEach((err) => {
toast.error(err.message || i18n.t('error:unknown'))
})
return
}
setSubmitted(true)
}
}
setSaving(false)
}, 1000)
}, interval)
}
}
}
void autosave()
}, [
i18n,
debouncedFields,
modified,
serverURL,
api,
collection,
globalDoc,
reportUpdate,
id,
dispatchFields,
getVersions,
globalDoc,
i18n,
id,
interval,
modified,
reportUpdate,
serverURL,
setSubmitted,
versionsConfig?.drafts,
])
useEffect(() => {

View File

@@ -74,6 +74,9 @@ export const DocumentControls: React.FC<{
collectionConfig && id && !disableActions && (hasCreatePermission || hasDeletePermission),
)
const unsavedDraftWithValidations =
!id && collectionConfig?.versions?.drafts && collectionConfig.versions?.drafts.validate
return (
<Gutter className={baseClass}>
<div className={`${baseClass}__wrapper`}>
@@ -103,7 +106,8 @@ export const DocumentControls: React.FC<{
</li>
)}
{((collectionConfig?.versions?.drafts &&
collectionConfig?.versions?.drafts?.autosave) ||
collectionConfig?.versions?.drafts?.autosave &&
!unsavedDraftWithValidations) ||
(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave)) &&
hasSavePermission && (
<li className={`${baseClass}__list-item`}>
@@ -168,6 +172,7 @@ export const DocumentControls: React.FC<{
<React.Fragment>
{((collectionConfig?.versions?.drafts &&
!collectionConfig?.versions?.drafts?.autosave) ||
unsavedDraftWithValidations ||
(globalConfig?.versions?.drafts &&
!globalConfig?.versions?.drafts?.autosave)) && (
<SaveDraftButton CustomComponent={componentMap.SaveDraftButton} />

View File

@@ -2,7 +2,11 @@
/* eslint-disable no-shadow */
import { useEffect, useRef, useState } from 'react'
type Intersect = [setNode: React.Dispatch<Element>, entry: IntersectionObserverEntry]
type Intersect = [
setNode: React.Dispatch<HTMLElement>,
entry: IntersectionObserverEntry,
node: HTMLElement,
]
export const useIntersect = (
{ root = null, rootMargin = '0px', threshold = 0 } = {},
@@ -33,5 +37,5 @@ export const useIntersect = (
return () => currentObserver.disconnect()
}, [node, disable])
return [setNode, entry]
return [setNode, entry, node]
}

View File

@@ -12,15 +12,13 @@ interface Resize {
size?: Size
}
export const useResize = (ref: React.MutableRefObject<HTMLElement>): Resize => {
export const useResize = (element: HTMLElement): Resize => {
const [size, setSize] = useState<Size>()
useEffect(() => {
let observer: any // eslint-disable-line
const { current: currentRef } = ref
if (currentRef) {
if (element) {
observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const {
@@ -53,15 +51,15 @@ export const useResize = (ref: React.MutableRefObject<HTMLElement>): Resize => {
})
})
observer.observe(currentRef)
observer.observe(element)
}
return () => {
if (observer) {
observer.unobserve(currentRef)
observer.unobserve(element)
}
}
}, [ref])
}, [element])
return {
size,

View File

@@ -0,0 +1,12 @@
import type { CollectionConfig } from 'payload/types'
import { slugs } from '../../shared.js'
import { ValidateDraftsOn } from '../ValidateDraftsOn/index.js'
export const ValidateDraftsOff: CollectionConfig = {
...ValidateDraftsOn,
slug: slugs.validateDraftsOff,
versions: {
drafts: true,
},
}

View File

@@ -0,0 +1,19 @@
import type { CollectionConfig } from 'payload/types'
import { slugs } from '../../shared.js'
export const ValidateDraftsOn: CollectionConfig = {
slug: slugs.validateDraftsOn,
fields: [
{
name: 'title',
type: 'text',
required: true,
},
],
versions: {
drafts: {
validate: true,
},
},
}

View File

@@ -0,0 +1,15 @@
import type { CollectionConfig } from 'payload/types'
import { slugs } from '../../shared.js'
import { ValidateDraftsOn } from '../ValidateDraftsOn/index.js'
export const ValidateDraftsOnAndAutosave: CollectionConfig = {
...ValidateDraftsOn,
slug: slugs.validateDraftsOnAutosave,
versions: {
drafts: {
autosave: true,
validate: true,
},
},
}

View File

@@ -2,9 +2,18 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { ErrorFieldsCollection } from './collections/ErrorFields/index.js'
import Uploads from './collections/Upload/index.js'
import { ValidateDraftsOff } from './collections/ValidateDraftsOff/index.js'
import { ValidateDraftsOn } from './collections/ValidateDraftsOn/index.js'
import { ValidateDraftsOnAndAutosave } from './collections/ValidateDraftsOnAutosave/index.js'
export default buildConfigWithDefaults({
collections: [ErrorFieldsCollection, Uploads],
collections: [
ErrorFieldsCollection,
Uploads,
ValidateDraftsOn,
ValidateDraftsOff,
ValidateDraftsOnAndAutosave,
],
onInit: async (payload) => {
await payload.create({
collection: 'users',

View File

@@ -1,12 +1,18 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { AdminUrlUtil } from 'helpers/adminUrlUtil.js'
import path from 'path'
import { fileURLToPath } from 'url'
import { ensureAutoLoginAndCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js'
import {
ensureAutoLoginAndCompilationIsDone,
initPageConsoleErrorCatch,
saveDocAndAssert,
} from '../helpers.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
import { slugs } from './shared.js'
const { beforeAll, describe } = test
const filename = fileURLToPath(import.meta.url)
@@ -15,10 +21,16 @@ const dirname = path.dirname(filename)
describe('field error states', () => {
let serverURL: string
let page: Page
let validateDraftsOff: AdminUrlUtil
let validateDraftsOn: AdminUrlUtil
let validateDraftsOnAutosave: AdminUrlUtil
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
;({ serverURL } = await initPayloadE2ENoConfig({ dirname }))
validateDraftsOff = new AdminUrlUtil(serverURL, slugs.validateDraftsOff)
validateDraftsOn = new AdminUrlUtil(serverURL, slugs.validateDraftsOn)
validateDraftsOnAutosave = new AdminUrlUtil(serverURL, slugs.validateDraftsOnAutosave)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
@@ -57,4 +69,27 @@ describe('field error states', () => {
)
expect(errorPill).toBeNull()
})
describe('draft validations', () => {
// eslint-disable-next-line playwright/expect-expect
test('should not validate drafts by default', async () => {
await page.goto(validateDraftsOff.create)
await saveDocAndAssert(page, '#action-save-draft')
})
// eslint-disable-next-line playwright/expect-expect
test('should validate drafts when enabled', async () => {
await page.goto(validateDraftsOn.create)
await saveDocAndAssert(page, '#action-save-draft', 'error')
})
// eslint-disable-next-line playwright/expect-expect
test('should show validation errors when validate and autosave are enabled', async () => {
await page.goto(validateDraftsOnAutosave.create)
await page.locator('#field-title').fill('valid')
await saveDocAndAssert(page)
await page.locator('#field-title').fill('')
await saveDocAndAssert(page, '#action-save', 'error')
})
})
})

View File

@@ -10,6 +10,9 @@ export interface Config {
collections: {
'error-fields': ErrorField;
uploads: Upload;
'validate-drafts-on': ValidateDraftsOn;
'validate-drafts-off': ValidateDraftsOff;
'validate-drafts-on-autosave': ValidateDraftsOnAutosave;
users: User;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -255,6 +258,41 @@ export interface Upload {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "validate-drafts-on".
*/
export interface ValidateDraftsOn {
id: string;
title: string;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "validate-drafts-off".
*/
export interface ValidateDraftsOff {
id: string;
title: string;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "validate-drafts-on-autosave".
*/
export interface ValidateDraftsOnAutosave {
id: string;
title: string;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema

View File

@@ -0,0 +1,5 @@
export const slugs = {
validateDraftsOn: 'validate-drafts-on',
validateDraftsOnAutosave: 'validate-drafts-on-autosave',
validateDraftsOff: 'validate-drafts-off',
}

View File

@@ -180,11 +180,20 @@ export async function saveDocHotkeyAndAssert(page: Page): Promise<void> {
await expect(page.locator('.Toastify')).toContainText('successfully')
}
export async function saveDocAndAssert(page: Page, selector = '#action-save'): Promise<void> {
export async function saveDocAndAssert(
page: Page,
selector = '#action-save',
expectation: 'error' | 'success' = 'success',
): Promise<void> {
await wait(500) // TODO: Fix this
await page.click(selector, { delay: 100 })
await expect(page.locator('.Toastify')).toContainText('successfully')
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain('create')
if (expectation === 'success') {
await expect(page.locator('.Toastify')).toContainText('successfully')
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain('create')
} else {
await expect(page.locator('.Toastify .Toastify__toast--error')).toBeVisible()
}
}
export async function openNav(page: Page): Promise<void> {

View File

@@ -11,6 +11,7 @@ import { Footer } from './globals/Footer.js'
import { Header } from './globals/Header.js'
import { seed } from './seed/index.js'
import {
desktopBreakpoint,
mobileBreakpoint,
pagesSlug,
postsSlug,
@@ -25,7 +26,7 @@ export default buildConfigWithDefaults({
// You can define any of these properties on a per collection or global basis
// The Live Preview config cascades from the top down, properties are inherited from here
url: formatLivePreviewURL,
breakpoints: [mobileBreakpoint],
breakpoints: [mobileBreakpoint, desktopBreakpoint],
collections: [pagesSlug, postsSlug, ssrPagesSlug, ssrAutosavePagesSlug],
globals: ['header', 'footer'],
},

View File

@@ -8,19 +8,29 @@ import {
ensureAutoLoginAndCompilationIsDone,
exactText,
initPageConsoleErrorCatch,
navigateToListCellLink,
saveDocAndAssert,
} from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
import {
ensureDeviceIsCentered,
ensureDeviceIsLeftAligned,
goToCollectionLivePreview,
goToDoc,
goToGlobalLivePreview,
selectLivePreviewBreakpoint,
selectLivePreviewZoom,
} from './helpers.js'
import {
desktopBreakpoint,
mobileBreakpoint,
pagesSlug,
renderedPageTitleID,
ssrAutosavePagesSlug,
ssrPagesSlug,
} from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -34,25 +44,6 @@ describe('Live Preview', () => {
let ssrPagesURLUtil: AdminUrlUtil
let ssrAutosavePostsURLUtil: AdminUrlUtil
const goToDoc = async (page: Page, urlUtil: AdminUrlUtil) => {
await page.goto(urlUtil.list)
await page.waitForURL(urlUtil.list)
await navigateToListCellLink(page)
}
const goToCollectionPreview = async (page: Page, urlUtil: AdminUrlUtil): Promise<void> => {
await goToDoc(page, urlUtil)
await page.goto(`${page.url()}/preview`)
await page.waitForURL(`**/preview`)
}
const goToGlobalPreview = async (page: Page, slug: string): Promise<void> => {
const global = new AdminUrlUtil(serverURL, slug)
const previewURL = `${global.global(slug)}/preview`
await page.goto(previewURL)
await page.waitForURL(previewURL)
}
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
;({ serverURL } = await initPayloadE2ENoConfig({ dirname }))
@@ -88,18 +79,18 @@ describe('Live Preview', () => {
})
test('collection — has route', async () => {
await goToCollectionPreview(page, pagesURLUtil)
await goToCollectionLivePreview(page, pagesURLUtil)
await expect(page.locator('.live-preview')).toBeVisible()
})
test('collection — renders iframe', async () => {
await goToCollectionPreview(page, pagesURLUtil)
await goToCollectionLivePreview(page, pagesURLUtil)
const iframe = page.locator('iframe.live-preview-iframe')
await expect(iframe).toBeVisible()
})
test('collection — re-renders iframe client-side when form state changes', async () => {
await goToCollectionPreview(page, pagesURLUtil)
await goToCollectionLivePreview(page, pagesURLUtil)
const titleField = page.locator('#field-title')
const frame = page.frameLocator('iframe.live-preview-iframe').first()
@@ -129,7 +120,7 @@ describe('Live Preview', () => {
})
test('collection ssr — re-render iframe when save is made', async () => {
await goToCollectionPreview(page, ssrPagesURLUtil)
await goToCollectionLivePreview(page, ssrPagesURLUtil)
const titleField = page.locator('#field-title')
const frame = page.frameLocator('iframe.live-preview-iframe').first()
@@ -159,7 +150,7 @@ describe('Live Preview', () => {
})
test('collection ssr — re-render iframe when autosave is made', async () => {
await goToCollectionPreview(page, ssrAutosavePostsURLUtil)
await goToCollectionLivePreview(page, ssrAutosavePostsURLUtil)
const titleField = page.locator('#field-title')
const frame = page.frameLocator('iframe.live-preview-iframe').first()
@@ -189,12 +180,12 @@ describe('Live Preview', () => {
})
test('collection — should show live-preview view-level action in live-preview view', async () => {
await goToCollectionPreview(page, pagesURLUtil)
await goToCollectionLivePreview(page, pagesURLUtil)
await expect(page.locator('.app-header .collection-live-preview-button')).toHaveCount(1)
})
test('global — should show live-preview view-level action in live-preview view', async () => {
await goToGlobalPreview(page, 'footer')
await goToGlobalLivePreview(page, 'footer', serverURL)
await expect(page.locator('.app-header .global-live-preview-button')).toHaveCount(1)
})
@@ -220,7 +211,7 @@ describe('Live Preview', () => {
test('global — has route', async () => {
const url = page.url()
await goToGlobalPreview(page, 'header')
await goToGlobalLivePreview(page, 'header', serverURL)
await expect(() => expect(page.url()).toBe(`${url}/preview`)).toPass({
timeout: POLL_TOPASS_TIMEOUT,
@@ -228,13 +219,13 @@ describe('Live Preview', () => {
})
test('global — renders iframe', async () => {
await goToGlobalPreview(page, 'header')
await goToGlobalLivePreview(page, 'header', serverURL)
const iframe = page.locator('iframe.live-preview-iframe')
await expect(iframe).toBeVisible()
})
test('global — can edit fields', async () => {
await goToGlobalPreview(page, 'header')
await goToGlobalLivePreview(page, 'header', serverURL)
const field = page.locator('input#field-navItems__0__link__newTab') //field-navItems__0__link__newTab
await expect(field).toBeVisible()
await expect(field).toBeEnabled()
@@ -242,14 +233,14 @@ describe('Live Preview', () => {
await saveDocAndAssert(page)
})
test('properly measures iframe and displays size', async () => {
test('device — properly measures size', async () => {
await page.goto(pagesURLUtil.create)
await page.waitForURL(pagesURLUtil.create)
await page.locator('#field-title').fill('Title 3')
await page.locator('#field-slug').fill('slug-3')
await saveDocAndAssert(page)
await goToCollectionPreview(page, pagesURLUtil)
await goToCollectionLivePreview(page, pagesURLUtil)
const iframe = page.locator('iframe')
@@ -291,37 +282,16 @@ describe('Live Preview', () => {
})
})
test('resizes iframe to specified breakpoint', async () => {
test('device — resizes to specified breakpoint', async () => {
await page.goto(pagesURLUtil.create)
await page.waitForURL(pagesURLUtil.create)
await page.locator('#field-title').fill('Title 4')
await page.locator('#field-slug').fill('slug-4')
await saveDocAndAssert(page)
await goToCollectionPreview(page, pagesURLUtil)
await goToCollectionLivePreview(page, pagesURLUtil)
// Check that the breakpoint select is present
const breakpointSelector = page.locator(
'.live-preview-toolbar-controls__breakpoint button.popup-button',
)
await expect(() => expect(breakpointSelector).toBeTruthy()).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
// Select the mobile breakpoint
await breakpointSelector.first().click()
await page
.locator(`.live-preview-toolbar-controls__breakpoint button.popup-button-list__button`)
.filter({ hasText: mobileBreakpoint.label })
.click()
// Make sure the value has been set
await expect(breakpointSelector).toContainText(mobileBreakpoint.label)
const option = page.locator(
'.live-preview-toolbar-controls__breakpoint button.popup-button-list__button--selected',
)
await expect(option).toHaveText(mobileBreakpoint.label)
await selectLivePreviewBreakpoint(page, mobileBreakpoint.label)
// Measure the size of the iframe against the specified breakpoint
const iframe = page.locator('iframe')
@@ -382,4 +352,34 @@ describe('Live Preview', () => {
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('device — centers device when smaller than frame despite zoom', async () => {
await goToCollectionLivePreview(page, pagesURLUtil)
await selectLivePreviewBreakpoint(page, mobileBreakpoint.label)
await ensureDeviceIsCentered(page)
await selectLivePreviewZoom(page, '75%')
await ensureDeviceIsCentered(page)
await selectLivePreviewZoom(page, '50%')
await ensureDeviceIsCentered(page)
await selectLivePreviewZoom(page, '125%')
await ensureDeviceIsCentered(page)
await selectLivePreviewZoom(page, '200%')
await ensureDeviceIsCentered(page)
expect(true).toBeTruthy()
})
test('device — left-aligns device when larger than frame despite zoom', async () => {
await goToCollectionLivePreview(page, pagesURLUtil)
await selectLivePreviewBreakpoint(page, desktopBreakpoint.label)
await ensureDeviceIsLeftAligned(page)
await selectLivePreviewZoom(page, '75%')
await ensureDeviceIsLeftAligned(page)
await selectLivePreviewZoom(page, '50%')
await ensureDeviceIsLeftAligned(page)
await selectLivePreviewZoom(page, '125%')
await ensureDeviceIsLeftAligned(page)
await selectLivePreviewZoom(page, '200%')
await ensureDeviceIsLeftAligned(page)
expect(true).toBeTruthy()
})
})

View File

@@ -0,0 +1,122 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { exactText, navigateToListCellLink } from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { POLL_TOPASS_TIMEOUT } from '../playwright.config.js'
export const goToDoc = async (page: Page, urlUtil: AdminUrlUtil) => {
await page.goto(urlUtil.list)
await page.waitForURL(urlUtil.list)
await navigateToListCellLink(page)
}
export const goToCollectionLivePreview = async (
page: Page,
urlUtil: AdminUrlUtil,
): Promise<void> => {
await goToDoc(page, urlUtil)
await page.goto(`${page.url()}/preview`)
await page.waitForURL(`**/preview`)
}
export const goToGlobalLivePreview = async (
page: Page,
slug: string,
serverURL: string,
): Promise<void> => {
const global = new AdminUrlUtil(serverURL, slug)
const previewURL = `${global.global(slug)}/preview`
await page.goto(previewURL)
await page.waitForURL(previewURL)
}
export const selectLivePreviewBreakpoint = async (page: Page, breakpointLabel: string) => {
const breakpointSelector = page.locator(
'.live-preview-toolbar-controls__breakpoint button.popup-button',
)
await expect(() => expect(breakpointSelector).toBeTruthy()).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
await breakpointSelector.first().click()
await page
.locator(`.live-preview-toolbar-controls__breakpoint button.popup-button-list__button`)
.filter({ hasText: breakpointLabel })
.click()
await expect(breakpointSelector).toContainText(breakpointLabel)
const option = page.locator(
'.live-preview-toolbar-controls__breakpoint button.popup-button-list__button--selected',
)
await expect(option).toHaveText(breakpointLabel)
}
export const selectLivePreviewZoom = async (page: Page, zoomLabel: string) => {
const zoomSelector = page.locator('.live-preview-toolbar-controls__zoom button.popup-button')
await expect(() => expect(zoomSelector).toBeTruthy()).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
await zoomSelector.first().click()
const zoomOption = page.locator(
'.live-preview-toolbar-controls__zoom button.popup-button-list__button',
{
hasText: exactText(zoomLabel),
},
)
expect(zoomOption).toBeTruthy()
await zoomOption.click()
await expect(zoomSelector).toContainText(zoomLabel)
const option = page.locator(
'.live-preview-toolbar-controls__zoom button.popup-button-list__button--selected',
)
await expect(option).toHaveText(zoomLabel)
}
export const ensureDeviceIsCentered = async (page: Page) => {
const main = page.locator('.live-preview-window__main')
const iframe = page.locator('iframe.live-preview-iframe')
const mainBoxAfterZoom = await main.boundingBox()
const iframeBoxAfterZoom = await iframe.boundingBox()
const distanceFromIframeLeftToMainLeftAfterZoom = Math.abs(
mainBoxAfterZoom?.x - iframeBoxAfterZoom?.x,
)
const distanceFromIFrameRightToMainRightAfterZoom = Math.abs(
mainBoxAfterZoom?.x +
mainBoxAfterZoom?.width -
iframeBoxAfterZoom?.x -
iframeBoxAfterZoom?.width,
)
await expect(() =>
expect(distanceFromIframeLeftToMainLeftAfterZoom).toBe(
distanceFromIFrameRightToMainRightAfterZoom,
),
).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
}
export const ensureDeviceIsLeftAligned = async (page: Page) => {
const main = page.locator('.live-preview-window__main > div')
const iframe = page.locator('iframe.live-preview-iframe')
const mainBoxAfterZoom = await main.boundingBox()
const iframeBoxAfterZoom = await iframe.boundingBox()
const distanceFromIframeLeftToMainLeftAfterZoom = Math.abs(
mainBoxAfterZoom?.x - iframeBoxAfterZoom?.x,
)
await expect(() => expect(distanceFromIframeLeftToMainLeftAfterZoom).toBe(0)).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
}

View File

@@ -15,4 +15,11 @@ export const mobileBreakpoint = {
height: 667,
}
export const desktopBreakpoint = {
label: 'Desktop',
name: 'desktop',
width: 1920,
height: 1080,
}
export const renderedPageTitleID = 'rendered-page-title'

View File

@@ -27,6 +27,17 @@ export default buildConfigWithDefaults({
plugins: [
redirectsPlugin({
collections: ['pages'],
overrides: {
fields: ({ defaultFields }) => {
return [
...defaultFields,
{
type: 'text',
name: 'customField',
},
]
},
},
}),
],
})

View File

@@ -11,10 +11,8 @@ import { Uploads1 } from './collections/Upload1/index.js'
import { Uploads2 } from './collections/Upload2/index.js'
import {
audioSlug,
cropOnlySlug,
enlargeSlug,
focalNoSizesSlug,
globalWithMediaSlug,
mediaSlug,
reduceSlug,
relationSlug,
@@ -134,7 +132,7 @@ export default buildConfigWithDefaults({
},
},
{
slug: cropOnlySlug,
slug: 'crop-only',
fields: [],
upload: {
focalPoint: false,
@@ -464,18 +462,6 @@ export default buildConfigWithDefaults({
},
},
],
globals: [
{
slug: globalWithMediaSlug,
fields: [
{
type: 'upload',
name: 'media',
relationTo: cropOnlySlug,
},
],
},
],
onInit: async (payload) => {
const uploadsDir = path.resolve(dirname, './media')
removeFiles(path.normalize(uploadsDir))

View File

@@ -1,4 +1,5 @@
import type { Page } from '@playwright/test'
import type { Payload } from 'payload/types'
import { expect, test } from '@playwright/test'
import path from 'path'
@@ -22,7 +23,6 @@ import {
adminThumbnailFunctionSlug,
adminThumbnailSizeSlug,
audioSlug,
globalWithMediaSlug,
mediaSlug,
relationSlug,
} from './shared.js'
@@ -39,7 +39,6 @@ let audioURL: AdminUrlUtil
let relationURL: AdminUrlUtil
let adminThumbnailSizeURL: AdminUrlUtil
let adminThumbnailFunctionURL: AdminUrlUtil
let globalWithMediaURL: string
describe('uploads', () => {
let page: Page
@@ -57,7 +56,6 @@ describe('uploads', () => {
relationURL = new AdminUrlUtil(serverURL, relationSlug)
adminThumbnailSizeURL = new AdminUrlUtil(serverURL, adminThumbnailSizeSlug)
adminThumbnailFunctionURL = new AdminUrlUtil(serverURL, adminThumbnailFunctionSlug)
globalWithMediaURL = new AdminUrlUtil(serverURL, globalWithMediaURL).global(globalWithMediaSlug)
const context = await browser.newContext()
page = await context.newPage()
@@ -375,17 +373,4 @@ describe('uploads', () => {
expect(redDoc.filesize).toEqual(1207)
})
})
describe('globals', () => {
test('should be able to crop media from a global', async () => {
await page.goto(globalWithMediaURL)
await page.click('.upload__toggler.doc-drawer__toggler')
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './image.png'))
await page.click('.file-field__edit')
await page.click('.btn.edit-upload__save')
await saveDocAndAssert(page, '.drawer__content #action-save')
await saveDocAndAssert(page)
await expect(page.locator('.thumbnail img')).toBeVisible()
})
})
})

View File

@@ -5,10 +5,8 @@ export const audioSlug = 'audio'
export const enlargeSlug = 'enlarge'
export const focalNoSizesSlug = 'focal-no-sizes'
export const focalOnlySlug = 'focal-only'
export const cropOnlySlug = 'crop-only'
export const reduceSlug = 'reduce'
export const adminThumbnailFunctionSlug = 'admin-thumbnail-function'
export const adminThumbnailSizeSlug = 'admin-thumbnail-size'
export const unstoredMediaSlug = 'unstored-media'
export const versionSlug = 'versions'
export const globalWithMediaSlug = 'globals-with-media'