test: dedicated bulk edit test suite (#11756)
Consolidates all bulk edit related tests into a single, dedicated suite. Currently, bulk edit tests are dispersed throughout the Admin > General and the Versions test suites, which are considerably bloated for their own purposes. This made them very hard to locate, mentally digest, and add on new tests. Going forward, many more tests specifically for bulk edit will need to be written. This gives us a simple, isolated place for that. With this change are also a few improvements to the tests themselves to make them more predictable and efficient.
This commit is contained in:
@@ -137,64 +137,6 @@ export const Posts: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'arrayOfFields',
|
||||
type: 'array',
|
||||
admin: {
|
||||
initCollapsed: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'optional',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'innerArrayOfFields',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'innerOptional',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'defaultValueField',
|
||||
type: 'text',
|
||||
defaultValue: 'testing',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'someBlock',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'textBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'textFieldForBlock',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'defaultValueField',
|
||||
type: 'text',
|
||||
defaultValue: 'testing',
|
||||
},
|
||||
{
|
||||
name: 'relationship',
|
||||
type: 'relationship',
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { Config, Geo, Post } from '../../payload-types.js'
|
||||
|
||||
import {
|
||||
ensureCompilationIsDone,
|
||||
exactText,
|
||||
getRoutes,
|
||||
initPageConsoleErrorCatch,
|
||||
saveDocAndAssert,
|
||||
@@ -781,205 +780,6 @@ describe('General', () => {
|
||||
expect(page.url()).toContain(postsUrl.list)
|
||||
})
|
||||
|
||||
test('should bulk delete all on page', async () => {
|
||||
await deleteAllPosts()
|
||||
await Promise.all([createPost(), createPost(), createPost()])
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await page.locator('#delete-posts #confirm-action').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
||||
'Deleted 3 Posts successfully.',
|
||||
)
|
||||
|
||||
// Poll until router has refreshed
|
||||
await expect.poll(() => page.locator('.collection-list__no-results').isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
test('should bulk delete with filters and across pages', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
Array.from({ length: 6 }).forEach(async (_, i) => {
|
||||
await createPost({ title: `Post ${i + 1}` })
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('#search-filter-input').fill('Post')
|
||||
await page.waitForURL(/search=Post/)
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(5)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('button#select-all-across-pages').click()
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await page.locator('#delete-posts #confirm-action').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
||||
'Deleted 6 Posts successfully.',
|
||||
)
|
||||
|
||||
// Poll until router has refreshed
|
||||
await expect.poll(() => page.locator('.table table > tbody > tr').count()).toBe(0)
|
||||
})
|
||||
|
||||
test('should bulk update', async () => {
|
||||
// First, delete all posts created by the seed
|
||||
await deleteAllPosts()
|
||||
const post1Title = 'Post'
|
||||
const updatedPostTitle = `${post1Title} (Updated)`
|
||||
await Promise.all([createPost({ title: post1Title }), createPost(), createPost()])
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
|
||||
const titleOption = page.locator('.field-select .rs__option', {
|
||||
hasText: exactText('Title'),
|
||||
})
|
||||
|
||||
await expect(titleOption).toBeVisible()
|
||||
await titleOption.click()
|
||||
const titleInput = page.locator('#field-title')
|
||||
await expect(titleInput).toBeVisible()
|
||||
await titleInput.fill(updatedPostTitle)
|
||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 3 Posts successfully.',
|
||||
)
|
||||
|
||||
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
|
||||
await expect(page.locator('.row-2 .cell-title')).toContainText(updatedPostTitle)
|
||||
await expect(page.locator('.row-3 .cell-title')).toContainText(updatedPostTitle)
|
||||
})
|
||||
|
||||
test('should not override un-edited values in bulk edit if it has a defaultValue', async () => {
|
||||
await deleteAllPosts()
|
||||
const post1Title = 'Post'
|
||||
|
||||
const postData = {
|
||||
title: 'Post',
|
||||
arrayOfFields: [
|
||||
{
|
||||
optional: 'some optional array field',
|
||||
innerArrayOfFields: [
|
||||
{
|
||||
innerOptional: 'some inner optional array field',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
group: {
|
||||
defaultValueField: 'not the group default value',
|
||||
title: 'some title',
|
||||
},
|
||||
someBlock: [
|
||||
{
|
||||
textFieldForBlock: 'some text for block text',
|
||||
blockType: 'textBlock',
|
||||
},
|
||||
],
|
||||
defaultValueField: 'not the default value',
|
||||
}
|
||||
|
||||
const updatedPostTitle = `${post1Title} (Updated)`
|
||||
await createPost(postData)
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
|
||||
const titleOption = page.locator('.field-select .rs__option', {
|
||||
hasText: exactText('Title'),
|
||||
})
|
||||
|
||||
await titleOption.click()
|
||||
const titleInput = page.locator('#field-title')
|
||||
await titleInput.fill(updatedPostTitle)
|
||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 1 Post successfully.',
|
||||
)
|
||||
|
||||
const updatedPost = await payload.find({
|
||||
collection: 'posts',
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
expect(updatedPost.docs[0].title).toBe(updatedPostTitle)
|
||||
expect(updatedPost.docs[0].arrayOfFields.length).toBe(1)
|
||||
expect(updatedPost.docs[0].arrayOfFields[0].optional).toBe('some optional array field')
|
||||
expect(updatedPost.docs[0].arrayOfFields[0].innerArrayOfFields.length).toBe(1)
|
||||
expect(updatedPost.docs[0].someBlock[0].textFieldForBlock).toBe('some text for block text')
|
||||
expect(updatedPost.docs[0].defaultValueField).toBe('not the default value')
|
||||
})
|
||||
|
||||
test('should not show "select all across pages" button if already selected all', async () => {
|
||||
await deleteAllPosts()
|
||||
await createPost({ title: `Post 1` })
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await expect(page.locator('button#select-all-across-pages')).toBeHidden()
|
||||
})
|
||||
|
||||
test('should bulk update with filters and across pages', async () => {
|
||||
// First, delete all posts created by the seed
|
||||
await deleteAllPosts()
|
||||
|
||||
Array.from({ length: 6 }).forEach(async (_, i) => {
|
||||
await createPost({ title: `Post ${i + 1}` })
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('#search-filter-input').fill('Post')
|
||||
await page.waitForURL(/search=Post/)
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(5)
|
||||
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('button#select-all-across-pages').click()
|
||||
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
|
||||
const titleOption = page.locator('.field-select .rs__option', {
|
||||
hasText: exactText('Title'),
|
||||
})
|
||||
|
||||
await expect(titleOption).toBeVisible()
|
||||
await titleOption.click()
|
||||
const titleInput = page.locator('#field-title')
|
||||
await expect(titleInput).toBeVisible()
|
||||
const updatedTitle = `Post (Updated)`
|
||||
await titleInput.fill(updatedTitle)
|
||||
|
||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 6 Posts successfully.',
|
||||
)
|
||||
|
||||
// Poll until router has refreshed
|
||||
await expect.poll(() => page.locator('.table table > tbody > tr').count()).toBe(5)
|
||||
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedTitle)
|
||||
})
|
||||
|
||||
test('should update selection state after deselecting item following select all', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
Array.from({ length: 6 }).forEach(async (_, i) => {
|
||||
await createPost({ title: `Post ${i + 1}` })
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('button#select-all-across-pages').click()
|
||||
|
||||
// Deselect the first row
|
||||
await page.locator('.row-1 input').click()
|
||||
|
||||
// eslint-disable-next-line jest-dom/prefer-checked
|
||||
await expect(page.locator('input#select-all')).not.toHaveAttribute('checked', '')
|
||||
})
|
||||
|
||||
test('should save globals', async () => {
|
||||
await page.goto(postsUrl.global(globalSlug))
|
||||
|
||||
@@ -1079,10 +879,6 @@ describe('General', () => {
|
||||
})
|
||||
})
|
||||
|
||||
async function deleteAllPosts() {
|
||||
await payload.delete({ collection: postsCollectionSlug, where: { id: { exists: true } } })
|
||||
}
|
||||
|
||||
async function createPost(overrides?: Partial<Post>): Promise<Post> {
|
||||
return payload.create({
|
||||
collection: postsCollectionSlug,
|
||||
|
||||
@@ -54,6 +54,7 @@ export type SupportedTimezones =
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
@@ -232,31 +233,6 @@ export interface Post {
|
||||
[k: string]: unknown;
|
||||
}[]
|
||||
| null;
|
||||
arrayOfFields?:
|
||||
| {
|
||||
optional?: string | null;
|
||||
innerArrayOfFields?:
|
||||
| {
|
||||
innerOptional?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
group?: {
|
||||
defaultValueField?: string | null;
|
||||
title?: string | null;
|
||||
};
|
||||
someBlock?:
|
||||
| {
|
||||
textFieldForBlock?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'textBlock';
|
||||
}[]
|
||||
| null;
|
||||
defaultValueField?: string | null;
|
||||
relationship?: (string | null) | Post;
|
||||
users?: (string | null) | User;
|
||||
customCell?: string | null;
|
||||
@@ -676,36 +652,6 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
description?: T;
|
||||
number?: T;
|
||||
richText?: T;
|
||||
arrayOfFields?:
|
||||
| T
|
||||
| {
|
||||
optional?: T;
|
||||
innerArrayOfFields?:
|
||||
| T
|
||||
| {
|
||||
innerOptional?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
group?:
|
||||
| T
|
||||
| {
|
||||
defaultValueField?: T;
|
||||
title?: T;
|
||||
};
|
||||
someBlock?:
|
||||
| T
|
||||
| {
|
||||
textBlock?:
|
||||
| T
|
||||
| {
|
||||
textFieldForBlock?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
};
|
||||
defaultValueField?: T;
|
||||
relationship?: T;
|
||||
users?: T;
|
||||
customCell?: T;
|
||||
|
||||
2
test/bulk-edit/.gitignore
vendored
Normal file
2
test/bulk-edit/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/media
|
||||
/media-gif
|
||||
86
test/bulk-edit/collections/Posts/index.ts
Normal file
86
test/bulk-edit/collections/Posts/index.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { postsSlug } from '../../shared.js'
|
||||
|
||||
export const PostsCollection: CollectionConfig = {
|
||||
slug: postsSlug,
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['id', 'title', 'description', '_status'],
|
||||
pagination: {
|
||||
defaultLimit: 5,
|
||||
limits: [5, 10, 15],
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
name: 'defaultValueField',
|
||||
type: 'text',
|
||||
defaultValue: 'This is a default value',
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'defaultValueField',
|
||||
type: 'text',
|
||||
defaultValue: 'This is a default value',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
admin: {
|
||||
initCollapsed: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'optional',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'innerArrayOfFields',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'innerOptional',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'textBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'textFieldForBlock',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
38
test/bulk-edit/config.ts
Normal file
38
test/bulk-edit/config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { PostsCollection } from './collections/Posts/index.js'
|
||||
import { postsSlug } from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [PostsCollection],
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
title: 'example post',
|
||||
},
|
||||
})
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
444
test/bulk-edit/e2e.spec.ts
Normal file
444
test/bulk-edit/e2e.spec.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
import type { BrowserContext, Page } from '@playwright/test'
|
||||
import type { PayloadTestSDK } from 'helpers/sdk/index.js'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { Config, Post } from './payload-types.js'
|
||||
|
||||
import {
|
||||
ensureCompilationIsDone,
|
||||
exactText,
|
||||
findTableCell,
|
||||
initPageConsoleErrorCatch,
|
||||
selectTableRow,
|
||||
// throttleTest,
|
||||
} from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import { postsSlug } from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
let context: BrowserContext
|
||||
let payload: PayloadTestSDK<Config>
|
||||
let serverURL: string
|
||||
|
||||
test.describe('Bulk Edit', () => {
|
||||
let page: Page
|
||||
let postsUrl: AdminUrlUtil
|
||||
|
||||
test.beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))
|
||||
postsUrl = new AdminUrlUtil(serverURL, postsSlug)
|
||||
|
||||
context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
await ensureCompilationIsDone({ page, serverURL })
|
||||
})
|
||||
|
||||
test.beforeEach(async () => {
|
||||
// await throttleTest({ page, context, delay: 'Fast 3G' })
|
||||
})
|
||||
|
||||
test('should not show "select all across pages" button if already selected all', async () => {
|
||||
await deleteAllPosts()
|
||||
await createPost({ title: 'Post 1' })
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await expect(page.locator('button#select-all-across-pages')).toBeHidden()
|
||||
})
|
||||
|
||||
test('should update selection state after deselecting item following select all', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
Array.from({ length: 6 }).forEach(async (_, i) => {
|
||||
await createPost({ title: `Post ${i + 1}` })
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('button#select-all-across-pages').click()
|
||||
|
||||
// Deselect the first row
|
||||
await page.locator('.row-1 input').click()
|
||||
|
||||
// eslint-disable-next-line jest-dom/prefer-checked
|
||||
await expect(page.locator('input#select-all')).not.toHaveAttribute('checked', '')
|
||||
})
|
||||
|
||||
test('should delete many', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
const titleOfPostToDelete1 = 'Post to delete (published)'
|
||||
const titleOfPostToDelete2 = 'Post to delete (draft)'
|
||||
|
||||
await Promise.all([
|
||||
createPost({ title: titleOfPostToDelete1 }),
|
||||
createPost({ title: titleOfPostToDelete2 }, { draft: true }),
|
||||
])
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
await expect(page.locator(`tbody tr:has-text("${titleOfPostToDelete1}")`)).toBeVisible()
|
||||
await expect(page.locator(`tbody tr:has-text("${titleOfPostToDelete2}")`)).toBeVisible()
|
||||
|
||||
await selectTableRow(page, titleOfPostToDelete1)
|
||||
await selectTableRow(page, titleOfPostToDelete2)
|
||||
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await page.locator('#delete-posts #confirm-action').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Deleted 2 Posts successfully.',
|
||||
)
|
||||
|
||||
await expect(page.locator(`tbody tr:has-text("${titleOfPostToDelete1}")`)).toBeHidden()
|
||||
await expect(page.locator(`tbody tr:has-text("${titleOfPostToDelete2}")`)).toBeHidden()
|
||||
})
|
||||
|
||||
test('should publish many', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
const titleOfPostToPublish1 = 'Post to publish (already published)'
|
||||
const titleOfPostToPublish2 = 'Post to publish (draft)'
|
||||
|
||||
await Promise.all([
|
||||
createPost({ title: titleOfPostToPublish1 }),
|
||||
createPost({ title: titleOfPostToPublish2 }, { draft: true }),
|
||||
])
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
await expect(page.locator(`tbody tr:has-text("${titleOfPostToPublish1}")`)).toBeVisible()
|
||||
await expect(page.locator(`tbody tr:has-text("${titleOfPostToPublish2}")`)).toBeVisible()
|
||||
|
||||
await selectTableRow(page, titleOfPostToPublish1)
|
||||
await selectTableRow(page, titleOfPostToPublish2)
|
||||
|
||||
await page.locator('.publish-many__toggle').click()
|
||||
await page.locator('#publish-posts #confirm-action').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 2 Posts successfully.',
|
||||
)
|
||||
|
||||
await expect(findTableCell(page, '_status', titleOfPostToPublish1)).toContainText('Published')
|
||||
await expect(findTableCell(page, '_status', titleOfPostToPublish2)).toContainText('Published')
|
||||
})
|
||||
|
||||
test('should unpublish many', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
const titleOfPostToUnpublish1 = 'Post to unpublish (published)'
|
||||
const titleOfPostToUnpublish2 = 'Post to unpublish (already draft)'
|
||||
|
||||
await Promise.all([
|
||||
createPost({ title: titleOfPostToUnpublish1 }),
|
||||
createPost({ title: titleOfPostToUnpublish2 }, { draft: true }),
|
||||
])
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
await expect(page.locator(`tbody tr:has-text("${titleOfPostToUnpublish1}")`)).toBeVisible()
|
||||
await expect(page.locator(`tbody tr:has-text("${titleOfPostToUnpublish2}")`)).toBeVisible()
|
||||
|
||||
await selectTableRow(page, titleOfPostToUnpublish1)
|
||||
await selectTableRow(page, titleOfPostToUnpublish2)
|
||||
|
||||
await page.locator('.unpublish-many__toggle').click()
|
||||
await page.locator('#unpublish-posts #confirm-action').click()
|
||||
|
||||
await expect(findTableCell(page, '_status', titleOfPostToUnpublish1)).toContainText('Draft')
|
||||
await expect(findTableCell(page, '_status', titleOfPostToUnpublish2)).toContainText('Draft')
|
||||
})
|
||||
|
||||
test('should update many', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
const updatedPostTitle = 'Post (Updated)'
|
||||
|
||||
Array.from({ length: 3 }).forEach(async (_, i) => {
|
||||
await createPost({ title: `Post ${i + 1}` })
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const invertedIndex = 4 - i
|
||||
await expect(page.locator(`.row-${invertedIndex} .cell-title`)).toContainText(`Post ${i}`)
|
||||
}
|
||||
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
|
||||
const titleOption = page.locator('.field-select .rs__option', {
|
||||
hasText: exactText('Title'),
|
||||
})
|
||||
|
||||
await expect(titleOption).toBeVisible()
|
||||
await titleOption.click()
|
||||
const titleInput = page.locator('#field-title')
|
||||
await expect(titleInput).toBeVisible()
|
||||
await titleInput.fill(updatedPostTitle)
|
||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 3 Posts successfully.',
|
||||
)
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const invertedIndex = 4 - i
|
||||
await expect(page.locator(`.row-${invertedIndex} .cell-title`)).toContainText(
|
||||
updatedPostTitle,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test('should publish many from drawer', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
const titleOfPostToPublish1 = 'Post to unpublish (published)'
|
||||
const titleOfPostToPublish2 = 'Post to publish (already draft)'
|
||||
|
||||
await Promise.all([
|
||||
createPost({ title: titleOfPostToPublish1 }),
|
||||
createPost({ title: titleOfPostToPublish2 }, { draft: true }),
|
||||
])
|
||||
|
||||
const description = 'published document'
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
await expect(page.locator(`tbody tr:has-text("${titleOfPostToPublish1}")`)).toBeVisible()
|
||||
await expect(page.locator(`tbody tr:has-text("${titleOfPostToPublish2}")`)).toBeVisible()
|
||||
|
||||
await selectTableRow(page, titleOfPostToPublish1)
|
||||
await selectTableRow(page, titleOfPostToPublish2)
|
||||
|
||||
// Bulk edit the selected rows to `published` status
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
const options = page.locator('.rs__option')
|
||||
const field = options.locator('text=Description')
|
||||
await field.click()
|
||||
await page.locator('#field-description').fill(description)
|
||||
await page.locator('.form-submit .edit-many__publish').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 2 Posts successfully.',
|
||||
)
|
||||
|
||||
await expect(findTableCell(page, '_status', titleOfPostToPublish1)).toContainText('Published')
|
||||
await expect(findTableCell(page, '_status', titleOfPostToPublish2)).toContainText('Published')
|
||||
})
|
||||
|
||||
test('should draft many from drawer', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
const titleOfPostToDraft1 = 'Post to draft (published)'
|
||||
const titleOfPostToDraft2 = 'Post to draft (draft)'
|
||||
|
||||
await Promise.all([
|
||||
createPost({ title: titleOfPostToDraft1 }),
|
||||
createPost({ title: titleOfPostToDraft2 }, { draft: true }),
|
||||
])
|
||||
|
||||
const description = 'draft document'
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
await selectTableRow(page, titleOfPostToDraft1)
|
||||
await selectTableRow(page, titleOfPostToDraft2)
|
||||
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
const options = page.locator('.rs__option')
|
||||
const field = options.locator('text=Description')
|
||||
await field.click()
|
||||
await page.locator('#field-description').fill(description)
|
||||
await page.locator('.form-submit .edit-many__draft').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 2 Posts successfully.',
|
||||
)
|
||||
|
||||
await expect(findTableCell(page, '_status', titleOfPostToDraft1)).toContainText('Draft')
|
||||
await expect(findTableCell(page, '_status', titleOfPostToDraft2)).toContainText('Draft')
|
||||
})
|
||||
|
||||
test('should delete all on page', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
Array.from({ length: 3 }).forEach(async (_, i) => {
|
||||
await createPost({ title: `Post ${i + 1}` })
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(3)
|
||||
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await page.locator('#delete-posts #confirm-action').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
||||
'Deleted 3 Posts successfully.',
|
||||
)
|
||||
|
||||
await page.locator('.collection-list__no-results').isVisible()
|
||||
})
|
||||
|
||||
test('should delete all with filters and across pages', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
Array.from({ length: 6 }).forEach(async (_, i) => {
|
||||
await createPost({ title: `Post ${i + 1}` })
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
await expect(page.locator('.collection-list__page-info')).toContainText('1-5 of 6')
|
||||
|
||||
await page.locator('#search-filter-input').fill('Post')
|
||||
await page.waitForURL(/search=Post/)
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(5)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('button#select-all-across-pages').click()
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await page.locator('#delete-posts #confirm-action').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
||||
'Deleted 6 Posts successfully.',
|
||||
)
|
||||
|
||||
await page.locator('.collection-list__no-results').isVisible()
|
||||
})
|
||||
|
||||
test('should update all with filters and across pages', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
Array.from({ length: 6 }).forEach(async (_, i) => {
|
||||
await createPost({ title: `Post ${i + 1}` })
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('#search-filter-input').fill('Post')
|
||||
await page.waitForURL(/search=Post/)
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(5)
|
||||
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('button#select-all-across-pages').click()
|
||||
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
|
||||
const titleOption = page.locator('.field-select .rs__option', {
|
||||
hasText: exactText('Title'),
|
||||
})
|
||||
|
||||
await expect(titleOption).toBeVisible()
|
||||
await titleOption.click()
|
||||
const titleInput = page.locator('#field-title')
|
||||
await expect(titleInput).toBeVisible()
|
||||
const updatedTitle = `Post (Updated)`
|
||||
await titleInput.fill(updatedTitle)
|
||||
|
||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 6 Posts successfully.',
|
||||
)
|
||||
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(5)
|
||||
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedTitle)
|
||||
})
|
||||
|
||||
test('should not override un-edited values if it has a defaultValue', async () => {
|
||||
await deleteAllPosts()
|
||||
|
||||
const postData = {
|
||||
title: 'Post 1',
|
||||
array: [
|
||||
{
|
||||
optional: 'some optional array field',
|
||||
innerArrayOfFields: [
|
||||
{
|
||||
innerOptional: 'some inner optional array field',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
group: {
|
||||
defaultValueField: 'This is NOT the default value',
|
||||
title: 'some title',
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
textFieldForBlock: 'some text for block text',
|
||||
blockType: 'textBlock',
|
||||
},
|
||||
],
|
||||
defaultValueField: 'This is NOT the default value',
|
||||
}
|
||||
|
||||
const updatedPostTitle = 'Post 1 (Updated)'
|
||||
|
||||
const { id: postID } = await createPost(postData)
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
|
||||
const titleOption = page.locator('.field-select .rs__option', {
|
||||
hasText: exactText('Title'),
|
||||
})
|
||||
|
||||
await titleOption.click()
|
||||
const titleInput = page.locator('#field-title')
|
||||
await titleInput.fill(updatedPostTitle)
|
||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 1 Post successfully.',
|
||||
)
|
||||
|
||||
const updatedPost = await payload.find({
|
||||
collection: postsSlug,
|
||||
limit: 1,
|
||||
depth: 0,
|
||||
where: {
|
||||
id: {
|
||||
equals: postID,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(updatedPost.docs[0]).toMatchObject({
|
||||
...postData,
|
||||
title: updatedPostTitle,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function deleteAllPosts() {
|
||||
await payload.delete({ collection: postsSlug, where: { id: { exists: true } } })
|
||||
}
|
||||
|
||||
async function createPost(
|
||||
dataOverrides?: Partial<Post>,
|
||||
overrides?: Record<string, unknown>,
|
||||
): Promise<Post> {
|
||||
return payload.create({
|
||||
collection: postsSlug,
|
||||
...(overrides || {}),
|
||||
data: {
|
||||
title: 'Post Title',
|
||||
...(dataOverrides || {}),
|
||||
},
|
||||
}) as unknown as Promise<Post>
|
||||
}
|
||||
19
test/bulk-edit/eslint.config.js
Normal file
19
test/bulk-edit/eslint.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { rootParserOptions } from '../../eslint.config.js'
|
||||
import { testEslintConfig } from '../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...testEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
327
test/bulk-edit/payload-types.ts
Normal file
327
test/bulk-edit/payload-types.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
posts: Post;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
defaultValueField?: string | null;
|
||||
group?: {
|
||||
defaultValueField?: string | null;
|
||||
title?: string | null;
|
||||
};
|
||||
array?:
|
||||
| {
|
||||
optional?: string | null;
|
||||
innerArrayOfFields?:
|
||||
| {
|
||||
innerOptional?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
blocks?:
|
||||
| {
|
||||
textFieldForBlock?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'textBlock';
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts_select".
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
description?: T;
|
||||
defaultValueField?: T;
|
||||
group?:
|
||||
| T
|
||||
| {
|
||||
defaultValueField?: T;
|
||||
title?: T;
|
||||
};
|
||||
array?:
|
||||
| T
|
||||
| {
|
||||
optional?: T;
|
||||
innerArrayOfFields?:
|
||||
| T
|
||||
| {
|
||||
innerOptional?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
blocks?:
|
||||
| T
|
||||
| {
|
||||
textBlock?:
|
||||
| T
|
||||
| {
|
||||
textFieldForBlock?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
1902
test/bulk-edit/schema.graphql
Normal file
1902
test/bulk-edit/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
1
test/bulk-edit/shared.ts
Normal file
1
test/bulk-edit/shared.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const postsSlug = 'posts'
|
||||
13
test/bulk-edit/tsconfig.eslint.json
Normal file
13
test/bulk-edit/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// extend your base config to share compilerOptions, etc
|
||||
//"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// ensure that nobody can accidentally use this config for a build
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
// whatever paths you intend to lint
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
]
|
||||
}
|
||||
3
test/bulk-edit/tsconfig.json
Normal file
3
test/bulk-edit/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.json"
|
||||
}
|
||||
9
test/bulk-edit/types.d.ts
vendored
Normal file
9
test/bulk-edit/types.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { RequestContext as OriginalRequestContext } from 'payload'
|
||||
|
||||
declare module 'payload' {
|
||||
// Create a new interface that merges your additional fields with the original one
|
||||
export interface RequestContext extends OriginalRequestContext {
|
||||
myObject?: string
|
||||
// ...
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,8 @@ import {
|
||||
changeLocale,
|
||||
ensureCompilationIsDone,
|
||||
exactText,
|
||||
findTableCell,
|
||||
initPageConsoleErrorCatch,
|
||||
saveDocAndAssert,
|
||||
selectTableRow,
|
||||
} from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { assertNetworkRequests } from '../helpers/e2e/assertNetworkRequests.js'
|
||||
@@ -47,7 +45,6 @@ import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../helpers/reInitializeDB.js'
|
||||
import { waitForAutoSaveToRunAndComplete } from '../helpers/waitForAutoSaveToRunAndComplete.js'
|
||||
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import { titleToDelete } from './shared.js'
|
||||
import {
|
||||
autosaveCollectionSlug,
|
||||
autoSaveGlobalSlug,
|
||||
@@ -125,130 +122,6 @@ describe('Versions', () => {
|
||||
postURL = new AdminUrlUtil(serverURL, postCollectionSlug)
|
||||
})
|
||||
|
||||
// This test has to run before bulk updates that will rename the title
|
||||
test('should delete', async () => {
|
||||
await page.goto(url.list)
|
||||
|
||||
const rows = page.locator(`tr`)
|
||||
const rowToDelete = rows.filter({ hasText: titleToDelete })
|
||||
|
||||
await rowToDelete.locator('.cell-_select input').check()
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await page.locator('#delete-draft-posts #confirm-action').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Deleted 1 Draft Post successfully.',
|
||||
)
|
||||
|
||||
await expect(page.locator('.row-1 .cell-title')).not.toHaveText(titleToDelete)
|
||||
})
|
||||
|
||||
test('bulk update - should publish many', async () => {
|
||||
await page.goto(url.list)
|
||||
|
||||
// Select specific rows by title
|
||||
await selectTableRow(page, 'Published Title')
|
||||
await selectTableRow(page, 'Draft Title')
|
||||
|
||||
// Bulk edit the selected rows
|
||||
await page.locator('.publish-many__toggle').click()
|
||||
await page.locator('#publish-draft-posts #confirm-action').click()
|
||||
|
||||
// Check that the statuses for each row has been updated to `published`
|
||||
await expect(findTableCell(page, '_status', 'Published Title')).toContainText('Published')
|
||||
|
||||
await expect(findTableCell(page, '_status', 'Draft Title')).toContainText('Published')
|
||||
})
|
||||
|
||||
test('bulk publish with autosave documents', async () => {
|
||||
const title = 'autosave title'
|
||||
const description = 'autosave description'
|
||||
await page.goto(autosaveURL.create)
|
||||
// gets redirected from /create to /slug/id due to autosave
|
||||
await page.waitForURL(new RegExp(`${autosaveURL.edit('')}`))
|
||||
await wait(500)
|
||||
await expect(page.locator('#field-title')).toBeEnabled()
|
||||
await page.locator('#field-title').fill(title)
|
||||
await expect(page.locator('#field-description')).toBeEnabled()
|
||||
await page.locator('#field-description').fill(description)
|
||||
await waitForAutoSaveToRunAndComplete(page)
|
||||
await page.goto(autosaveURL.list)
|
||||
await expect(findTableCell(page, '_status', title)).toContainText('Draft')
|
||||
await selectTableRow(page, title)
|
||||
await page.locator('.publish-many__toggle').click()
|
||||
await page.locator('#publish-autosave-posts #confirm-action').click()
|
||||
await expect(findTableCell(page, '_status', title)).toContainText('Published')
|
||||
})
|
||||
|
||||
test('bulk update - should unpublish many', async () => {
|
||||
await page.goto(url.list)
|
||||
|
||||
// Select specific rows by title
|
||||
await selectTableRow(page, 'Published Title')
|
||||
await selectTableRow(page, 'Draft Title')
|
||||
|
||||
// Bulk edit the selected rows
|
||||
await page.locator('.unpublish-many__toggle').click()
|
||||
await page.locator('#unpublish-draft-posts #confirm-action').click()
|
||||
|
||||
// Check that the statuses for each row has been updated to `draft`
|
||||
await expect(findTableCell(page, '_status', 'Published Title')).toContainText('Draft')
|
||||
await expect(findTableCell(page, '_status', 'Draft Title')).toContainText('Draft')
|
||||
})
|
||||
|
||||
test('bulk update — should publish changes', async () => {
|
||||
const description = 'published document'
|
||||
await page.goto(url.list)
|
||||
|
||||
// Select specific rows by title
|
||||
await selectTableRow(page, 'Published Title')
|
||||
await selectTableRow(page, 'Draft Title')
|
||||
|
||||
// Bulk edit the selected rows to `published` status
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
const options = page.locator('.rs__option')
|
||||
const field = options.locator('text=Description')
|
||||
await field.click()
|
||||
await page.locator('#field-description').fill(description)
|
||||
await page.locator('.form-submit .edit-many__publish').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Draft Posts successfully.',
|
||||
)
|
||||
|
||||
// Check that the statuses for each row has been updated to `published`
|
||||
await expect(findTableCell(page, '_status', 'Published Title')).toContainText('Published')
|
||||
|
||||
await expect(findTableCell(page, '_status', 'Draft Title')).toContainText('Published')
|
||||
})
|
||||
|
||||
test('bulk update — should draft changes', async () => {
|
||||
const description = 'draft document'
|
||||
await page.goto(url.list)
|
||||
|
||||
// Select specific rows by title
|
||||
await selectTableRow(page, 'Published Title')
|
||||
await selectTableRow(page, 'Draft Title')
|
||||
|
||||
// Bulk edit the selected rows to `draft` status
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
const options = page.locator('.rs__option')
|
||||
const field = options.locator('text=Description')
|
||||
await field.click()
|
||||
await page.locator('#field-description').fill(description)
|
||||
await page.locator('.form-submit .edit-many__draft').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Draft Posts successfully.',
|
||||
)
|
||||
|
||||
// Check that the statuses for each row has been updated to `draft`
|
||||
await expect(findTableCell(page, '_status', 'Published Title')).toContainText('Draft')
|
||||
await expect(findTableCell(page, '_status', 'Draft Title')).toContainText('Draft')
|
||||
})
|
||||
|
||||
test('collection — has versions tab', async () => {
|
||||
await page.goto(url.list)
|
||||
await page.locator('tbody tr .cell-title a').first().click()
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { DraftPost } from './payload-types.js'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { executePromises } from '../helpers/executePromises.js'
|
||||
import { titleToDelete } from './shared.js'
|
||||
import {
|
||||
autosaveWithValidateCollectionSlug,
|
||||
diffCollectionSlug,
|
||||
@@ -113,18 +112,6 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
|
||||
draft: false,
|
||||
})
|
||||
|
||||
await _payload.create({
|
||||
collection: draftCollectionSlug,
|
||||
data: {
|
||||
blocksField,
|
||||
description: 'Description',
|
||||
title: titleToDelete,
|
||||
},
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
draft: true,
|
||||
})
|
||||
|
||||
await _payload.create({
|
||||
collection: autosaveWithValidateCollectionSlug,
|
||||
data: {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const titleToDelete = 'Title To Delete'
|
||||
|
||||
Reference in New Issue
Block a user