Compare commits
9 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b95218577 | ||
|
|
a79d23c631 | ||
|
|
52c81ad525 | ||
|
|
8ec836737e | ||
|
|
e4a90294ea | ||
|
|
7c8d562f03 | ||
|
|
11c3a65e63 | ||
|
|
8dd5e4dc24 | ||
|
|
9bd9e7a986 |
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -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}",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.43",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -158,7 +158,7 @@
|
||||
"react-dom": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2",
|
||||
"node": "^18.20.2 || >=20.9.0",
|
||||
"pnpm": "^8.15.7"
|
||||
},
|
||||
"pnpm": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -40,7 +40,7 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
GraphQLUnionType,
|
||||
} from 'graphql'
|
||||
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'
|
||||
import { MissingEditorProp } from 'payload/errors'
|
||||
import { tabHasName } from 'payload/types'
|
||||
import { createDataloaderCacheKey, toWords } from 'payload/utilities'
|
||||
|
||||
@@ -476,6 +477,10 @@ function buildObjectType({
|
||||
async resolve(parent, args, context: Context) {
|
||||
let depth = config.defaultDepth
|
||||
if (typeof args.depth !== 'undefined') depth = args.depth
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
@@ -84,7 +84,7 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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() : []
|
||||
|
||||
@@ -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",
|
||||
@@ -137,7 +137,7 @@
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.6.0"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ const collectionSchema = joi.object().keys({
|
||||
interval: joi.number(),
|
||||
}),
|
||||
),
|
||||
validate: joi.boolean(),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -101,7 +101,7 @@ export default joi.object({
|
||||
defaultMaxTextLength: joi.number(),
|
||||
editor: joi
|
||||
.object()
|
||||
.required()
|
||||
.optional()
|
||||
.keys({
|
||||
CellComponent: componentSchema.optional(),
|
||||
FieldComponent: componentSchema.optional(),
|
||||
|
||||
@@ -615,7 +615,7 @@ export type Config = {
|
||||
*/
|
||||
defaultMaxTextLength?: number
|
||||
/** Default richtext editor to use for richText fields */
|
||||
editor: RichTextAdapterProvider<any, any, any>
|
||||
editor?: RichTextAdapterProvider<any, any, any>
|
||||
/**
|
||||
* Email Adapter
|
||||
*
|
||||
@@ -747,7 +747,7 @@ export type SanitizedConfig = Omit<
|
||||
> & {
|
||||
collections: SanitizedCollectionConfig[]
|
||||
/** Default richtext editor to use for richText fields */
|
||||
editor: RichTextAdapter<any, any, any>
|
||||
editor?: RichTextAdapter<any, any, any>
|
||||
endpoints: Endpoint[]
|
||||
globals: SanitizedGlobalConfig[]
|
||||
i18n: Required<I18nOptions>
|
||||
|
||||
@@ -12,6 +12,7 @@ export { InvalidFieldName } from './InvalidFieldName.js'
|
||||
export { InvalidFieldRelationship } from './InvalidFieldRelationship.js'
|
||||
export { LockedAuth } from './LockedAuth.js'
|
||||
export { MissingCollectionLabel } from './MissingCollectionLabel.js'
|
||||
export { MissingEditorProp } from './MissingEditorProp.js'
|
||||
export { MissingFieldInputOptions } from './MissingFieldInputOptions.js'
|
||||
export { MissingFieldType } from './MissingFieldType.js'
|
||||
export { MissingFile } from './MissingFile.js'
|
||||
|
||||
@@ -2,8 +2,10 @@ export {
|
||||
APIError,
|
||||
AuthenticationError,
|
||||
DuplicateCollection,
|
||||
DuplicateFieldName,
|
||||
DuplicateGlobal,
|
||||
ErrorDeletingFile,
|
||||
FileRetrievalError,
|
||||
FileUploadError,
|
||||
Forbidden,
|
||||
InvalidConfiguration,
|
||||
@@ -11,6 +13,7 @@ export {
|
||||
InvalidFieldRelationship,
|
||||
LockedAuth,
|
||||
MissingCollectionLabel,
|
||||
MissingEditorProp,
|
||||
MissingFieldInputOptions,
|
||||
MissingFieldType,
|
||||
MissingFile,
|
||||
|
||||
@@ -158,7 +158,7 @@ export const sanitizeFields = async ({
|
||||
// config.editor should be sanitized at this point
|
||||
field.editor = _config.editor
|
||||
} else {
|
||||
throw new MissingEditorProp(field)
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import getValueWithDefault from '../../getDefaultValue.js'
|
||||
import { relationshipPopulationPromise } from './relationshipPopulationPromise.js'
|
||||
@@ -143,6 +144,9 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
@@ -268,6 +268,9 @@ export const richText: Validate<object, unknown, unknown, RichTextField> = async
|
||||
value,
|
||||
options,
|
||||
) => {
|
||||
if (!options?.editor) {
|
||||
throw new Error('richText field has no editor property.')
|
||||
}
|
||||
if (typeof options?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { Field, FieldAffectingData, Option } from '../fields/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../errors/MissingEditorProp.js'
|
||||
import { fieldAffectsData, tabHasName } from '../fields/config/types.js'
|
||||
import { deepCopyObject } from './deepCopyObject.js'
|
||||
import { toWords } from './formatLabels.js'
|
||||
@@ -195,6 +196,9 @@ export function fieldsToJSONSchema(
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export const enforceMaxVersions = async ({
|
||||
}
|
||||
|
||||
await payload.db.deleteVersions({
|
||||
collection: collection?.slug,
|
||||
collection: slug,
|
||||
req,
|
||||
where: deleteQuery,
|
||||
})
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
@@ -95,7 +95,7 @@
|
||||
"react-dom": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -55,7 +55,7 @@
|
||||
"react": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -45,7 +45,7 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -43,7 +43,7 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
@@ -153,7 +153,7 @@
|
||||
"react-dom": "^19.0.0 || ^19.0.0-rc-f994737d14-20240522"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.2"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
SanitizedConfig,
|
||||
} from 'payload/types'
|
||||
|
||||
import { MissingEditorProp } from 'payload/errors'
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/types'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
@@ -566,6 +567,9 @@ export const mapFields = (args: {
|
||||
style: field.admin?.style,
|
||||
width: field.admin?.width,
|
||||
}
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { Field, SanitizedConfig } from 'payload/types'
|
||||
|
||||
import { MissingEditorProp } from 'payload/errors'
|
||||
import { tabHasName } from 'payload/types'
|
||||
|
||||
import type { FieldSchemaMap } from './types.js'
|
||||
@@ -68,6 +69,9 @@ export const traverseFields = ({
|
||||
break
|
||||
|
||||
case 'richText':
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.6.0"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.6.0"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"ci": "payload migrate && pnpm build",
|
||||
"dev": "next dev",
|
||||
"generate:types": "payload generate:types",
|
||||
"lint": "next lint",
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"generate:types": "payload generate:types"
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/db-postgres": "beta",
|
||||
@@ -34,5 +34,8 @@
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,9 @@
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "18.2.74"
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.6.0"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
"@payloadcms/next": "beta",
|
||||
"@payloadcms/plugin-cloud": "beta",
|
||||
"@payloadcms/richtext-lexical": "beta",
|
||||
"@payloadcms/storage-vercel-blob": "beta",
|
||||
"cross-env": "^7.0.3",
|
||||
"graphql": "^16.8.1",
|
||||
"next": "15.0.0-rc.0",
|
||||
"payload": "beta",
|
||||
"react": "^19.0.0-rc-f994737d14-20240522",
|
||||
"react-dom": "^19.0.0-rc-f994737d14-20240522",
|
||||
"@payloadcms/storage-vercel-blob": "beta"
|
||||
"react-dom": "^19.0.0-rc-f994737d14-20240522"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.12",
|
||||
@@ -36,7 +36,7 @@
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.6.0"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
||||
@@ -14,17 +14,17 @@
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/db-postgres": "beta",
|
||||
"@payloadcms/next": "beta",
|
||||
"@payloadcms/plugin-cloud": "beta",
|
||||
"@payloadcms/richtext-lexical": "beta",
|
||||
"@payloadcms/storage-vercel-blob": "beta",
|
||||
"cross-env": "^7.0.3",
|
||||
"graphql": "^16.8.1",
|
||||
"next": "15.0.0-rc.0",
|
||||
"payload": "beta",
|
||||
"react": "^19.0.0-rc-f994737d14-20240522",
|
||||
"react-dom": "^19.0.0-rc-f994737d14-20240522",
|
||||
"@payloadcms/db-postgres": "beta",
|
||||
"@payloadcms/storage-vercel-blob": "beta"
|
||||
"react-dom": "^19.0.0-rc-f994737d14-20240522"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.12",
|
||||
@@ -36,7 +36,7 @@
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.6.0"
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
5
test/field-error-states/shared.ts
Normal file
5
test/field-error-states/shared.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const slugs = {
|
||||
validateDraftsOn: 'validate-drafts-on',
|
||||
validateDraftsOnAutosave: 'validate-drafts-on-autosave',
|
||||
validateDraftsOff: 'validate-drafts-off',
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
122
test/live-preview/helpers.ts
Normal file
122
test/live-preview/helpers.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -27,6 +27,17 @@ export default buildConfigWithDefaults({
|
||||
plugins: [
|
||||
redirectsPlugin({
|
||||
collections: ['pages'],
|
||||
overrides: {
|
||||
fields: ({ defaultFields }) => {
|
||||
return [
|
||||
...defaultFields,
|
||||
{
|
||||
type: 'text',
|
||||
name: 'customField',
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
125
test/versions/collections/DraftsWithMax.ts
Normal file
125
test/versions/collections/DraftsWithMax.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
import CollectionVersionButton from '../elements/CollectionVersionButton/index.js'
|
||||
import CollectionVersionsButton from '../elements/CollectionVersionsButton/index.js'
|
||||
import { CustomPublishButton } from '../elements/CustomSaveButton/index.js'
|
||||
import { draftWithMaxCollectionSlug } from '../slugs.js'
|
||||
|
||||
const DraftWithMaxPosts: CollectionConfig = {
|
||||
slug: draftWithMaxCollectionSlug,
|
||||
access: {
|
||||
read: ({ req: { user } }) => {
|
||||
if (user) {
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
},
|
||||
{
|
||||
_status: {
|
||||
exists: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
readVersions: ({ req: { user } }) => Boolean(user),
|
||||
},
|
||||
admin: {
|
||||
components: {
|
||||
edit: {
|
||||
PublishButton: CustomPublishButton,
|
||||
},
|
||||
views: {
|
||||
Edit: {
|
||||
Version: {
|
||||
actions: [CollectionVersionButton],
|
||||
},
|
||||
Versions: {
|
||||
actions: [CollectionVersionsButton],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultColumns: ['title', 'description', 'createdAt', '_status'],
|
||||
preview: () => 'https://payloadcms.com',
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
label: 'Title',
|
||||
localized: true,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
label: 'Description',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'radio',
|
||||
type: 'radio',
|
||||
options: [
|
||||
{
|
||||
label: { en: 'Test en', es: 'Test es' },
|
||||
value: 'test',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'select',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: [
|
||||
{
|
||||
label: { en: 'Test1 en', es: 'Test1 es' },
|
||||
value: 'test1',
|
||||
},
|
||||
{
|
||||
label: { en: 'Test2 en', es: 'Test2 es' },
|
||||
value: 'test2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blocksField',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'localized',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'relation',
|
||||
type: 'relationship',
|
||||
relationTo: draftWithMaxCollectionSlug,
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: true,
|
||||
maxPerDoc: 1,
|
||||
},
|
||||
}
|
||||
|
||||
export default DraftWithMaxPosts
|
||||
@@ -3,16 +3,26 @@ import AutosavePosts from './collections/Autosave.js'
|
||||
import CustomIDs from './collections/CustomIDs.js'
|
||||
import DisablePublish from './collections/DisablePublish.js'
|
||||
import DraftPosts from './collections/Drafts.js'
|
||||
import DraftWithMax from './collections/DraftsWithMax.js'
|
||||
import Posts from './collections/Posts.js'
|
||||
import VersionPosts from './collections/Versions.js'
|
||||
import AutosaveGlobal from './globals/Autosave.js'
|
||||
import DisablePublishGlobal from './globals/DisablePublish.js'
|
||||
import DraftGlobal from './globals/Draft.js'
|
||||
import DraftWithMaxGlobal from './globals/DraftWithMax.js'
|
||||
import { seed } from './seed.js'
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [DisablePublish, Posts, AutosavePosts, DraftPosts, VersionPosts, CustomIDs],
|
||||
globals: [AutosaveGlobal, DraftGlobal, DisablePublishGlobal],
|
||||
collections: [
|
||||
DisablePublish,
|
||||
Posts,
|
||||
AutosavePosts,
|
||||
DraftPosts,
|
||||
DraftWithMax,
|
||||
VersionPosts,
|
||||
CustomIDs,
|
||||
],
|
||||
globals: [AutosaveGlobal, DraftGlobal, DraftWithMaxGlobal, DisablePublishGlobal],
|
||||
indexSortableFields: true,
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
|
||||
@@ -55,6 +55,8 @@ import {
|
||||
disablePublishSlug,
|
||||
draftCollectionSlug,
|
||||
draftGlobalSlug,
|
||||
draftWithMaxCollectionSlug,
|
||||
draftWithMaxGlobalSlug,
|
||||
postCollectionSlug,
|
||||
} from './slugs.js'
|
||||
|
||||
@@ -352,6 +354,44 @@ describe('versions', () => {
|
||||
expect(href).toBe(`${pathname}/versions`)
|
||||
})
|
||||
|
||||
test('global — respects max number of versions', async () => {
|
||||
await payload.updateGlobal({
|
||||
slug: draftWithMaxGlobalSlug,
|
||||
data: {
|
||||
title: 'initial title',
|
||||
},
|
||||
})
|
||||
|
||||
const global = new AdminUrlUtil(serverURL, draftWithMaxGlobalSlug)
|
||||
await page.goto(global.global(draftWithMaxGlobalSlug))
|
||||
|
||||
const titleFieldInitial = page.locator('#field-title')
|
||||
await titleFieldInitial.fill('updated title')
|
||||
await saveDocAndAssert(page, '#action-save-draft')
|
||||
await expect(titleFieldInitial).toHaveValue('updated title')
|
||||
|
||||
const versionsTab = page.locator('.doc-tab', {
|
||||
hasText: '1',
|
||||
})
|
||||
|
||||
await versionsTab.waitFor({ state: 'visible' })
|
||||
|
||||
expect(versionsTab).toBeTruthy()
|
||||
|
||||
const titleFieldUpdated = page.locator('#field-title')
|
||||
await titleFieldUpdated.fill('latest title')
|
||||
await saveDocAndAssert(page, '#action-save-draft')
|
||||
await expect(titleFieldUpdated).toHaveValue('latest title')
|
||||
|
||||
const versionsTabUpdated = page.locator('.doc-tab', {
|
||||
hasText: '1',
|
||||
})
|
||||
|
||||
await versionsTabUpdated.waitFor({ state: 'visible' })
|
||||
|
||||
expect(versionsTabUpdated).toBeTruthy()
|
||||
})
|
||||
|
||||
test('global — has versions route', async () => {
|
||||
const global = new AdminUrlUtil(serverURL, globalSlug)
|
||||
const versionsURL = `${global.global(globalSlug)}/versions`
|
||||
@@ -535,5 +575,45 @@ describe('versions', () => {
|
||||
|
||||
await expect(page.locator('.rs__option')).toHaveText('some title')
|
||||
})
|
||||
|
||||
test('collection — respects max number of versions', async () => {
|
||||
const maxOneCollection = await payload.create({
|
||||
collection: draftWithMaxCollectionSlug,
|
||||
data: {
|
||||
title: 'initial title',
|
||||
description: 'some description',
|
||||
},
|
||||
draft: true,
|
||||
})
|
||||
|
||||
const collection = new AdminUrlUtil(serverURL, draftWithMaxCollectionSlug)
|
||||
await page.goto(collection.edit(maxOneCollection.id))
|
||||
|
||||
const titleFieldInitial = page.locator('#field-title')
|
||||
await titleFieldInitial.fill('updated title')
|
||||
await saveDocAndAssert(page, '#action-save-draft')
|
||||
await expect(titleFieldInitial).toHaveValue('updated title')
|
||||
|
||||
const versionsTab = page.locator('.doc-tab', {
|
||||
hasText: '1',
|
||||
})
|
||||
|
||||
await versionsTab.waitFor({ state: 'visible' })
|
||||
|
||||
expect(versionsTab).toBeTruthy()
|
||||
|
||||
const titleFieldUpdated = page.locator('#field-title')
|
||||
await titleFieldUpdated.fill('latest title')
|
||||
await saveDocAndAssert(page, '#action-save-draft')
|
||||
await expect(titleFieldUpdated).toHaveValue('latest title')
|
||||
|
||||
const versionsTabUpdated = page.locator('.doc-tab', {
|
||||
hasText: '1',
|
||||
})
|
||||
|
||||
await versionsTabUpdated.waitFor({ state: 'visible' })
|
||||
|
||||
expect(versionsTabUpdated).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
61
test/versions/globals/DraftWithMax.ts
Normal file
61
test/versions/globals/DraftWithMax.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { GlobalConfig } from 'payload/types'
|
||||
|
||||
import GlobalVersionButton from '../elements/GlobalVersionButton/index.js'
|
||||
import GlobalVersionsButton from '../elements/GlobalVersionsButton/index.js'
|
||||
import { draftWithMaxGlobalSlug } from '../slugs.js'
|
||||
|
||||
const DraftWithMaxGlobal: GlobalConfig = {
|
||||
slug: draftWithMaxGlobalSlug,
|
||||
label: 'Draft Global',
|
||||
admin: {
|
||||
preview: () => 'https://payloadcms.com',
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
Version: {
|
||||
actions: [GlobalVersionButton],
|
||||
},
|
||||
Versions: {
|
||||
actions: [GlobalVersionsButton],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
max: 1,
|
||||
drafts: true,
|
||||
},
|
||||
access: {
|
||||
read: ({ req: { user } }) => {
|
||||
if (user) {
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
},
|
||||
{
|
||||
_status: {
|
||||
exists: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
localized: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default DraftWithMaxGlobal
|
||||
@@ -12,6 +12,7 @@ export interface Config {
|
||||
posts: Post;
|
||||
'autosave-posts': AutosavePost;
|
||||
'draft-posts': DraftPost;
|
||||
'draft-with-max-posts': DraftWithMaxPost;
|
||||
'version-posts': VersionPost;
|
||||
'custom-ids': CustomId;
|
||||
users: User;
|
||||
@@ -21,6 +22,7 @@ export interface Config {
|
||||
globals: {
|
||||
'autosave-global': AutosaveGlobal;
|
||||
'draft-global': DraftGlobal;
|
||||
'draft-with-max-global': DraftWithMaxGlobal;
|
||||
'disable-publish-global': DisablePublishGlobal;
|
||||
};
|
||||
locale: 'en' | 'es';
|
||||
@@ -98,6 +100,30 @@ export interface DraftPost {
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "draft-with-max-posts".
|
||||
*/
|
||||
export interface DraftWithMaxPost {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
radio?: 'test' | null;
|
||||
select?: ('test1' | 'test2')[] | null;
|
||||
blocksField?:
|
||||
| {
|
||||
text?: string | null;
|
||||
localized?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'block';
|
||||
}[]
|
||||
| null;
|
||||
relation?: (string | null) | DraftWithMaxPost;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "custom-ids".
|
||||
@@ -181,6 +207,17 @@ export interface DraftGlobal {
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "draft-with-max-global".
|
||||
*/
|
||||
export interface DraftWithMaxGlobal {
|
||||
id: string;
|
||||
title: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "disable-publish-global".
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
export const autosaveCollectionSlug = 'autosave-posts' as const
|
||||
export const autosaveCollectionSlug = 'autosave-posts'
|
||||
|
||||
export const customIDSlug = 'custom-ids' as const
|
||||
export const customIDSlug = 'custom-ids'
|
||||
|
||||
export const draftCollectionSlug = 'draft-posts' as const
|
||||
export const draftCollectionSlug = 'draft-posts'
|
||||
export const draftWithMaxCollectionSlug = 'draft-with-max-posts'
|
||||
|
||||
export const postCollectionSlug = 'posts' as const
|
||||
export const postCollectionSlug = 'posts'
|
||||
|
||||
export const versionCollectionSlug = 'version-posts' as const
|
||||
export const versionCollectionSlug = 'version-posts'
|
||||
|
||||
export const disablePublishSlug = 'disable-publish' as const
|
||||
export const disablePublishSlug = 'disable-publish'
|
||||
|
||||
export const disablePublishGlobalSlug = 'disable-publish-global' as const
|
||||
export const disablePublishGlobalSlug = 'disable-publish-global'
|
||||
|
||||
export const collectionSlugs = [
|
||||
autosaveCollectionSlug,
|
||||
@@ -19,7 +20,8 @@ export const collectionSlugs = [
|
||||
versionCollectionSlug,
|
||||
]
|
||||
|
||||
export const autoSaveGlobalSlug = 'autosave-global' as const
|
||||
export const draftGlobalSlug = 'draft-global' as const
|
||||
export const autoSaveGlobalSlug = 'autosave-global'
|
||||
export const draftGlobalSlug = 'draft-global'
|
||||
export const draftWithMaxGlobalSlug = 'draft-with-max-global'
|
||||
|
||||
export const globalSlugs = [autoSaveGlobalSlug, draftGlobalSlug]
|
||||
|
||||
Reference in New Issue
Block a user