Merge branch 'main' into feat/folders
This commit is contained in:
@@ -69,6 +69,12 @@ export const Posts: CollectionConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
edit: {
|
||||
beforeDocumentControls: [
|
||||
'/components/BeforeDocumentControls/CustomDraftButton/index.js#CustomDraftButton',
|
||||
'/components/BeforeDocumentControls/CustomSaveButton/index.js#CustomSaveButton',
|
||||
],
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
defaultLimit: 5,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { BeforeDocumentControlsServerProps } from 'payload'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
const baseClass = 'custom-draft-button'
|
||||
|
||||
export function CustomDraftButton(props: BeforeDocumentControlsServerProps) {
|
||||
return (
|
||||
<div
|
||||
className={baseClass}
|
||||
id="custom-draft-button"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'calc(var(--base) / 4)',
|
||||
}}
|
||||
>
|
||||
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
|
||||
Custom Draft Button
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { BeforeDocumentControlsServerProps } from 'payload'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
const baseClass = 'custom-save-button'
|
||||
|
||||
export function CustomSaveButton(props: BeforeDocumentControlsServerProps) {
|
||||
return (
|
||||
<div
|
||||
className={baseClass}
|
||||
id="custom-save-button"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'calc(var(--base) / 4)',
|
||||
}}
|
||||
>
|
||||
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
|
||||
Custom Save Button
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -110,7 +110,7 @@ describe('Document View', () => {
|
||||
})
|
||||
expect(collectionItems.docs.length).toBe(1)
|
||||
await page.goto(
|
||||
`${postsUrl.collection(noApiViewGlobalSlug)}/${collectionItems.docs[0].id}/api`,
|
||||
`${postsUrl.collection(noApiViewGlobalSlug)}/${collectionItems?.docs[0]?.id}/api`,
|
||||
)
|
||||
await expect(page.locator('.not-found')).toHaveCount(1)
|
||||
})
|
||||
@@ -333,20 +333,32 @@ describe('Document View', () => {
|
||||
await navigateToDoc(page, postsUrl)
|
||||
await page.locator('#field-title').fill(title)
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
await page
|
||||
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
|
||||
.click()
|
||||
|
||||
await wait(500)
|
||||
|
||||
const drawer1Content = page.locator('[id^=doc-drawer_posts_1_] .drawer__content')
|
||||
await expect(drawer1Content).toBeVisible()
|
||||
const drawerLeft = await drawer1Content.boundingBox().then((box) => box.x)
|
||||
|
||||
const drawer1Box = await drawer1Content.boundingBox()
|
||||
await expect.poll(() => drawer1Box).not.toBeNull()
|
||||
const drawerLeft = drawer1Box!.x
|
||||
|
||||
await drawer1Content
|
||||
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
|
||||
.click()
|
||||
|
||||
const drawer2Content = page.locator('[id^=doc-drawer_posts_2_] .drawer__content')
|
||||
await expect(drawer2Content).toBeVisible()
|
||||
const drawer2Left = await drawer2Content.boundingBox().then((box) => box.x)
|
||||
expect(drawer2Left > drawerLeft).toBe(true)
|
||||
|
||||
const drawer2Box = await drawer2Content.boundingBox()
|
||||
await expect.poll(() => drawer2Box).not.toBeNull()
|
||||
const drawer2Left = drawer2Box!.x
|
||||
|
||||
await expect.poll(() => drawer2Left > drawerLeft).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -523,6 +535,24 @@ describe('Document View', () => {
|
||||
await expect(fileField).toHaveValue('some file text')
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom document controls', () => {
|
||||
test('should show custom elements in document controls in collection', async () => {
|
||||
await page.goto(postsUrl.create)
|
||||
const customDraftButton = page.locator('#custom-draft-button')
|
||||
const customSaveButton = page.locator('#custom-save-button')
|
||||
|
||||
await expect(customDraftButton).toBeVisible()
|
||||
await expect(customSaveButton).toBeVisible()
|
||||
})
|
||||
|
||||
test('should show custom elements in document controls in global', async () => {
|
||||
await page.goto(globalURL.global(globalSlug))
|
||||
const customDraftButton = page.locator('#custom-draft-button')
|
||||
|
||||
await expect(customDraftButton).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createPost(overrides?: Partial<Post>): Promise<Post> {
|
||||
|
||||
@@ -6,6 +6,11 @@ export const Global: GlobalConfig = {
|
||||
slug: globalSlug,
|
||||
admin: {
|
||||
components: {
|
||||
elements: {
|
||||
beforeDocumentControls: [
|
||||
'/components/BeforeDocumentControls/CustomDraftButton/index.js#CustomDraftButton',
|
||||
],
|
||||
},
|
||||
views: {
|
||||
edit: {
|
||||
api: {
|
||||
|
||||
@@ -36,6 +36,15 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: 'categories',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: postsSlug,
|
||||
fields: [
|
||||
@@ -43,6 +52,21 @@ export default buildConfigWithDefaults({
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
// access: { read: () => false },
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'categories',
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'localized',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
@@ -433,6 +457,33 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'virtual-relations',
|
||||
admin: { useAsTitle: 'postTitle' },
|
||||
fields: [
|
||||
{
|
||||
name: 'postTitle',
|
||||
type: 'text',
|
||||
virtual: 'post.title',
|
||||
},
|
||||
{
|
||||
name: 'postCategoryTitle',
|
||||
type: 'text',
|
||||
virtual: 'post.category.title',
|
||||
},
|
||||
{
|
||||
name: 'postLocalized',
|
||||
type: 'text',
|
||||
virtual: 'post.localized',
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
],
|
||||
versions: { drafts: true },
|
||||
},
|
||||
{
|
||||
slug: fieldsPersistanceSlug,
|
||||
fields: [
|
||||
@@ -658,6 +709,21 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'virtual-relation-global',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'postTitle',
|
||||
virtual: 'post.title',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'post',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
migrateRelationshipsV2_V3,
|
||||
migrateVersionsV1_V2,
|
||||
} from '@payloadcms/db-mongodb/migration-utils'
|
||||
import { objectToFrontmatter } from '@payloadcms/richtext-lexical'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type Table } from 'drizzle-orm'
|
||||
import * as drizzlePg from 'drizzle-orm/pg-core'
|
||||
@@ -1977,6 +1978,145 @@ describe('database', () => {
|
||||
expect(res.textWithinRow).toBeUndefined()
|
||||
expect(res.textWithinTabs).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow virtual field with reference', async () => {
|
||||
const post = await payload.create({ collection: 'posts', data: { title: 'my-title' } })
|
||||
const { id } = await payload.create({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
data: { post: post.id },
|
||||
})
|
||||
|
||||
const doc = await payload.findByID({ collection: 'virtual-relations', depth: 0, id })
|
||||
expect(doc.postTitle).toBe('my-title')
|
||||
const draft = await payload.find({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
where: { id: { equals: id } },
|
||||
draft: true,
|
||||
})
|
||||
expect(draft.docs[0]?.postTitle).toBe('my-title')
|
||||
})
|
||||
|
||||
it('should allow virtual field with reference localized', async () => {
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: 'my-title', localized: 'localized en' },
|
||||
})
|
||||
|
||||
await payload.update({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
locale: 'es',
|
||||
data: { localized: 'localized es' },
|
||||
})
|
||||
|
||||
const { id } = await payload.create({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
data: { post: post.id },
|
||||
})
|
||||
|
||||
let doc = await payload.findByID({ collection: 'virtual-relations', depth: 0, id })
|
||||
expect(doc.postLocalized).toBe('localized en')
|
||||
|
||||
doc = await payload.findByID({ collection: 'virtual-relations', depth: 0, id, locale: 'es' })
|
||||
expect(doc.postLocalized).toBe('localized es')
|
||||
})
|
||||
|
||||
it('should allow to query by a virtual field with reference', async () => {
|
||||
await payload.delete({ collection: 'posts', where: {} })
|
||||
await payload.delete({ collection: 'virtual-relations', where: {} })
|
||||
const post_1 = await payload.create({ collection: 'posts', data: { title: 'Dan' } })
|
||||
const post_2 = await payload.create({ collection: 'posts', data: { title: 'Mr.Dan' } })
|
||||
|
||||
const doc_1 = await payload.create({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
data: { post: post_1.id },
|
||||
})
|
||||
const doc_2 = await payload.create({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
data: { post: post_2.id },
|
||||
})
|
||||
|
||||
const { docs: ascDocs } = await payload.find({
|
||||
collection: 'virtual-relations',
|
||||
sort: 'postTitle',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(ascDocs[0]?.id).toBe(doc_1.id)
|
||||
|
||||
expect(ascDocs[1]?.id).toBe(doc_2.id)
|
||||
|
||||
const { docs: descDocs } = await payload.find({
|
||||
collection: 'virtual-relations',
|
||||
sort: '-postTitle',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(descDocs[1]?.id).toBe(doc_1.id)
|
||||
|
||||
expect(descDocs[0]?.id).toBe(doc_2.id)
|
||||
})
|
||||
|
||||
it.todo('should allow to sort by a virtual field with reference')
|
||||
|
||||
it('should allow virtual field 2x deep', async () => {
|
||||
const category = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: '1-category' },
|
||||
})
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: '1-post', category: category.id },
|
||||
})
|
||||
const doc = await payload.create({ collection: 'virtual-relations', data: { post: post.id } })
|
||||
expect(doc.postCategoryTitle).toBe('1-category')
|
||||
})
|
||||
|
||||
it('should allow to query by virtual field 2x deep', async () => {
|
||||
const category = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: '2-category' },
|
||||
})
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: '2-post', category: category.id },
|
||||
})
|
||||
const doc = await payload.create({ collection: 'virtual-relations', data: { post: post.id } })
|
||||
const found = await payload.find({
|
||||
collection: 'virtual-relations',
|
||||
where: { postCategoryTitle: { equals: '2-category' } },
|
||||
})
|
||||
expect(found.docs).toHaveLength(1)
|
||||
expect(found.docs[0].id).toBe(doc.id)
|
||||
})
|
||||
|
||||
it('should allow referenced virtual field in globals', async () => {
|
||||
const post = await payload.create({ collection: 'posts', data: { title: 'post' } })
|
||||
const globalData = await payload.updateGlobal({
|
||||
slug: 'virtual-relation-global',
|
||||
data: { post: post.id },
|
||||
depth: 0,
|
||||
})
|
||||
expect(globalData.postTitle).toBe('post')
|
||||
})
|
||||
})
|
||||
|
||||
it('should convert numbers to text', async () => {
|
||||
const result = await payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
title: 'testing',
|
||||
// @ts-expect-error hardcoding a number and expecting that it will convert to string
|
||||
text: 1,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.text).toStrictEqual('1')
|
||||
})
|
||||
|
||||
it('should not allow to query by a field with `virtual: true`', async () => {
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface Config {
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
categories: Category;
|
||||
posts: Post;
|
||||
'error-on-unnamed-fields': ErrorOnUnnamedField;
|
||||
'default-values': DefaultValue;
|
||||
@@ -75,6 +76,7 @@ export interface Config {
|
||||
'pg-migrations': PgMigration;
|
||||
'custom-schema': CustomSchema;
|
||||
places: Place;
|
||||
'virtual-relations': VirtualRelation;
|
||||
'fields-persistance': FieldsPersistance;
|
||||
'custom-ids': CustomId;
|
||||
'fake-custom-ids': FakeCustomId;
|
||||
@@ -88,6 +90,7 @@ export interface Config {
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
categories: CategoriesSelect<false> | CategoriesSelect<true>;
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
'error-on-unnamed-fields': ErrorOnUnnamedFieldsSelect<false> | ErrorOnUnnamedFieldsSelect<true>;
|
||||
'default-values': DefaultValuesSelect<false> | DefaultValuesSelect<true>;
|
||||
@@ -96,6 +99,7 @@ export interface Config {
|
||||
'pg-migrations': PgMigrationsSelect<false> | PgMigrationsSelect<true>;
|
||||
'custom-schema': CustomSchemaSelect<false> | CustomSchemaSelect<true>;
|
||||
places: PlacesSelect<false> | PlacesSelect<true>;
|
||||
'virtual-relations': VirtualRelationsSelect<false> | VirtualRelationsSelect<true>;
|
||||
'fields-persistance': FieldsPersistanceSelect<false> | FieldsPersistanceSelect<true>;
|
||||
'custom-ids': CustomIdsSelect<false> | CustomIdsSelect<true>;
|
||||
'fake-custom-ids': FakeCustomIdsSelect<false> | FakeCustomIdsSelect<true>;
|
||||
@@ -114,11 +118,13 @@ export interface Config {
|
||||
global: Global;
|
||||
'global-2': Global2;
|
||||
'global-3': Global3;
|
||||
'virtual-relation-global': VirtualRelationGlobal;
|
||||
};
|
||||
globalsSelect: {
|
||||
global: GlobalSelect<false> | GlobalSelect<true>;
|
||||
'global-2': Global2Select<false> | Global2Select<true>;
|
||||
'global-3': Global3Select<false> | Global3Select<true>;
|
||||
'virtual-relation-global': VirtualRelationGlobalSelect<false> | VirtualRelationGlobalSelect<true>;
|
||||
};
|
||||
locale: 'en' | 'es';
|
||||
user: User & {
|
||||
@@ -147,6 +153,16 @@ export interface UserAuthOperations {
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "categories".
|
||||
*/
|
||||
export interface Category {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
@@ -154,6 +170,9 @@ export interface UserAuthOperations {
|
||||
export interface Post {
|
||||
id: string;
|
||||
title: string;
|
||||
category?: (string | null) | Category;
|
||||
localized?: string | null;
|
||||
text?: string | null;
|
||||
number?: number | null;
|
||||
D1?: {
|
||||
D2?: {
|
||||
@@ -346,6 +365,20 @@ export interface Place {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtual-relations".
|
||||
*/
|
||||
export interface VirtualRelation {
|
||||
id: string;
|
||||
postTitle?: string | null;
|
||||
postCategoryTitle?: string | null;
|
||||
postLocalized?: string | null;
|
||||
post?: (string | null) | Post;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "fields-persistance".
|
||||
@@ -465,6 +498,10 @@ export interface User {
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'categories';
|
||||
value: string | Category;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
@@ -497,6 +534,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'places';
|
||||
value: string | Place;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'virtual-relations';
|
||||
value: string | VirtualRelation;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'fields-persistance';
|
||||
value: string | FieldsPersistance;
|
||||
@@ -567,12 +608,24 @@ export interface PayloadMigration {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "categories_select".
|
||||
*/
|
||||
export interface CategoriesSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts_select".
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
category?: T;
|
||||
localized?: T;
|
||||
text?: T;
|
||||
number?: T;
|
||||
D1?:
|
||||
| T
|
||||
@@ -747,6 +800,19 @@ export interface PlacesSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtual-relations_select".
|
||||
*/
|
||||
export interface VirtualRelationsSelect<T extends boolean = true> {
|
||||
postTitle?: T;
|
||||
postCategoryTitle?: T;
|
||||
postLocalized?: T;
|
||||
post?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "fields-persistance_select".
|
||||
@@ -917,6 +983,17 @@ export interface Global3 {
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtual-relation-global".
|
||||
*/
|
||||
export interface VirtualRelationGlobal {
|
||||
id: string;
|
||||
postTitle?: string | null;
|
||||
post?: (string | null) | Post;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "global_select".
|
||||
@@ -947,6 +1024,17 @@ export interface Global3Select<T extends boolean = true> {
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtual-relation-global_select".
|
||||
*/
|
||||
export interface VirtualRelationGlobalSelect<T extends boolean = true> {
|
||||
postTitle?: T;
|
||||
post?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
|
||||
@@ -57,7 +57,10 @@ describe('Field Error States', () => {
|
||||
|
||||
// add third child array
|
||||
await page.locator('#parentArray-row-0 .collapsible__content .array-field__add-row').click()
|
||||
|
||||
// remove the row
|
||||
await page.locator('#parentArray-0-childArray-row-2 .array-actions__button').click()
|
||||
|
||||
await page
|
||||
.locator('#parentArray-0-childArray-row-2 .array-actions__action.array-actions__remove')
|
||||
.click()
|
||||
@@ -68,6 +71,7 @@ describe('Field Error States', () => {
|
||||
'#parentArray-row-0 > .collapsible > .collapsible__toggle-wrap .array-field__row-error-pill',
|
||||
{ state: 'hidden', timeout: 500 },
|
||||
)
|
||||
|
||||
expect(errorPill).toBeNull()
|
||||
})
|
||||
|
||||
@@ -77,13 +81,11 @@ describe('Field Error States', () => {
|
||||
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')
|
||||
|
||||
@@ -4,12 +4,12 @@ import * as React from 'react'
|
||||
|
||||
import { collection1Slug } from '../slugs.js'
|
||||
|
||||
export const PrePopulateFieldUI: React.FC<{
|
||||
export const PopulateFieldButton: React.FC<{
|
||||
hasMany?: boolean
|
||||
hasMultipleRelations?: boolean
|
||||
path?: string
|
||||
targetFieldPath: string
|
||||
}> = ({ hasMany = true, hasMultipleRelations = false, path, targetFieldPath }) => {
|
||||
}> = ({ hasMany = true, hasMultipleRelations = false, targetFieldPath }) => {
|
||||
const { setValue } = useField({ path: targetFieldPath })
|
||||
|
||||
const addDefaults = React.useCallback(() => {
|
||||
@@ -19,7 +19,7 @@ export const RelationshipUpdatedExternally: CollectionConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
path: '/PrePopulateFieldUI/index.js#PrePopulateFieldUI',
|
||||
path: '/PopulateFieldButton/index.js#PopulateFieldButton',
|
||||
clientProps: {
|
||||
hasMany: false,
|
||||
hasMultipleRelations: false,
|
||||
@@ -50,7 +50,7 @@ export const RelationshipUpdatedExternally: CollectionConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
path: '/PrePopulateFieldUI/index.js#PrePopulateFieldUI',
|
||||
path: '/PopulateFieldButton/index.js#PopulateFieldButton',
|
||||
clientProps: {
|
||||
hasMultipleRelations: false,
|
||||
targetFieldPath: 'relationHasMany',
|
||||
@@ -80,7 +80,7 @@ export const RelationshipUpdatedExternally: CollectionConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
path: '/PrePopulateFieldUI/index.js#PrePopulateFieldUI',
|
||||
path: '/PopulateFieldButton/index.js#PopulateFieldButton',
|
||||
clientProps: {
|
||||
hasMultipleRelations: true,
|
||||
targetFieldPath: 'relationToManyHasMany',
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Config } from '../../payload-types.js'
|
||||
import {
|
||||
ensureCompilationIsDone,
|
||||
initPageConsoleErrorCatch,
|
||||
saveDocAndAssert,
|
||||
// throttleTest,
|
||||
} from '../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
@@ -225,4 +226,19 @@ describe('Conditional Logic', () => {
|
||||
|
||||
await expect(numberField).toBeVisible()
|
||||
})
|
||||
|
||||
test('should render field based on operation argument', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
const textField = page.locator('#field-text')
|
||||
const fieldWithOperationCondition = page.locator('#field-fieldWithOperationCondition')
|
||||
|
||||
await textField.fill('some text')
|
||||
|
||||
await expect(fieldWithOperationCondition).toBeVisible()
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
await expect(fieldWithOperationCondition).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,6 +24,19 @@ const ConditionalLogic: CollectionConfig = {
|
||||
condition: ({ toggleField }) => Boolean(toggleField),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldWithOperationCondition',
|
||||
type: 'text',
|
||||
admin: {
|
||||
condition: (data, siblingData, { operation }) => {
|
||||
if (operation === 'create') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'customFieldWithField',
|
||||
type: 'text',
|
||||
@@ -217,7 +230,7 @@ const ConditionalLogic: CollectionConfig = {
|
||||
name: 'numberField',
|
||||
type: 'number',
|
||||
admin: {
|
||||
condition: (data, siblingData, { path, user }) => {
|
||||
condition: (data, siblingData, { path }) => {
|
||||
// Ensure path has enough depth
|
||||
if (path.length < 5) {
|
||||
return false
|
||||
|
||||
@@ -650,6 +650,163 @@ describe('relationship', () => {
|
||||
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('should be able to select relationship with drawer appearance', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
const relationshipField = page.locator('#field-relationshipDrawer')
|
||||
await relationshipField.click()
|
||||
const listDrawerContent = page.locator('.list-drawer').locator('.drawer__content')
|
||||
await expect(listDrawerContent).toBeVisible()
|
||||
|
||||
const firstRow = listDrawerContent.locator('table tbody tr').first()
|
||||
const button = firstRow.locator('button')
|
||||
await button.click()
|
||||
await expect(listDrawerContent).toBeHidden()
|
||||
|
||||
const selectedValue = relationshipField.locator('.relationship--single-value__text')
|
||||
await expect(selectedValue).toBeVisible()
|
||||
|
||||
// Fill required field
|
||||
await page.locator('#field-relationship').click()
|
||||
await page.locator('.rs__option:has-text("Seeded text document")').click()
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
})
|
||||
|
||||
test('should be able to search within relationship list drawer', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
const relationshipField = page.locator('#field-relationshipDrawer')
|
||||
await relationshipField.click()
|
||||
const searchField = page.locator('.list-drawer .search-filter')
|
||||
await expect(searchField).toBeVisible()
|
||||
|
||||
const searchInput = searchField.locator('input')
|
||||
await searchInput.fill('seeded')
|
||||
const rows = page.locator('.list-drawer table tbody tr')
|
||||
|
||||
await expect(rows).toHaveCount(1)
|
||||
const closeButton = page.locator('.list-drawer__header-close')
|
||||
await closeButton.click()
|
||||
|
||||
await expect(page.locator('.list-drawer')).toBeHidden()
|
||||
})
|
||||
|
||||
test('should handle read-only relationship field when `appearance: "drawer"`', async () => {
|
||||
await page.goto(url.create)
|
||||
const readOnlyField = page.locator(
|
||||
'#field-relationshipDrawerReadOnly .rs__control--is-disabled',
|
||||
)
|
||||
await expect(readOnlyField).toBeVisible()
|
||||
})
|
||||
|
||||
test('should handle polymorphic relationship when `appearance: "drawer"`', async () => {
|
||||
await page.goto(url.create)
|
||||
const relationshipField = page.locator('#field-polymorphicRelationshipDrawer')
|
||||
await relationshipField.click()
|
||||
const listDrawerContent = page.locator('.list-drawer').locator('.drawer__content')
|
||||
await expect(listDrawerContent).toBeVisible()
|
||||
|
||||
const relationToSelector = page.locator('.list-header__select-collection')
|
||||
await expect(relationToSelector).toBeVisible()
|
||||
|
||||
await relationToSelector.locator('.rs__control').click()
|
||||
const option = relationToSelector.locator('.rs__option').nth(1)
|
||||
await option.click()
|
||||
const firstRow = listDrawerContent.locator('table tbody tr').first()
|
||||
const button = firstRow.locator('button')
|
||||
await button.click()
|
||||
await expect(listDrawerContent).toBeHidden()
|
||||
|
||||
const selectedValue = relationshipField.locator('.relationship--single-value__text')
|
||||
await expect(selectedValue).toBeVisible()
|
||||
|
||||
// Fill required field
|
||||
await page.locator('#field-relationship').click()
|
||||
await page.locator('.rs__option:has-text("Seeded text document")').click()
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
})
|
||||
|
||||
test('should handle `hasMany` relationship when `appearance: "drawer"`', async () => {
|
||||
await page.goto(url.create)
|
||||
const relationshipField = page.locator('#field-relationshipDrawerHasMany')
|
||||
await relationshipField.click()
|
||||
const listDrawerContent = page.locator('.list-drawer').locator('.drawer__content')
|
||||
await expect(listDrawerContent).toBeVisible()
|
||||
|
||||
const firstRow = listDrawerContent.locator('table tbody tr').first()
|
||||
const button = firstRow.locator('button')
|
||||
await button.click()
|
||||
await expect(listDrawerContent).toBeHidden()
|
||||
|
||||
const selectedValue = relationshipField.locator('.relationship--multi-value-label__text')
|
||||
await expect(selectedValue).toBeVisible()
|
||||
})
|
||||
|
||||
test('should handle `hasMany` polymorphic relationship when `appearance: "drawer"`', async () => {
|
||||
await page.goto(url.create)
|
||||
const relationshipField = page.locator('#field-relationshipDrawerHasManyPolymorphic')
|
||||
await relationshipField.click()
|
||||
const listDrawerContent = page.locator('.list-drawer').locator('.drawer__content')
|
||||
await expect(listDrawerContent).toBeVisible()
|
||||
|
||||
const firstRow = listDrawerContent.locator('table tbody tr').first()
|
||||
const button = firstRow.locator('button')
|
||||
await button.click()
|
||||
await expect(listDrawerContent).toBeHidden()
|
||||
|
||||
const selectedValue = relationshipField.locator('.relationship--multi-value-label__text')
|
||||
await expect(selectedValue).toBeVisible()
|
||||
})
|
||||
|
||||
test('should not be allowed to create in relationship list drawer when `allowCreate` is `false`', async () => {
|
||||
await page.goto(url.create)
|
||||
const relationshipField = page.locator('#field-relationshipDrawerWithAllowCreateFalse')
|
||||
await relationshipField.click()
|
||||
const listDrawerContent = page.locator('.list-drawer').locator('.drawer__content')
|
||||
await expect(listDrawerContent).toBeVisible()
|
||||
|
||||
const createNewButton = listDrawerContent.locator('list-drawer__create-new-button')
|
||||
await expect(createNewButton).toBeHidden()
|
||||
})
|
||||
|
||||
test('should respect `filterOptions` in the relationship list drawer for filtered relationship', async () => {
|
||||
// Create test documents
|
||||
await createTextFieldDoc({ text: 'list drawer test' })
|
||||
await createTextFieldDoc({ text: 'not test' })
|
||||
await page.goto(url.create)
|
||||
|
||||
const relationshipField = page.locator('#field-relationshipDrawerWithFilterOptions')
|
||||
await relationshipField.click()
|
||||
const listDrawerContent = page.locator('.list-drawer').locator('.drawer__content')
|
||||
await expect(listDrawerContent).toBeVisible()
|
||||
|
||||
const rows = page.locator('.list-drawer table tbody tr')
|
||||
await expect(rows).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('should filter out existing values from relationship list drawer', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
await page.locator('#field-relationshipDrawer').click()
|
||||
const listDrawerContent = page.locator('.list-drawer').locator('.drawer__content')
|
||||
await expect(listDrawerContent).toBeVisible()
|
||||
const rows = listDrawerContent.locator('table tbody tr')
|
||||
await expect(rows).toHaveCount(2)
|
||||
await listDrawerContent.getByText('Seeded text document', { exact: true }).click()
|
||||
|
||||
const selectedValue = page.locator(
|
||||
'#field-relationshipDrawer .relationship--single-value__text',
|
||||
)
|
||||
|
||||
await expect(selectedValue).toHaveText('Seeded text document')
|
||||
await page.locator('#field-relationshipDrawer').click()
|
||||
const newRows = listDrawerContent.locator('table tbody tr')
|
||||
await expect(newRows).toHaveCount(1)
|
||||
await expect(listDrawerContent.getByText('Seeded text document')).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
async function createTextFieldDoc(overrides?: Partial<TextField>): Promise<TextField> {
|
||||
|
||||
@@ -126,6 +126,71 @@ const RelationshipFields: CollectionConfig = {
|
||||
type: 'relationship',
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'relationshipDrawer',
|
||||
relationTo: 'text-fields',
|
||||
admin: { appearance: 'drawer' },
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
name: 'relationshipDrawerReadOnly',
|
||||
relationTo: 'text-fields',
|
||||
admin: {
|
||||
readOnly: true,
|
||||
appearance: 'drawer',
|
||||
},
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
name: 'polymorphicRelationshipDrawer',
|
||||
admin: { appearance: 'drawer' },
|
||||
type: 'relationship',
|
||||
relationTo: ['text-fields', 'array-fields'],
|
||||
},
|
||||
{
|
||||
name: 'relationshipDrawerHasMany',
|
||||
relationTo: 'text-fields',
|
||||
admin: {
|
||||
appearance: 'drawer',
|
||||
},
|
||||
hasMany: true,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
name: 'relationshipDrawerHasManyPolymorphic',
|
||||
relationTo: ['text-fields'],
|
||||
admin: {
|
||||
appearance: 'drawer',
|
||||
},
|
||||
hasMany: true,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
name: 'relationshipDrawerWithAllowCreateFalse',
|
||||
admin: {
|
||||
allowCreate: false,
|
||||
appearance: 'drawer',
|
||||
},
|
||||
type: 'relationship',
|
||||
relationTo: 'text-fields',
|
||||
},
|
||||
{
|
||||
name: 'relationshipDrawerWithFilterOptions',
|
||||
admin: { appearance: 'drawer' },
|
||||
type: 'relationship',
|
||||
relationTo: ['text-fields'],
|
||||
filterOptions: ({ relationTo }) => {
|
||||
if (relationTo === 'text-fields') {
|
||||
return {
|
||||
text: {
|
||||
equals: 'list drawer test',
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
slug: relationshipFieldsSlug,
|
||||
}
|
||||
|
||||
@@ -790,6 +790,7 @@ export interface ConditionalLogic {
|
||||
text: string;
|
||||
toggleField?: boolean | null;
|
||||
fieldWithCondition?: string | null;
|
||||
fieldWithOperationCondition?: string | null;
|
||||
customFieldWithField?: string | null;
|
||||
customFieldWithHOC?: string | null;
|
||||
customClientFieldWithCondition?: string | null;
|
||||
@@ -1312,6 +1313,29 @@ export interface RelationshipField {
|
||||
| null;
|
||||
relationToRow?: (string | null) | RowField;
|
||||
relationToRowMany?: (string | RowField)[] | null;
|
||||
relationshipDrawer?: (string | null) | TextField;
|
||||
relationshipDrawerReadOnly?: (string | null) | TextField;
|
||||
polymorphicRelationshipDrawer?:
|
||||
| ({
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'array-fields';
|
||||
value: string | ArrayField;
|
||||
} | null);
|
||||
relationshipDrawerHasMany?: (string | TextField)[] | null;
|
||||
relationshipDrawerHasManyPolymorphic?:
|
||||
| {
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
}[]
|
||||
| null;
|
||||
relationshipDrawerWithAllowCreateFalse?: (string | null) | TextField;
|
||||
relationshipDrawerWithFilterOptions?: {
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -2341,6 +2365,7 @@ export interface ConditionalLogicSelect<T extends boolean = true> {
|
||||
text?: T;
|
||||
toggleField?: T;
|
||||
fieldWithCondition?: T;
|
||||
fieldWithOperationCondition?: T;
|
||||
customFieldWithField?: T;
|
||||
customFieldWithHOC?: T;
|
||||
customClientFieldWithCondition?: T;
|
||||
@@ -2786,6 +2811,13 @@ export interface RelationshipFieldsSelect<T extends boolean = true> {
|
||||
relationshipWithMinRows?: T;
|
||||
relationToRow?: T;
|
||||
relationToRowMany?: T;
|
||||
relationshipDrawer?: T;
|
||||
relationshipDrawerReadOnly?: T;
|
||||
polymorphicRelationshipDrawer?: T;
|
||||
relationshipDrawerHasMany?: T;
|
||||
relationshipDrawerHasManyPolymorphic?: T;
|
||||
relationshipDrawerWithAllowCreateFalse?: T;
|
||||
relationshipDrawerWithFilterOptions?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,10 @@ export const PostsCollection: CollectionConfig = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'defaultTextField',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -213,7 +213,37 @@ test.describe('Form State', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('new rows should contain default values', async () => {
|
||||
test('should not render stale values for server components while form state is in flight', async () => {
|
||||
await page.goto(postsUrl.create)
|
||||
|
||||
await page.locator('#field-array .array-field__add-row').click()
|
||||
await page.locator('#field-array #array-row-0 #field-array__0__customTextField').fill('1')
|
||||
|
||||
await page.locator('#field-array .array-field__add-row').click()
|
||||
await page.locator('#field-array #array-row-1 #field-array__1__customTextField').fill('2')
|
||||
|
||||
// block the next form state request from firing to ensure the field remains in stale state
|
||||
await page.route(postsUrl.create, async (route) => {
|
||||
if (route.request().method() === 'POST' && route.request().url() === postsUrl.create) {
|
||||
await route.abort()
|
||||
}
|
||||
|
||||
await route.continue()
|
||||
})
|
||||
|
||||
// remove the first row
|
||||
await page.locator('#field-array #array-row-0 .array-actions__button').click()
|
||||
|
||||
await page
|
||||
.locator('#field-array #array-row-0 .array-actions__action.array-actions__remove')
|
||||
.click()
|
||||
|
||||
await expect(
|
||||
page.locator('#field-array #array-row-0 #field-array__0__customTextField'),
|
||||
).toHaveValue('2')
|
||||
})
|
||||
|
||||
test('should queue onChange functions', async () => {
|
||||
await page.goto(postsUrl.create)
|
||||
await page.locator('#field-array .array-field__add-row').click()
|
||||
await expect(
|
||||
|
||||
@@ -144,6 +144,7 @@ export interface Post {
|
||||
array?:
|
||||
| {
|
||||
customTextField?: string | null;
|
||||
defaultTextField?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
@@ -254,6 +255,7 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
| T
|
||||
| {
|
||||
customTextField?: T;
|
||||
defaultTextField?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { NotFound, type Payload } from 'payload'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -300,8 +301,8 @@ describe('@payloadcms/plugin-search', () => {
|
||||
collection: 'search',
|
||||
depth: 0,
|
||||
where: {
|
||||
'doc.value': {
|
||||
equals: page.id,
|
||||
id: {
|
||||
equals: results[0].id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -24,9 +24,9 @@ export default buildConfigWithDefaults({
|
||||
// },
|
||||
access: {
|
||||
read: ({ req: { user } }) =>
|
||||
user ? !user?.roles?.some((role) => role === 'anonymous') : false,
|
||||
user ? user && !user?.roles?.some((role) => role === 'anonymous') : false,
|
||||
update: ({ req: { user } }) =>
|
||||
user ? !user?.roles?.some((role) => role === 'anonymous') : false,
|
||||
user ? user && !user?.roles?.some((role) => role === 'anonymous') : false,
|
||||
},
|
||||
constraints: {
|
||||
read: [
|
||||
@@ -40,6 +40,11 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Noone',
|
||||
value: 'noone',
|
||||
access: () => false,
|
||||
},
|
||||
],
|
||||
update: [
|
||||
{
|
||||
|
||||
@@ -503,6 +503,42 @@ describe('Query Presets', () => {
|
||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should respect boolean access control results', async () => {
|
||||
// create a preset with the read constraint set to "noone"
|
||||
const presetForNoone = await payload.create({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
user,
|
||||
data: {
|
||||
relatedCollection: 'pages',
|
||||
title: 'Noone',
|
||||
where: {
|
||||
text: {
|
||||
equals: 'example page',
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: {
|
||||
constraint: 'noone',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const foundPresetWithUser1 = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user,
|
||||
overrideAccess: false,
|
||||
id: presetForNoone.id,
|
||||
})
|
||||
|
||||
expect(foundPresetWithUser1).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('Not Found')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('should disable query presets when "enabledQueryPresets" is not true on the collection', async () => {
|
||||
|
||||
@@ -86,7 +86,7 @@ export interface Config {
|
||||
'payload-query-presets': PayloadQueryPresetsSelect<false> | PayloadQueryPresetsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: number;
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
@@ -122,7 +122,7 @@ export interface UserAuthOperations {
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: number;
|
||||
id: string;
|
||||
text?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -133,7 +133,7 @@ export interface Page {
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: number;
|
||||
id: string;
|
||||
name?: string | null;
|
||||
roles?: ('admin' | 'user' | 'anonymous')[] | null;
|
||||
updatedAt: string;
|
||||
@@ -152,7 +152,7 @@ export interface User {
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: number;
|
||||
id: string;
|
||||
text?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -163,24 +163,24 @@ export interface Post {
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: number;
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: number | Page;
|
||||
value: string | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
value: string | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: number | Post;
|
||||
value: string | Post;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -190,10 +190,10 @@ export interface PayloadLockedDocument {
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: number;
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
@@ -213,7 +213,7 @@ export interface PayloadPreference {
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: number;
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
@@ -224,23 +224,23 @@ export interface PayloadMigration {
|
||||
* via the `definition` "payload-query-presets".
|
||||
*/
|
||||
export interface PayloadQueryPreset {
|
||||
id: number;
|
||||
id: string;
|
||||
title: string;
|
||||
isShared?: boolean | null;
|
||||
access?: {
|
||||
read?: {
|
||||
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null;
|
||||
users?: (number | User)[] | null;
|
||||
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles' | 'noone') | null;
|
||||
users?: (string | User)[] | null;
|
||||
roles?: ('admin' | 'user' | 'anonymous')[] | null;
|
||||
};
|
||||
update?: {
|
||||
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null;
|
||||
users?: (number | User)[] | null;
|
||||
users?: (string | User)[] | null;
|
||||
roles?: ('admin' | 'user' | 'anonymous')[] | null;
|
||||
};
|
||||
delete?: {
|
||||
constraint?: ('everyone' | 'onlyMe' | 'specificUsers') | null;
|
||||
users?: (number | User)[] | null;
|
||||
users?: (string | User)[] | null;
|
||||
};
|
||||
};
|
||||
where?:
|
||||
|
||||
@@ -167,6 +167,21 @@ export const seed = async (_payload: Payload) => {
|
||||
overrideAccess: false,
|
||||
data: seedData.onlyMe,
|
||||
}),
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: 'payload-query-presets',
|
||||
user: devUser,
|
||||
overrideAccess: false,
|
||||
data: {
|
||||
relatedCollection: 'pages',
|
||||
title: 'Noone',
|
||||
access: {
|
||||
read: {
|
||||
constraint: 'noone',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ export const DraftsCollection: CollectionConfig = {
|
||||
admin: {
|
||||
useAsTitle: 'text',
|
||||
},
|
||||
orderable: true,
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
|
||||
@@ -82,19 +82,15 @@ describe('Sort functionality', () => {
|
||||
await page.getByText('Join A').click()
|
||||
await expect(page.locator('.sort-header button')).toHaveCount(2)
|
||||
|
||||
await page.locator('.sort-header button').nth(0).click()
|
||||
await assertRows(0, 'A', 'B', 'C', 'D')
|
||||
await moveRow(2, 3, 'success', 0) // move to middle
|
||||
await assertRows(0, 'A', 'C', 'B', 'D')
|
||||
|
||||
await page.locator('.sort-header button').nth(1).click()
|
||||
await assertRows(1, 'A', 'B', 'C', 'D')
|
||||
await moveRow(1, 4, 'success', 1) // move to end
|
||||
await assertRows(1, 'B', 'C', 'D', 'A')
|
||||
|
||||
await page.reload()
|
||||
await page.locator('.sort-header button').nth(0).click()
|
||||
await page.locator('.sort-header button').nth(1).click()
|
||||
await assertRows(0, 'A', 'C', 'B', 'D')
|
||||
await assertRows(1, 'B', 'C', 'D', 'A')
|
||||
})
|
||||
|
||||
@@ -4,9 +4,10 @@ import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
import type { Orderable, OrderableJoin } from './payload-types.js'
|
||||
import type { Draft, Orderable, OrderableJoin } from './payload-types.js'
|
||||
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { draftsSlug } from './collections/Drafts/index.js'
|
||||
import { orderableSlug } from './collections/Orderable/index.js'
|
||||
import { orderableJoinSlug } from './collections/OrderableJoin/index.js'
|
||||
|
||||
@@ -330,6 +331,121 @@ describe('Sort', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Orderable', () => {
|
||||
let orderable1: Orderable
|
||||
let orderable2: Orderable
|
||||
let orderableDraft1: Draft
|
||||
let orderableDraft2: Draft
|
||||
beforeAll(async () => {
|
||||
orderable1 = await payload.create({
|
||||
collection: orderableSlug,
|
||||
data: {
|
||||
title: 'Orderable 1',
|
||||
},
|
||||
})
|
||||
orderable2 = await payload.create({
|
||||
collection: orderableSlug,
|
||||
data: {
|
||||
title: 'Orderable 2',
|
||||
},
|
||||
})
|
||||
orderableDraft1 = await payload.create({
|
||||
collection: draftsSlug,
|
||||
data: {
|
||||
text: 'Orderable 1',
|
||||
_status: 'draft',
|
||||
},
|
||||
})
|
||||
orderableDraft2 = await payload.create({
|
||||
collection: draftsSlug,
|
||||
data: {
|
||||
text: 'Orderable 2',
|
||||
_status: 'draft',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should set order by default', async () => {
|
||||
const ordered = await payload.find({
|
||||
collection: orderableSlug,
|
||||
where: {
|
||||
title: {
|
||||
contains: 'Orderable ',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(orderable1._order).toBeDefined()
|
||||
expect(orderable2._order).toBeDefined()
|
||||
expect(parseInt(orderable1._order, 16)).toBeLessThan(parseInt(orderable2._order, 16))
|
||||
expect(ordered.docs[0].id).toStrictEqual(orderable1.id)
|
||||
expect(ordered.docs[1].id).toStrictEqual(orderable2.id)
|
||||
})
|
||||
|
||||
it('should allow reordering with REST API', async () => {
|
||||
const res = await restClient.POST('/reorder', {
|
||||
body: JSON.stringify({
|
||||
collectionSlug: orderableSlug,
|
||||
docsToMove: [orderable1.id],
|
||||
newKeyWillBe: 'greater',
|
||||
orderableFieldName: '_order',
|
||||
target: {
|
||||
id: orderable2.id,
|
||||
key: orderable2._order,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(res.status).toStrictEqual(200)
|
||||
|
||||
const ordered = await payload.find({
|
||||
collection: 'orderable',
|
||||
where: {
|
||||
title: {
|
||||
contains: 'Orderable ',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(parseInt(ordered.docs[0]._order, 16)).toBeLessThan(
|
||||
parseInt(ordered.docs[1]._order, 16),
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow reordering with REST API with drafts enabled', async () => {
|
||||
const res = await restClient.POST('/reorder', {
|
||||
body: JSON.stringify({
|
||||
collectionSlug: draftsSlug,
|
||||
docsToMove: [orderableDraft1.id],
|
||||
newKeyWillBe: 'greater',
|
||||
orderableFieldName: '_order',
|
||||
target: {
|
||||
id: orderableDraft2.id,
|
||||
key: orderableDraft2._order,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(res.status).toStrictEqual(200)
|
||||
|
||||
const ordered = await payload.find({
|
||||
collection: draftsSlug,
|
||||
draft: true,
|
||||
where: {
|
||||
text: {
|
||||
contains: 'Orderable ',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(ordered.docs).toHaveLength(2)
|
||||
|
||||
expect(parseInt(ordered.docs[0]._order, 16)).toBeLessThan(
|
||||
parseInt(ordered.docs[1]._order, 16),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Orderable join', () => {
|
||||
let related: OrderableJoin
|
||||
let orderable1: Orderable
|
||||
|
||||
32
test/versions/collections/AutosaveWithDraftButton.ts
Normal file
32
test/versions/collections/AutosaveWithDraftButton.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { autosaveWithDraftButtonSlug } from '../slugs.js'
|
||||
|
||||
const AutosaveWithDraftButtonPosts: CollectionConfig = {
|
||||
slug: autosaveWithDraftButtonSlug,
|
||||
labels: {
|
||||
singular: 'Autosave with Draft Button Post',
|
||||
plural: 'Autosave with Draft Button Posts',
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'subtitle', 'createdAt', '_status'],
|
||||
},
|
||||
versions: {
|
||||
drafts: {
|
||||
autosave: {
|
||||
showSaveDraftButton: true,
|
||||
interval: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default AutosaveWithDraftButtonPosts
|
||||
@@ -4,6 +4,7 @@ const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import AutosavePosts from './collections/Autosave.js'
|
||||
import AutosaveWithDraftButtonPosts from './collections/AutosaveWithDraftButton.js'
|
||||
import AutosaveWithValidate from './collections/AutosaveWithValidate.js'
|
||||
import CustomIDs from './collections/CustomIDs.js'
|
||||
import { Diff } from './collections/Diff/index.js'
|
||||
@@ -17,6 +18,7 @@ import Posts from './collections/Posts.js'
|
||||
import { TextCollection } from './collections/Text.js'
|
||||
import VersionPosts from './collections/Versions.js'
|
||||
import AutosaveGlobal from './globals/Autosave.js'
|
||||
import AutosaveWithDraftButtonGlobal from './globals/AutosaveWithDraftButton.js'
|
||||
import DisablePublishGlobal from './globals/DisablePublish.js'
|
||||
import DraftGlobal from './globals/Draft.js'
|
||||
import DraftWithMaxGlobal from './globals/DraftWithMax.js'
|
||||
@@ -35,6 +37,7 @@ export default buildConfigWithDefaults({
|
||||
DisablePublish,
|
||||
Posts,
|
||||
AutosavePosts,
|
||||
AutosaveWithDraftButtonPosts,
|
||||
AutosaveWithValidate,
|
||||
DraftPosts,
|
||||
DraftWithMax,
|
||||
@@ -46,7 +49,14 @@ export default buildConfigWithDefaults({
|
||||
TextCollection,
|
||||
Media,
|
||||
],
|
||||
globals: [AutosaveGlobal, DraftGlobal, DraftWithMaxGlobal, DisablePublishGlobal, LocalizedGlobal],
|
||||
globals: [
|
||||
AutosaveGlobal,
|
||||
AutosaveWithDraftButtonGlobal,
|
||||
DraftGlobal,
|
||||
DraftWithMaxGlobal,
|
||||
DisablePublishGlobal,
|
||||
LocalizedGlobal,
|
||||
],
|
||||
indexSortableFields: true,
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
|
||||
@@ -48,6 +48,8 @@ import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import {
|
||||
autosaveCollectionSlug,
|
||||
autoSaveGlobalSlug,
|
||||
autosaveWithDraftButtonGlobal,
|
||||
autosaveWithDraftButtonSlug,
|
||||
autosaveWithValidateCollectionSlug,
|
||||
customIDSlug,
|
||||
diffCollectionSlug,
|
||||
@@ -78,6 +80,7 @@ describe('Versions', () => {
|
||||
let url: AdminUrlUtil
|
||||
let serverURL: string
|
||||
let autosaveURL: AdminUrlUtil
|
||||
let autosaveWithDraftButtonURL: AdminUrlUtil
|
||||
let autosaveWithValidateURL: AdminUrlUtil
|
||||
let draftWithValidateURL: AdminUrlUtil
|
||||
let disablePublishURL: AdminUrlUtil
|
||||
@@ -116,6 +119,7 @@ describe('Versions', () => {
|
||||
beforeAll(() => {
|
||||
url = new AdminUrlUtil(serverURL, draftCollectionSlug)
|
||||
autosaveURL = new AdminUrlUtil(serverURL, autosaveCollectionSlug)
|
||||
autosaveWithDraftButtonURL = new AdminUrlUtil(serverURL, autosaveWithDraftButtonSlug)
|
||||
autosaveWithValidateURL = new AdminUrlUtil(serverURL, autosaveWithValidateCollectionSlug)
|
||||
disablePublishURL = new AdminUrlUtil(serverURL, disablePublishSlug)
|
||||
customIDURL = new AdminUrlUtil(serverURL, customIDSlug)
|
||||
@@ -201,78 +205,6 @@ describe('Versions', () => {
|
||||
await expect(page.locator('#field-title')).toHaveValue('v1')
|
||||
})
|
||||
|
||||
test('should show global versions view level action in globals versions view', async () => {
|
||||
const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
|
||||
await page.goto(`${global.global(draftGlobalSlug)}/versions`)
|
||||
await expect(page.locator('.app-header .global-versions-button')).toHaveCount(1)
|
||||
})
|
||||
|
||||
// TODO: Check versions/:version-id view for collections / globals
|
||||
|
||||
test('global — has versions tab', async () => {
|
||||
const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
|
||||
await page.goto(global.global(draftGlobalSlug))
|
||||
|
||||
const docURL = page.url()
|
||||
const pathname = new URL(docURL).pathname
|
||||
|
||||
const versionsTab = page.locator('.doc-tab', {
|
||||
hasText: 'Versions',
|
||||
})
|
||||
await versionsTab.waitFor({ state: 'visible' })
|
||||
|
||||
expect(versionsTab).toBeTruthy()
|
||||
const href = versionsTab.locator('a').first()
|
||||
await expect(href).toHaveAttribute('href', `${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, autoSaveGlobalSlug)
|
||||
const versionsURL = `${global.global(autoSaveGlobalSlug)}/versions`
|
||||
await page.goto(versionsURL)
|
||||
await expect(() => {
|
||||
expect(page.url()).toMatch(/\/versions/)
|
||||
}).toPass({ timeout: 10000, intervals: [100] })
|
||||
})
|
||||
|
||||
test('collection - should autosave', async () => {
|
||||
await page.goto(autosaveURL.create)
|
||||
await page.locator('#field-title').fill('autosave title')
|
||||
@@ -309,6 +241,16 @@ describe('Versions', () => {
|
||||
await expect(drawer.locator('.id-label')).toBeVisible()
|
||||
})
|
||||
|
||||
test('collection - should show "save as draft" button when showSaveDraftButton is true', async () => {
|
||||
await page.goto(autosaveWithDraftButtonURL.create)
|
||||
await expect(page.locator('#action-save-draft')).toBeVisible()
|
||||
})
|
||||
|
||||
test('collection - should not show "save as draft" button when showSaveDraftButton is false', async () => {
|
||||
await page.goto(autosaveURL.create)
|
||||
await expect(page.locator('#action-save-draft')).toBeHidden()
|
||||
})
|
||||
|
||||
test('collection - autosave - should not create duplicates when clicking Create new', async () => {
|
||||
// This test checks that when we click "Create new" in the list view, it only creates 1 extra document and not more
|
||||
const { totalDocs: initialDocsCount } = await payload.find({
|
||||
@@ -402,17 +344,6 @@ describe('Versions', () => {
|
||||
await expect(newUpdatedAt).not.toHaveText(initialUpdatedAt)
|
||||
})
|
||||
|
||||
test('global - should autosave', async () => {
|
||||
const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug)
|
||||
await page.goto(url.global(autoSaveGlobalSlug))
|
||||
const titleField = page.locator('#field-title')
|
||||
await titleField.fill('global title')
|
||||
await waitForAutoSaveToRunAndComplete(page)
|
||||
await expect(titleField).toHaveValue('global title')
|
||||
await page.goto(url.global(autoSaveGlobalSlug))
|
||||
await expect(page.locator('#field-title')).toHaveValue('global title')
|
||||
})
|
||||
|
||||
test('should retain localized data during autosave', async () => {
|
||||
const en = 'en'
|
||||
const es = 'es'
|
||||
@@ -519,12 +450,6 @@ describe('Versions', () => {
|
||||
await expect(page.locator('#field-title')).toHaveValue('title')
|
||||
})
|
||||
|
||||
test('globals — should hide publish button when access control prevents update', async () => {
|
||||
const url = new AdminUrlUtil(serverURL, disablePublishGlobalSlug)
|
||||
await page.goto(url.global(disablePublishGlobalSlug))
|
||||
await expect(page.locator('#action-save')).not.toBeAttached()
|
||||
})
|
||||
|
||||
test('collections — should hide publish button when access control prevents create', async () => {
|
||||
await page.goto(disablePublishURL.create)
|
||||
await expect(page.locator('#action-save')).not.toBeAttached()
|
||||
@@ -652,6 +577,107 @@ describe('Versions', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('draft globals', () => {
|
||||
test('should show global versions view level action in globals versions view', async () => {
|
||||
const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
|
||||
await page.goto(`${global.global(draftGlobalSlug)}/versions`)
|
||||
await expect(page.locator('.app-header .global-versions-button')).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('global — has versions tab', async () => {
|
||||
const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
|
||||
await page.goto(global.global(draftGlobalSlug))
|
||||
|
||||
const docURL = page.url()
|
||||
const pathname = new URL(docURL).pathname
|
||||
|
||||
const versionsTab = page.locator('.doc-tab', {
|
||||
hasText: 'Versions',
|
||||
})
|
||||
await versionsTab.waitFor({ state: 'visible' })
|
||||
|
||||
expect(versionsTab).toBeTruthy()
|
||||
const href = versionsTab.locator('a').first()
|
||||
await expect(href).toHaveAttribute('href', `${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, autoSaveGlobalSlug)
|
||||
const versionsURL = `${global.global(autoSaveGlobalSlug)}/versions`
|
||||
await page.goto(versionsURL)
|
||||
await expect(() => {
|
||||
expect(page.url()).toMatch(/\/versions/)
|
||||
}).toPass({ timeout: 10000, intervals: [100] })
|
||||
})
|
||||
|
||||
test('global - should show "save as draft" button when showSaveDraftButton is true', async () => {
|
||||
const url = new AdminUrlUtil(serverURL, autosaveWithDraftButtonGlobal)
|
||||
await page.goto(url.global(autosaveWithDraftButtonGlobal))
|
||||
await expect(page.locator('#action-save-draft')).toBeVisible()
|
||||
})
|
||||
|
||||
test('global - should not show "save as draft" button when showSaveDraftButton is false', async () => {
|
||||
const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug)
|
||||
await page.goto(url.global(autoSaveGlobalSlug))
|
||||
await expect(page.locator('#action-save-draft')).toBeHidden()
|
||||
})
|
||||
|
||||
test('global - should autosave', async () => {
|
||||
const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug)
|
||||
await page.goto(url.global(autoSaveGlobalSlug))
|
||||
const titleField = page.locator('#field-title')
|
||||
await titleField.fill('global title')
|
||||
await waitForAutoSaveToRunAndComplete(page)
|
||||
await expect(titleField).toHaveValue('global title')
|
||||
await page.goto(url.global(autoSaveGlobalSlug))
|
||||
await expect(page.locator('#field-title')).toHaveValue('global title')
|
||||
})
|
||||
|
||||
test('globals — should hide publish button when access control prevents update', async () => {
|
||||
const url = new AdminUrlUtil(serverURL, disablePublishGlobalSlug)
|
||||
await page.goto(url.global(disablePublishGlobalSlug))
|
||||
await expect(page.locator('#action-save')).not.toBeAttached()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Scheduled publish', () => {
|
||||
beforeAll(() => {
|
||||
url = new AdminUrlUtil(serverURL, draftCollectionSlug)
|
||||
|
||||
26
test/versions/globals/AutosaveWithDraftButton.ts
Normal file
26
test/versions/globals/AutosaveWithDraftButton.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
import { autosaveWithDraftButtonGlobal } from '../slugs.js'
|
||||
|
||||
const AutosaveWithDraftButtonGlobal: GlobalConfig = {
|
||||
slug: autosaveWithDraftButtonGlobal,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
label: 'Autosave with Draft Button Global',
|
||||
versions: {
|
||||
drafts: {
|
||||
autosave: {
|
||||
showSaveDraftButton: true,
|
||||
interval: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default AutosaveWithDraftButtonGlobal
|
||||
@@ -70,6 +70,7 @@ export interface Config {
|
||||
'disable-publish': DisablePublish;
|
||||
posts: Post;
|
||||
'autosave-posts': AutosavePost;
|
||||
'autosave-with-draft-button-posts': AutosaveWithDraftButtonPost;
|
||||
'autosave-with-validate-posts': AutosaveWithValidatePost;
|
||||
'draft-posts': DraftPost;
|
||||
'draft-with-max-posts': DraftWithMaxPost;
|
||||
@@ -91,6 +92,7 @@ export interface Config {
|
||||
'disable-publish': DisablePublishSelect<false> | DisablePublishSelect<true>;
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
'autosave-posts': AutosavePostsSelect<false> | AutosavePostsSelect<true>;
|
||||
'autosave-with-draft-button-posts': AutosaveWithDraftButtonPostsSelect<false> | AutosaveWithDraftButtonPostsSelect<true>;
|
||||
'autosave-with-validate-posts': AutosaveWithValidatePostsSelect<false> | AutosaveWithValidatePostsSelect<true>;
|
||||
'draft-posts': DraftPostsSelect<false> | DraftPostsSelect<true>;
|
||||
'draft-with-max-posts': DraftWithMaxPostsSelect<false> | DraftWithMaxPostsSelect<true>;
|
||||
@@ -112,6 +114,7 @@ export interface Config {
|
||||
};
|
||||
globals: {
|
||||
'autosave-global': AutosaveGlobal;
|
||||
'autosave-with-draft-button-global': AutosaveWithDraftButtonGlobal;
|
||||
'draft-global': DraftGlobal;
|
||||
'draft-with-max-global': DraftWithMaxGlobal;
|
||||
'disable-publish-global': DisablePublishGlobal;
|
||||
@@ -119,6 +122,7 @@ export interface Config {
|
||||
};
|
||||
globalsSelect: {
|
||||
'autosave-global': AutosaveGlobalSelect<false> | AutosaveGlobalSelect<true>;
|
||||
'autosave-with-draft-button-global': AutosaveWithDraftButtonGlobalSelect<false> | AutosaveWithDraftButtonGlobalSelect<true>;
|
||||
'draft-global': DraftGlobalSelect<false> | DraftGlobalSelect<true>;
|
||||
'draft-with-max-global': DraftWithMaxGlobalSelect<false> | DraftWithMaxGlobalSelect<true>;
|
||||
'disable-publish-global': DisablePublishGlobalSelect<false> | DisablePublishGlobalSelect<true>;
|
||||
@@ -228,6 +232,17 @@ export interface DraftPost {
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "autosave-with-draft-button-posts".
|
||||
*/
|
||||
export interface AutosaveWithDraftButtonPost {
|
||||
id: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "autosave-with-validate-posts".
|
||||
@@ -554,6 +569,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'autosave-posts';
|
||||
value: string | AutosavePost;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'autosave-with-draft-button-posts';
|
||||
value: string | AutosaveWithDraftButtonPost;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'autosave-with-validate-posts';
|
||||
value: string | AutosaveWithValidatePost;
|
||||
@@ -676,6 +695,16 @@ export interface AutosavePostsSelect<T extends boolean = true> {
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "autosave-with-draft-button-posts_select".
|
||||
*/
|
||||
export interface AutosaveWithDraftButtonPostsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "autosave-with-validate-posts_select".
|
||||
@@ -973,6 +1002,17 @@ export interface AutosaveGlobal {
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "autosave-with-draft-button-global".
|
||||
*/
|
||||
export interface AutosaveWithDraftButtonGlobal {
|
||||
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` "draft-global".
|
||||
@@ -1029,6 +1069,17 @@ export interface AutosaveGlobalSelect<T extends boolean = true> {
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "autosave-with-draft-button-global_select".
|
||||
*/
|
||||
export interface AutosaveWithDraftButtonGlobalSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
_status?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "draft-global_select".
|
||||
@@ -1082,10 +1133,15 @@ export interface TaskSchedulePublish {
|
||||
input: {
|
||||
type?: ('publish' | 'unpublish') | null;
|
||||
locale?: string | null;
|
||||
doc?: {
|
||||
relationTo: 'draft-posts';
|
||||
value: string | DraftPost;
|
||||
} | null;
|
||||
doc?:
|
||||
| ({
|
||||
relationTo: 'autosave-posts';
|
||||
value: string | AutosavePost;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'draft-posts';
|
||||
value: string | DraftPost;
|
||||
} | null);
|
||||
global?: 'draft-global' | null;
|
||||
user?: (string | null) | User;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export const autosaveCollectionSlug = 'autosave-posts'
|
||||
|
||||
export const autosaveWithDraftButtonSlug = 'autosave-with-draft-button-posts'
|
||||
|
||||
export const autosaveWithValidateCollectionSlug = 'autosave-with-validate-posts'
|
||||
|
||||
export const customIDSlug = 'custom-ids'
|
||||
@@ -33,7 +35,11 @@ export const collectionSlugs = [
|
||||
]
|
||||
|
||||
export const autoSaveGlobalSlug = 'autosave-global'
|
||||
|
||||
export const autosaveWithDraftButtonGlobal = 'autosave-with-draft-button-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