diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4870e707e5..755f7acde0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -279,6 +279,7 @@ jobs: - admin-root - auth - auth-basic + - bulk-edit - joins - field-error-states - fields-relationship diff --git a/test/admin/collections/Posts.ts b/test/admin/collections/Posts.ts index 88fe04328a..257cbb9692 100644 --- a/test/admin/collections/Posts.ts +++ b/test/admin/collections/Posts.ts @@ -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', diff --git a/test/admin/e2e/general/e2e.spec.ts b/test/admin/e2e/general/e2e.spec.ts index 5840c6789f..5a8ca2ff04 100644 --- a/test/admin/e2e/general/e2e.spec.ts +++ b/test/admin/e2e/general/e2e.spec.ts @@ -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): Promise { return payload.create({ collection: postsCollectionSlug, diff --git a/test/admin/payload-types.ts b/test/admin/payload-types.ts index 81586b26ab..67d6a7a417 100644 --- a/test/admin/payload-types.ts +++ b/test/admin/payload-types.ts @@ -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 { 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; diff --git a/test/bulk-edit/.gitignore b/test/bulk-edit/.gitignore new file mode 100644 index 0000000000..cce01755f4 --- /dev/null +++ b/test/bulk-edit/.gitignore @@ -0,0 +1,2 @@ +/media +/media-gif diff --git a/test/bulk-edit/collections/Posts/index.ts b/test/bulk-edit/collections/Posts/index.ts new file mode 100644 index 0000000000..bc5417ee7c --- /dev/null +++ b/test/bulk-edit/collections/Posts/index.ts @@ -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', + }, + ], + }, + ], + }, + ], +} diff --git a/test/bulk-edit/config.ts b/test/bulk-edit/config.ts new file mode 100644 index 0000000000..6886682bda --- /dev/null +++ b/test/bulk-edit/config.ts @@ -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'), + }, +}) diff --git a/test/bulk-edit/e2e.spec.ts b/test/bulk-edit/e2e.spec.ts new file mode 100644 index 0000000000..be1904236d --- /dev/null +++ b/test/bulk-edit/e2e.spec.ts @@ -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 +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, + overrides?: Record, +): Promise { + return payload.create({ + collection: postsSlug, + ...(overrides || {}), + data: { + title: 'Post Title', + ...(dataOverrides || {}), + }, + }) as unknown as Promise +} diff --git a/test/bulk-edit/eslint.config.js b/test/bulk-edit/eslint.config.js new file mode 100644 index 0000000000..f295df083f --- /dev/null +++ b/test/bulk-edit/eslint.config.js @@ -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 diff --git a/test/bulk-edit/payload-types.ts b/test/bulk-edit/payload-types.ts new file mode 100644 index 0000000000..40df2be010 --- /dev/null +++ b/test/bulk-edit/payload-types.ts @@ -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 | PostsSelect; + users: UsersSelect | UsersSelect; + 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; + 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; + 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; + }; + 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 { + 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 { + 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 { + 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 { + 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 { + 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 {} +} \ No newline at end of file diff --git a/test/bulk-edit/schema.graphql b/test/bulk-edit/schema.graphql new file mode 100644 index 0000000000..1038d843d6 --- /dev/null +++ b/test/bulk-edit/schema.graphql @@ -0,0 +1,1902 @@ +type Query { + Post(id: String!, draft: Boolean): Post + Posts(draft: Boolean, where: Post_where, limit: Int, page: Int, sort: String): Posts + countPosts(draft: Boolean, where: Post_where): countPosts + docAccessPost(id: String!): postsDocAccess + versionPost(id: String): PostVersion + versionsPosts(where: versionsPost_where, limit: Int, page: Int, sort: String): versionsPosts + User(id: String!, draft: Boolean): User + Users(draft: Boolean, where: User_where, limit: Int, page: Int, sort: String): Users + countUsers(draft: Boolean, where: User_where): countUsers + docAccessUser(id: String!): usersDocAccess + meUser: usersMe + initializedUser: Boolean + PayloadPreference(id: String!, draft: Boolean): PayloadPreference + PayloadPreferences( + draft: Boolean + where: PayloadPreference_where + limit: Int + page: Int + sort: String + ): PayloadPreferences + countPayloadPreferences(draft: Boolean, where: PayloadPreference_where): countPayloadPreferences + docAccessPayloadPreference(id: String!): payload_preferencesDocAccess + Menu(draft: Boolean): Menu + docAccessMenu: menuDocAccess + Access: Access +} + +type Post { + id: String + text: String + richText(depth: Int): JSON + richText2(depth: Int): JSON + updatedAt: DateTime + createdAt: DateTime + _status: Post__status +} + +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON + @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") + +""" +A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. +""" +scalar DateTime + +enum Post__status { + draft + published +} + +type Posts { + docs: [Post] + hasNextPage: Boolean + hasPrevPage: Boolean + limit: Int + nextPage: Int + offset: Int + page: Int + pagingCounter: Int + prevPage: Int + totalDocs: Int + totalPages: Int +} + +input Post_where { + text: Post_text_operator + richText: Post_richText_operator + richText2: Post_richText2_operator + updatedAt: Post_updatedAt_operator + createdAt: Post_createdAt_operator + _status: Post__status_operator + id: Post_id_operator + AND: [Post_where_and] + OR: [Post_where_or] +} + +input Post_text_operator { + equals: String + not_equals: String + like: String + contains: String + in: [String] + not_in: [String] + all: [String] + exists: Boolean +} + +input Post_richText_operator { + equals: JSON + not_equals: JSON + like: JSON + contains: JSON + exists: Boolean +} + +input Post_richText2_operator { + equals: JSON + not_equals: JSON + like: JSON + contains: JSON + exists: Boolean +} + +input Post_updatedAt_operator { + equals: DateTime + not_equals: DateTime + greater_than_equal: DateTime + greater_than: DateTime + less_than_equal: DateTime + less_than: DateTime + like: DateTime + exists: Boolean +} + +input Post_createdAt_operator { + equals: DateTime + not_equals: DateTime + greater_than_equal: DateTime + greater_than: DateTime + less_than_equal: DateTime + less_than: DateTime + like: DateTime + exists: Boolean +} + +input Post__status_operator { + equals: Post__status_Input + not_equals: Post__status_Input + in: [Post__status_Input] + not_in: [Post__status_Input] + all: [Post__status_Input] + exists: Boolean +} + +enum Post__status_Input { + draft + published +} + +input Post_id_operator { + equals: String + not_equals: String + like: String + contains: String + in: [String] + not_in: [String] + all: [String] + exists: Boolean +} + +input Post_where_and { + text: Post_text_operator + richText: Post_richText_operator + richText2: Post_richText2_operator + updatedAt: Post_updatedAt_operator + createdAt: Post_createdAt_operator + _status: Post__status_operator + id: Post_id_operator + AND: [Post_where_and] + OR: [Post_where_or] +} + +input Post_where_or { + text: Post_text_operator + richText: Post_richText_operator + richText2: Post_richText2_operator + updatedAt: Post_updatedAt_operator + createdAt: Post_createdAt_operator + _status: Post__status_operator + id: Post_id_operator + AND: [Post_where_and] + OR: [Post_where_or] +} + +type countPosts { + totalDocs: Int +} + +type postsDocAccess { + fields: PostsDocAccessFields + create: PostsCreateDocAccess + read: PostsReadDocAccess + update: PostsUpdateDocAccess + delete: PostsDeleteDocAccess + readVersions: PostsReadVersionsDocAccess +} + +type PostsDocAccessFields { + text: PostsDocAccessFields_text + richText: PostsDocAccessFields_richText + richText2: PostsDocAccessFields_richText2 + updatedAt: PostsDocAccessFields_updatedAt + createdAt: PostsDocAccessFields_createdAt + _status: PostsDocAccessFields__status +} + +type PostsDocAccessFields_text { + create: PostsDocAccessFields_text_Create + read: PostsDocAccessFields_text_Read + update: PostsDocAccessFields_text_Update + delete: PostsDocAccessFields_text_Delete +} + +type PostsDocAccessFields_text_Create { + permission: Boolean! +} + +type PostsDocAccessFields_text_Read { + permission: Boolean! +} + +type PostsDocAccessFields_text_Update { + permission: Boolean! +} + +type PostsDocAccessFields_text_Delete { + permission: Boolean! +} + +type PostsDocAccessFields_richText { + create: PostsDocAccessFields_richText_Create + read: PostsDocAccessFields_richText_Read + update: PostsDocAccessFields_richText_Update + delete: PostsDocAccessFields_richText_Delete +} + +type PostsDocAccessFields_richText_Create { + permission: Boolean! +} + +type PostsDocAccessFields_richText_Read { + permission: Boolean! +} + +type PostsDocAccessFields_richText_Update { + permission: Boolean! +} + +type PostsDocAccessFields_richText_Delete { + permission: Boolean! +} + +type PostsDocAccessFields_richText2 { + create: PostsDocAccessFields_richText2_Create + read: PostsDocAccessFields_richText2_Read + update: PostsDocAccessFields_richText2_Update + delete: PostsDocAccessFields_richText2_Delete +} + +type PostsDocAccessFields_richText2_Create { + permission: Boolean! +} + +type PostsDocAccessFields_richText2_Read { + permission: Boolean! +} + +type PostsDocAccessFields_richText2_Update { + permission: Boolean! +} + +type PostsDocAccessFields_richText2_Delete { + permission: Boolean! +} + +type PostsDocAccessFields_updatedAt { + create: PostsDocAccessFields_updatedAt_Create + read: PostsDocAccessFields_updatedAt_Read + update: PostsDocAccessFields_updatedAt_Update + delete: PostsDocAccessFields_updatedAt_Delete +} + +type PostsDocAccessFields_updatedAt_Create { + permission: Boolean! +} + +type PostsDocAccessFields_updatedAt_Read { + permission: Boolean! +} + +type PostsDocAccessFields_updatedAt_Update { + permission: Boolean! +} + +type PostsDocAccessFields_updatedAt_Delete { + permission: Boolean! +} + +type PostsDocAccessFields_createdAt { + create: PostsDocAccessFields_createdAt_Create + read: PostsDocAccessFields_createdAt_Read + update: PostsDocAccessFields_createdAt_Update + delete: PostsDocAccessFields_createdAt_Delete +} + +type PostsDocAccessFields_createdAt_Create { + permission: Boolean! +} + +type PostsDocAccessFields_createdAt_Read { + permission: Boolean! +} + +type PostsDocAccessFields_createdAt_Update { + permission: Boolean! +} + +type PostsDocAccessFields_createdAt_Delete { + permission: Boolean! +} + +type PostsDocAccessFields__status { + create: PostsDocAccessFields__status_Create + read: PostsDocAccessFields__status_Read + update: PostsDocAccessFields__status_Update + delete: PostsDocAccessFields__status_Delete +} + +type PostsDocAccessFields__status_Create { + permission: Boolean! +} + +type PostsDocAccessFields__status_Read { + permission: Boolean! +} + +type PostsDocAccessFields__status_Update { + permission: Boolean! +} + +type PostsDocAccessFields__status_Delete { + permission: Boolean! +} + +type PostsCreateDocAccess { + permission: Boolean! + where: JSONObject +} + +""" +The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSONObject + @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") + +type PostsReadDocAccess { + permission: Boolean! + where: JSONObject +} + +type PostsUpdateDocAccess { + permission: Boolean! + where: JSONObject +} + +type PostsDeleteDocAccess { + permission: Boolean! + where: JSONObject +} + +type PostsReadVersionsDocAccess { + permission: Boolean! + where: JSONObject +} + +type PostVersion { + parent(draft: Boolean): Post + version: PostVersion_Version + createdAt: DateTime + updatedAt: DateTime + latest: Boolean + id: String +} + +type PostVersion_Version { + text: String + richText(depth: Int): JSON + richText2(depth: Int): JSON + updatedAt: DateTime + createdAt: DateTime + _status: PostVersion_Version__status +} + +enum PostVersion_Version__status { + draft + published +} + +type versionsPosts { + docs: [PostVersion] + hasNextPage: Boolean + hasPrevPage: Boolean + limit: Int + nextPage: Int + offset: Int + page: Int + pagingCounter: Int + prevPage: Int + totalDocs: Int + totalPages: Int +} + +input versionsPost_where { + parent: versionsPost_parent_operator + version__text: versionsPost_version__text_operator + version__richText: versionsPost_version__richText_operator + version__richText2: versionsPost_version__richText2_operator + version__updatedAt: versionsPost_version__updatedAt_operator + version__createdAt: versionsPost_version__createdAt_operator + version___status: versionsPost_version___status_operator + createdAt: versionsPost_createdAt_operator + updatedAt: versionsPost_updatedAt_operator + latest: versionsPost_latest_operator + id: versionsPost_id_operator + AND: [versionsPost_where_and] + OR: [versionsPost_where_or] +} + +input versionsPost_parent_operator { + equals: JSON + not_equals: JSON + in: [JSON] + not_in: [JSON] + all: [JSON] + exists: Boolean +} + +input versionsPost_version__text_operator { + equals: String + not_equals: String + like: String + contains: String + in: [String] + not_in: [String] + all: [String] + exists: Boolean +} + +input versionsPost_version__richText_operator { + equals: JSON + not_equals: JSON + like: JSON + contains: JSON + exists: Boolean +} + +input versionsPost_version__richText2_operator { + equals: JSON + not_equals: JSON + like: JSON + contains: JSON + exists: Boolean +} + +input versionsPost_version__updatedAt_operator { + equals: DateTime + not_equals: DateTime + greater_than_equal: DateTime + greater_than: DateTime + less_than_equal: DateTime + less_than: DateTime + like: DateTime + exists: Boolean +} + +input versionsPost_version__createdAt_operator { + equals: DateTime + not_equals: DateTime + greater_than_equal: DateTime + greater_than: DateTime + less_than_equal: DateTime + less_than: DateTime + like: DateTime + exists: Boolean +} + +input versionsPost_version___status_operator { + equals: versionsPost_version___status_Input + not_equals: versionsPost_version___status_Input + in: [versionsPost_version___status_Input] + not_in: [versionsPost_version___status_Input] + all: [versionsPost_version___status_Input] + exists: Boolean +} + +enum versionsPost_version___status_Input { + draft + published +} + +input versionsPost_createdAt_operator { + equals: DateTime + not_equals: DateTime + greater_than_equal: DateTime + greater_than: DateTime + less_than_equal: DateTime + less_than: DateTime + like: DateTime + exists: Boolean +} + +input versionsPost_updatedAt_operator { + equals: DateTime + not_equals: DateTime + greater_than_equal: DateTime + greater_than: DateTime + less_than_equal: DateTime + less_than: DateTime + like: DateTime + exists: Boolean +} + +input versionsPost_latest_operator { + equals: Boolean + not_equals: Boolean + exists: Boolean +} + +input versionsPost_id_operator { + equals: String + not_equals: String + like: String + contains: String + in: [String] + not_in: [String] + all: [String] + exists: Boolean +} + +input versionsPost_where_and { + parent: versionsPost_parent_operator + version__text: versionsPost_version__text_operator + version__richText: versionsPost_version__richText_operator + version__richText2: versionsPost_version__richText2_operator + version__updatedAt: versionsPost_version__updatedAt_operator + version__createdAt: versionsPost_version__createdAt_operator + version___status: versionsPost_version___status_operator + createdAt: versionsPost_createdAt_operator + updatedAt: versionsPost_updatedAt_operator + latest: versionsPost_latest_operator + id: versionsPost_id_operator + AND: [versionsPost_where_and] + OR: [versionsPost_where_or] +} + +input versionsPost_where_or { + parent: versionsPost_parent_operator + version__text: versionsPost_version__text_operator + version__richText: versionsPost_version__richText_operator + version__richText2: versionsPost_version__richText2_operator + version__updatedAt: versionsPost_version__updatedAt_operator + version__createdAt: versionsPost_version__createdAt_operator + version___status: versionsPost_version___status_operator + createdAt: versionsPost_createdAt_operator + updatedAt: versionsPost_updatedAt_operator + latest: versionsPost_latest_operator + id: versionsPost_id_operator + AND: [versionsPost_where_and] + OR: [versionsPost_where_or] +} + +type User { + id: String + updatedAt: DateTime + createdAt: DateTime + email: EmailAddress! + resetPasswordToken: String + resetPasswordExpiration: DateTime + salt: String + hash: String + loginAttempts: Float + lockUntil: DateTime + password: String! +} + +""" +A field whose value conforms to the standard internet email address format as specified in HTML Spec: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address. +""" +scalar EmailAddress + @specifiedBy(url: "https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address") + +type Users { + docs: [User] + hasNextPage: Boolean + hasPrevPage: Boolean + limit: Int + nextPage: Int + offset: Int + page: Int + pagingCounter: Int + prevPage: Int + totalDocs: Int + totalPages: Int +} + +input User_where { + updatedAt: User_updatedAt_operator + createdAt: User_createdAt_operator + email: User_email_operator + id: User_id_operator + AND: [User_where_and] + OR: [User_where_or] +} + +input User_updatedAt_operator { + equals: DateTime + not_equals: DateTime + greater_than_equal: DateTime + greater_than: DateTime + less_than_equal: DateTime + less_than: DateTime + like: DateTime + exists: Boolean +} + +input User_createdAt_operator { + equals: DateTime + not_equals: DateTime + greater_than_equal: DateTime + greater_than: DateTime + less_than_equal: DateTime + less_than: DateTime + like: DateTime + exists: Boolean +} + +input User_email_operator { + equals: EmailAddress + not_equals: EmailAddress + like: EmailAddress + contains: EmailAddress + in: [EmailAddress] + not_in: [EmailAddress] + all: [EmailAddress] +} + +input User_id_operator { + equals: String + not_equals: String + like: String + contains: String + in: [String] + not_in: [String] + all: [String] + exists: Boolean +} + +input User_where_and { + updatedAt: User_updatedAt_operator + createdAt: User_createdAt_operator + email: User_email_operator + id: User_id_operator + AND: [User_where_and] + OR: [User_where_or] +} + +input User_where_or { + updatedAt: User_updatedAt_operator + createdAt: User_createdAt_operator + email: User_email_operator + id: User_id_operator + AND: [User_where_and] + OR: [User_where_or] +} + +type countUsers { + totalDocs: Int +} + +type usersDocAccess { + fields: UsersDocAccessFields + create: UsersCreateDocAccess + read: UsersReadDocAccess + update: UsersUpdateDocAccess + delete: UsersDeleteDocAccess + unlock: UsersUnlockDocAccess +} + +type UsersDocAccessFields { + updatedAt: UsersDocAccessFields_updatedAt + createdAt: UsersDocAccessFields_createdAt + email: UsersDocAccessFields_email + password: UsersDocAccessFields_password +} + +type UsersDocAccessFields_updatedAt { + create: UsersDocAccessFields_updatedAt_Create + read: UsersDocAccessFields_updatedAt_Read + update: UsersDocAccessFields_updatedAt_Update + delete: UsersDocAccessFields_updatedAt_Delete +} + +type UsersDocAccessFields_updatedAt_Create { + permission: Boolean! +} + +type UsersDocAccessFields_updatedAt_Read { + permission: Boolean! +} + +type UsersDocAccessFields_updatedAt_Update { + permission: Boolean! +} + +type UsersDocAccessFields_updatedAt_Delete { + permission: Boolean! +} + +type UsersDocAccessFields_createdAt { + create: UsersDocAccessFields_createdAt_Create + read: UsersDocAccessFields_createdAt_Read + update: UsersDocAccessFields_createdAt_Update + delete: UsersDocAccessFields_createdAt_Delete +} + +type UsersDocAccessFields_createdAt_Create { + permission: Boolean! +} + +type UsersDocAccessFields_createdAt_Read { + permission: Boolean! +} + +type UsersDocAccessFields_createdAt_Update { + permission: Boolean! +} + +type UsersDocAccessFields_createdAt_Delete { + permission: Boolean! +} + +type UsersDocAccessFields_email { + create: UsersDocAccessFields_email_Create + read: UsersDocAccessFields_email_Read + update: UsersDocAccessFields_email_Update + delete: UsersDocAccessFields_email_Delete +} + +type UsersDocAccessFields_email_Create { + permission: Boolean! +} + +type UsersDocAccessFields_email_Read { + permission: Boolean! +} + +type UsersDocAccessFields_email_Update { + permission: Boolean! +} + +type UsersDocAccessFields_email_Delete { + permission: Boolean! +} + +type UsersDocAccessFields_password { + create: UsersDocAccessFields_password_Create + read: UsersDocAccessFields_password_Read + update: UsersDocAccessFields_password_Update + delete: UsersDocAccessFields_password_Delete +} + +type UsersDocAccessFields_password_Create { + permission: Boolean! +} + +type UsersDocAccessFields_password_Read { + permission: Boolean! +} + +type UsersDocAccessFields_password_Update { + permission: Boolean! +} + +type UsersDocAccessFields_password_Delete { + permission: Boolean! +} + +type UsersCreateDocAccess { + permission: Boolean! + where: JSONObject +} + +type UsersReadDocAccess { + permission: Boolean! + where: JSONObject +} + +type UsersUpdateDocAccess { + permission: Boolean! + where: JSONObject +} + +type UsersDeleteDocAccess { + permission: Boolean! + where: JSONObject +} + +type UsersUnlockDocAccess { + permission: Boolean! + where: JSONObject +} + +type usersMe { + collection: String + exp: Int + token: String + user: User +} + +type PayloadPreference { + id: String + user: PayloadPreference_User_Relationship! + key: String + value: JSON + updatedAt: DateTime + createdAt: DateTime +} + +type PayloadPreference_User_Relationship { + relationTo: PayloadPreference_User_RelationTo + value: PayloadPreference_User +} + +enum PayloadPreference_User_RelationTo { + users +} + +union PayloadPreference_User = User + +type PayloadPreferences { + docs: [PayloadPreference] + hasNextPage: Boolean + hasPrevPage: Boolean + limit: Int + nextPage: Int + offset: Int + page: Int + pagingCounter: Int + prevPage: Int + totalDocs: Int + totalPages: Int +} + +input PayloadPreference_where { + user: PayloadPreference_user_Relation + key: PayloadPreference_key_operator + value: PayloadPreference_value_operator + updatedAt: PayloadPreference_updatedAt_operator + createdAt: PayloadPreference_createdAt_operator + id: PayloadPreference_id_operator + AND: [PayloadPreference_where_and] + OR: [PayloadPreference_where_or] +} + +input PayloadPreference_user_Relation { + relationTo: PayloadPreference_user_Relation_RelationTo + value: JSON +} + +enum PayloadPreference_user_Relation_RelationTo { + users +} + +input PayloadPreference_key_operator { + equals: String + not_equals: String + like: String + contains: String + in: [String] + not_in: [String] + all: [String] + exists: Boolean +} + +input PayloadPreference_value_operator { + equals: JSON + not_equals: JSON + like: JSON + contains: JSON + within: JSON + intersects: JSON + exists: Boolean +} + +input PayloadPreference_updatedAt_operator { + equals: DateTime + not_equals: DateTime + greater_than_equal: DateTime + greater_than: DateTime + less_than_equal: DateTime + less_than: DateTime + like: DateTime + exists: Boolean +} + +input PayloadPreference_createdAt_operator { + equals: DateTime + not_equals: DateTime + greater_than_equal: DateTime + greater_than: DateTime + less_than_equal: DateTime + less_than: DateTime + like: DateTime + exists: Boolean +} + +input PayloadPreference_id_operator { + equals: String + not_equals: String + like: String + contains: String + in: [String] + not_in: [String] + all: [String] + exists: Boolean +} + +input PayloadPreference_where_and { + user: PayloadPreference_user_Relation + key: PayloadPreference_key_operator + value: PayloadPreference_value_operator + updatedAt: PayloadPreference_updatedAt_operator + createdAt: PayloadPreference_createdAt_operator + id: PayloadPreference_id_operator + AND: [PayloadPreference_where_and] + OR: [PayloadPreference_where_or] +} + +input PayloadPreference_where_or { + user: PayloadPreference_user_Relation + key: PayloadPreference_key_operator + value: PayloadPreference_value_operator + updatedAt: PayloadPreference_updatedAt_operator + createdAt: PayloadPreference_createdAt_operator + id: PayloadPreference_id_operator + AND: [PayloadPreference_where_and] + OR: [PayloadPreference_where_or] +} + +type countPayloadPreferences { + totalDocs: Int +} + +type payload_preferencesDocAccess { + fields: PayloadPreferencesDocAccessFields + create: PayloadPreferencesCreateDocAccess + read: PayloadPreferencesReadDocAccess + update: PayloadPreferencesUpdateDocAccess + delete: PayloadPreferencesDeleteDocAccess +} + +type PayloadPreferencesDocAccessFields { + user: PayloadPreferencesDocAccessFields_user + key: PayloadPreferencesDocAccessFields_key + value: PayloadPreferencesDocAccessFields_value + updatedAt: PayloadPreferencesDocAccessFields_updatedAt + createdAt: PayloadPreferencesDocAccessFields_createdAt +} + +type PayloadPreferencesDocAccessFields_user { + create: PayloadPreferencesDocAccessFields_user_Create + read: PayloadPreferencesDocAccessFields_user_Read + update: PayloadPreferencesDocAccessFields_user_Update + delete: PayloadPreferencesDocAccessFields_user_Delete +} + +type PayloadPreferencesDocAccessFields_user_Create { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_user_Read { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_user_Update { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_user_Delete { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_key { + create: PayloadPreferencesDocAccessFields_key_Create + read: PayloadPreferencesDocAccessFields_key_Read + update: PayloadPreferencesDocAccessFields_key_Update + delete: PayloadPreferencesDocAccessFields_key_Delete +} + +type PayloadPreferencesDocAccessFields_key_Create { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_key_Read { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_key_Update { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_key_Delete { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_value { + create: PayloadPreferencesDocAccessFields_value_Create + read: PayloadPreferencesDocAccessFields_value_Read + update: PayloadPreferencesDocAccessFields_value_Update + delete: PayloadPreferencesDocAccessFields_value_Delete +} + +type PayloadPreferencesDocAccessFields_value_Create { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_value_Read { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_value_Update { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_value_Delete { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_updatedAt { + create: PayloadPreferencesDocAccessFields_updatedAt_Create + read: PayloadPreferencesDocAccessFields_updatedAt_Read + update: PayloadPreferencesDocAccessFields_updatedAt_Update + delete: PayloadPreferencesDocAccessFields_updatedAt_Delete +} + +type PayloadPreferencesDocAccessFields_updatedAt_Create { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_updatedAt_Read { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_updatedAt_Update { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_updatedAt_Delete { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_createdAt { + create: PayloadPreferencesDocAccessFields_createdAt_Create + read: PayloadPreferencesDocAccessFields_createdAt_Read + update: PayloadPreferencesDocAccessFields_createdAt_Update + delete: PayloadPreferencesDocAccessFields_createdAt_Delete +} + +type PayloadPreferencesDocAccessFields_createdAt_Create { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_createdAt_Read { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_createdAt_Update { + permission: Boolean! +} + +type PayloadPreferencesDocAccessFields_createdAt_Delete { + permission: Boolean! +} + +type PayloadPreferencesCreateDocAccess { + permission: Boolean! + where: JSONObject +} + +type PayloadPreferencesReadDocAccess { + permission: Boolean! + where: JSONObject +} + +type PayloadPreferencesUpdateDocAccess { + permission: Boolean! + where: JSONObject +} + +type PayloadPreferencesDeleteDocAccess { + permission: Boolean! + where: JSONObject +} + +type Menu { + globalText: String + updatedAt: DateTime + createdAt: DateTime +} + +type menuDocAccess { + fields: MenuDocAccessFields + read: MenuReadDocAccess + update: MenuUpdateDocAccess +} + +type MenuDocAccessFields { + globalText: MenuDocAccessFields_globalText + updatedAt: MenuDocAccessFields_updatedAt + createdAt: MenuDocAccessFields_createdAt +} + +type MenuDocAccessFields_globalText { + create: MenuDocAccessFields_globalText_Create + read: MenuDocAccessFields_globalText_Read + update: MenuDocAccessFields_globalText_Update + delete: MenuDocAccessFields_globalText_Delete +} + +type MenuDocAccessFields_globalText_Create { + permission: Boolean! +} + +type MenuDocAccessFields_globalText_Read { + permission: Boolean! +} + +type MenuDocAccessFields_globalText_Update { + permission: Boolean! +} + +type MenuDocAccessFields_globalText_Delete { + permission: Boolean! +} + +type MenuDocAccessFields_updatedAt { + create: MenuDocAccessFields_updatedAt_Create + read: MenuDocAccessFields_updatedAt_Read + update: MenuDocAccessFields_updatedAt_Update + delete: MenuDocAccessFields_updatedAt_Delete +} + +type MenuDocAccessFields_updatedAt_Create { + permission: Boolean! +} + +type MenuDocAccessFields_updatedAt_Read { + permission: Boolean! +} + +type MenuDocAccessFields_updatedAt_Update { + permission: Boolean! +} + +type MenuDocAccessFields_updatedAt_Delete { + permission: Boolean! +} + +type MenuDocAccessFields_createdAt { + create: MenuDocAccessFields_createdAt_Create + read: MenuDocAccessFields_createdAt_Read + update: MenuDocAccessFields_createdAt_Update + delete: MenuDocAccessFields_createdAt_Delete +} + +type MenuDocAccessFields_createdAt_Create { + permission: Boolean! +} + +type MenuDocAccessFields_createdAt_Read { + permission: Boolean! +} + +type MenuDocAccessFields_createdAt_Update { + permission: Boolean! +} + +type MenuDocAccessFields_createdAt_Delete { + permission: Boolean! +} + +type MenuReadDocAccess { + permission: Boolean! + where: JSONObject +} + +type MenuUpdateDocAccess { + permission: Boolean! + where: JSONObject +} + +type Access { + canAccessAdmin: Boolean! + posts: postsAccess + users: usersAccess + payload_preferences: payload_preferencesAccess + menu: menuAccess +} + +type postsAccess { + fields: PostsFields + create: PostsCreateAccess + read: PostsReadAccess + update: PostsUpdateAccess + delete: PostsDeleteAccess + readVersions: PostsReadVersionsAccess +} + +type PostsFields { + text: PostsFields_text + richText: PostsFields_richText + richText2: PostsFields_richText2 + updatedAt: PostsFields_updatedAt + createdAt: PostsFields_createdAt + _status: PostsFields__status +} + +type PostsFields_text { + create: PostsFields_text_Create + read: PostsFields_text_Read + update: PostsFields_text_Update + delete: PostsFields_text_Delete +} + +type PostsFields_text_Create { + permission: Boolean! +} + +type PostsFields_text_Read { + permission: Boolean! +} + +type PostsFields_text_Update { + permission: Boolean! +} + +type PostsFields_text_Delete { + permission: Boolean! +} + +type PostsFields_richText { + create: PostsFields_richText_Create + read: PostsFields_richText_Read + update: PostsFields_richText_Update + delete: PostsFields_richText_Delete +} + +type PostsFields_richText_Create { + permission: Boolean! +} + +type PostsFields_richText_Read { + permission: Boolean! +} + +type PostsFields_richText_Update { + permission: Boolean! +} + +type PostsFields_richText_Delete { + permission: Boolean! +} + +type PostsFields_richText2 { + create: PostsFields_richText2_Create + read: PostsFields_richText2_Read + update: PostsFields_richText2_Update + delete: PostsFields_richText2_Delete +} + +type PostsFields_richText2_Create { + permission: Boolean! +} + +type PostsFields_richText2_Read { + permission: Boolean! +} + +type PostsFields_richText2_Update { + permission: Boolean! +} + +type PostsFields_richText2_Delete { + permission: Boolean! +} + +type PostsFields_updatedAt { + create: PostsFields_updatedAt_Create + read: PostsFields_updatedAt_Read + update: PostsFields_updatedAt_Update + delete: PostsFields_updatedAt_Delete +} + +type PostsFields_updatedAt_Create { + permission: Boolean! +} + +type PostsFields_updatedAt_Read { + permission: Boolean! +} + +type PostsFields_updatedAt_Update { + permission: Boolean! +} + +type PostsFields_updatedAt_Delete { + permission: Boolean! +} + +type PostsFields_createdAt { + create: PostsFields_createdAt_Create + read: PostsFields_createdAt_Read + update: PostsFields_createdAt_Update + delete: PostsFields_createdAt_Delete +} + +type PostsFields_createdAt_Create { + permission: Boolean! +} + +type PostsFields_createdAt_Read { + permission: Boolean! +} + +type PostsFields_createdAt_Update { + permission: Boolean! +} + +type PostsFields_createdAt_Delete { + permission: Boolean! +} + +type PostsFields__status { + create: PostsFields__status_Create + read: PostsFields__status_Read + update: PostsFields__status_Update + delete: PostsFields__status_Delete +} + +type PostsFields__status_Create { + permission: Boolean! +} + +type PostsFields__status_Read { + permission: Boolean! +} + +type PostsFields__status_Update { + permission: Boolean! +} + +type PostsFields__status_Delete { + permission: Boolean! +} + +type PostsCreateAccess { + permission: Boolean! + where: JSONObject +} + +type PostsReadAccess { + permission: Boolean! + where: JSONObject +} + +type PostsUpdateAccess { + permission: Boolean! + where: JSONObject +} + +type PostsDeleteAccess { + permission: Boolean! + where: JSONObject +} + +type PostsReadVersionsAccess { + permission: Boolean! + where: JSONObject +} + +type usersAccess { + fields: UsersFields + create: UsersCreateAccess + read: UsersReadAccess + update: UsersUpdateAccess + delete: UsersDeleteAccess + unlock: UsersUnlockAccess +} + +type UsersFields { + updatedAt: UsersFields_updatedAt + createdAt: UsersFields_createdAt + email: UsersFields_email + password: UsersFields_password +} + +type UsersFields_updatedAt { + create: UsersFields_updatedAt_Create + read: UsersFields_updatedAt_Read + update: UsersFields_updatedAt_Update + delete: UsersFields_updatedAt_Delete +} + +type UsersFields_updatedAt_Create { + permission: Boolean! +} + +type UsersFields_updatedAt_Read { + permission: Boolean! +} + +type UsersFields_updatedAt_Update { + permission: Boolean! +} + +type UsersFields_updatedAt_Delete { + permission: Boolean! +} + +type UsersFields_createdAt { + create: UsersFields_createdAt_Create + read: UsersFields_createdAt_Read + update: UsersFields_createdAt_Update + delete: UsersFields_createdAt_Delete +} + +type UsersFields_createdAt_Create { + permission: Boolean! +} + +type UsersFields_createdAt_Read { + permission: Boolean! +} + +type UsersFields_createdAt_Update { + permission: Boolean! +} + +type UsersFields_createdAt_Delete { + permission: Boolean! +} + +type UsersFields_email { + create: UsersFields_email_Create + read: UsersFields_email_Read + update: UsersFields_email_Update + delete: UsersFields_email_Delete +} + +type UsersFields_email_Create { + permission: Boolean! +} + +type UsersFields_email_Read { + permission: Boolean! +} + +type UsersFields_email_Update { + permission: Boolean! +} + +type UsersFields_email_Delete { + permission: Boolean! +} + +type UsersFields_password { + create: UsersFields_password_Create + read: UsersFields_password_Read + update: UsersFields_password_Update + delete: UsersFields_password_Delete +} + +type UsersFields_password_Create { + permission: Boolean! +} + +type UsersFields_password_Read { + permission: Boolean! +} + +type UsersFields_password_Update { + permission: Boolean! +} + +type UsersFields_password_Delete { + permission: Boolean! +} + +type UsersCreateAccess { + permission: Boolean! + where: JSONObject +} + +type UsersReadAccess { + permission: Boolean! + where: JSONObject +} + +type UsersUpdateAccess { + permission: Boolean! + where: JSONObject +} + +type UsersDeleteAccess { + permission: Boolean! + where: JSONObject +} + +type UsersUnlockAccess { + permission: Boolean! + where: JSONObject +} + +type payload_preferencesAccess { + fields: PayloadPreferencesFields + create: PayloadPreferencesCreateAccess + read: PayloadPreferencesReadAccess + update: PayloadPreferencesUpdateAccess + delete: PayloadPreferencesDeleteAccess +} + +type PayloadPreferencesFields { + user: PayloadPreferencesFields_user + key: PayloadPreferencesFields_key + value: PayloadPreferencesFields_value + updatedAt: PayloadPreferencesFields_updatedAt + createdAt: PayloadPreferencesFields_createdAt +} + +type PayloadPreferencesFields_user { + create: PayloadPreferencesFields_user_Create + read: PayloadPreferencesFields_user_Read + update: PayloadPreferencesFields_user_Update + delete: PayloadPreferencesFields_user_Delete +} + +type PayloadPreferencesFields_user_Create { + permission: Boolean! +} + +type PayloadPreferencesFields_user_Read { + permission: Boolean! +} + +type PayloadPreferencesFields_user_Update { + permission: Boolean! +} + +type PayloadPreferencesFields_user_Delete { + permission: Boolean! +} + +type PayloadPreferencesFields_key { + create: PayloadPreferencesFields_key_Create + read: PayloadPreferencesFields_key_Read + update: PayloadPreferencesFields_key_Update + delete: PayloadPreferencesFields_key_Delete +} + +type PayloadPreferencesFields_key_Create { + permission: Boolean! +} + +type PayloadPreferencesFields_key_Read { + permission: Boolean! +} + +type PayloadPreferencesFields_key_Update { + permission: Boolean! +} + +type PayloadPreferencesFields_key_Delete { + permission: Boolean! +} + +type PayloadPreferencesFields_value { + create: PayloadPreferencesFields_value_Create + read: PayloadPreferencesFields_value_Read + update: PayloadPreferencesFields_value_Update + delete: PayloadPreferencesFields_value_Delete +} + +type PayloadPreferencesFields_value_Create { + permission: Boolean! +} + +type PayloadPreferencesFields_value_Read { + permission: Boolean! +} + +type PayloadPreferencesFields_value_Update { + permission: Boolean! +} + +type PayloadPreferencesFields_value_Delete { + permission: Boolean! +} + +type PayloadPreferencesFields_updatedAt { + create: PayloadPreferencesFields_updatedAt_Create + read: PayloadPreferencesFields_updatedAt_Read + update: PayloadPreferencesFields_updatedAt_Update + delete: PayloadPreferencesFields_updatedAt_Delete +} + +type PayloadPreferencesFields_updatedAt_Create { + permission: Boolean! +} + +type PayloadPreferencesFields_updatedAt_Read { + permission: Boolean! +} + +type PayloadPreferencesFields_updatedAt_Update { + permission: Boolean! +} + +type PayloadPreferencesFields_updatedAt_Delete { + permission: Boolean! +} + +type PayloadPreferencesFields_createdAt { + create: PayloadPreferencesFields_createdAt_Create + read: PayloadPreferencesFields_createdAt_Read + update: PayloadPreferencesFields_createdAt_Update + delete: PayloadPreferencesFields_createdAt_Delete +} + +type PayloadPreferencesFields_createdAt_Create { + permission: Boolean! +} + +type PayloadPreferencesFields_createdAt_Read { + permission: Boolean! +} + +type PayloadPreferencesFields_createdAt_Update { + permission: Boolean! +} + +type PayloadPreferencesFields_createdAt_Delete { + permission: Boolean! +} + +type PayloadPreferencesCreateAccess { + permission: Boolean! + where: JSONObject +} + +type PayloadPreferencesReadAccess { + permission: Boolean! + where: JSONObject +} + +type PayloadPreferencesUpdateAccess { + permission: Boolean! + where: JSONObject +} + +type PayloadPreferencesDeleteAccess { + permission: Boolean! + where: JSONObject +} + +type menuAccess { + fields: MenuFields + read: MenuReadAccess + update: MenuUpdateAccess +} + +type MenuFields { + globalText: MenuFields_globalText + updatedAt: MenuFields_updatedAt + createdAt: MenuFields_createdAt +} + +type MenuFields_globalText { + create: MenuFields_globalText_Create + read: MenuFields_globalText_Read + update: MenuFields_globalText_Update + delete: MenuFields_globalText_Delete +} + +type MenuFields_globalText_Create { + permission: Boolean! +} + +type MenuFields_globalText_Read { + permission: Boolean! +} + +type MenuFields_globalText_Update { + permission: Boolean! +} + +type MenuFields_globalText_Delete { + permission: Boolean! +} + +type MenuFields_updatedAt { + create: MenuFields_updatedAt_Create + read: MenuFields_updatedAt_Read + update: MenuFields_updatedAt_Update + delete: MenuFields_updatedAt_Delete +} + +type MenuFields_updatedAt_Create { + permission: Boolean! +} + +type MenuFields_updatedAt_Read { + permission: Boolean! +} + +type MenuFields_updatedAt_Update { + permission: Boolean! +} + +type MenuFields_updatedAt_Delete { + permission: Boolean! +} + +type MenuFields_createdAt { + create: MenuFields_createdAt_Create + read: MenuFields_createdAt_Read + update: MenuFields_createdAt_Update + delete: MenuFields_createdAt_Delete +} + +type MenuFields_createdAt_Create { + permission: Boolean! +} + +type MenuFields_createdAt_Read { + permission: Boolean! +} + +type MenuFields_createdAt_Update { + permission: Boolean! +} + +type MenuFields_createdAt_Delete { + permission: Boolean! +} + +type MenuReadAccess { + permission: Boolean! + where: JSONObject +} + +type MenuUpdateAccess { + permission: Boolean! + where: JSONObject +} + +type Mutation { + createPost(data: mutationPostInput!, draft: Boolean): Post + updatePost(id: String!, autosave: Boolean, data: mutationPostUpdateInput!, draft: Boolean): Post + deletePost(id: String!): Post + duplicatePost(id: String!): Post + restoreVersionPost(id: String): Post + createUser(data: mutationUserInput!, draft: Boolean): User + updateUser(id: String!, autosave: Boolean, data: mutationUserUpdateInput!, draft: Boolean): User + deleteUser(id: String!): User + refreshTokenUser(token: String): usersRefreshedUser + logoutUser: String + unlockUser(email: String!): Boolean! + loginUser(email: String, password: String): usersLoginResult + forgotPasswordUser(disableEmail: Boolean, email: String!, expiration: Int): Boolean! + resetPasswordUser(password: String, token: String): usersResetPassword + verifyEmailUser(token: String): Boolean + createPayloadPreference(data: mutationPayloadPreferenceInput!, draft: Boolean): PayloadPreference + updatePayloadPreference( + id: String! + autosave: Boolean + data: mutationPayloadPreferenceUpdateInput! + draft: Boolean + ): PayloadPreference + deletePayloadPreference(id: String!): PayloadPreference + duplicatePayloadPreference(id: String!): PayloadPreference + updateMenu(data: mutationMenuInput!, draft: Boolean): Menu +} + +input mutationPostInput { + text: String + richText: JSON + richText2: JSON + updatedAt: String + createdAt: String + _status: Post__status_MutationInput +} + +enum Post__status_MutationInput { + draft + published +} + +input mutationPostUpdateInput { + text: String + richText: JSON + richText2: JSON + updatedAt: String + createdAt: String + _status: PostUpdate__status_MutationInput +} + +enum PostUpdate__status_MutationInput { + draft + published +} + +input mutationUserInput { + updatedAt: String + createdAt: String + email: String! + resetPasswordToken: String + resetPasswordExpiration: String + salt: String + hash: String + loginAttempts: Float + lockUntil: String + password: String! +} + +input mutationUserUpdateInput { + updatedAt: String + createdAt: String + email: String + resetPasswordToken: String + resetPasswordExpiration: String + salt: String + hash: String + loginAttempts: Float + lockUntil: String + password: String +} + +type usersRefreshedUser { + exp: Int + refreshedToken: String + user: usersJWT +} + +type usersJWT { + email: EmailAddress! + collection: String! +} + +type usersLoginResult { + exp: Int + token: String + user: User +} + +type usersResetPassword { + token: String + user: User +} + +input mutationPayloadPreferenceInput { + user: PayloadPreference_UserRelationshipInput + key: String + value: JSON + updatedAt: String + createdAt: String +} + +input PayloadPreference_UserRelationshipInput { + relationTo: PayloadPreference_UserRelationshipInputRelationTo + value: JSON +} + +enum PayloadPreference_UserRelationshipInputRelationTo { + users +} + +input mutationPayloadPreferenceUpdateInput { + user: PayloadPreferenceUpdate_UserRelationshipInput + key: String + value: JSON + updatedAt: String + createdAt: String +} + +input PayloadPreferenceUpdate_UserRelationshipInput { + relationTo: PayloadPreferenceUpdate_UserRelationshipInputRelationTo + value: JSON +} + +enum PayloadPreferenceUpdate_UserRelationshipInputRelationTo { + users +} + +input mutationMenuInput { + globalText: String + updatedAt: String + createdAt: String +} diff --git a/test/bulk-edit/shared.ts b/test/bulk-edit/shared.ts new file mode 100644 index 0000000000..f4dad31482 --- /dev/null +++ b/test/bulk-edit/shared.ts @@ -0,0 +1 @@ +export const postsSlug = 'posts' diff --git a/test/bulk-edit/tsconfig.eslint.json b/test/bulk-edit/tsconfig.eslint.json new file mode 100644 index 0000000000..b34cc7afbb --- /dev/null +++ b/test/bulk-edit/tsconfig.eslint.json @@ -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" + ] +} diff --git a/test/bulk-edit/tsconfig.json b/test/bulk-edit/tsconfig.json new file mode 100644 index 0000000000..3c43903cfd --- /dev/null +++ b/test/bulk-edit/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} diff --git a/test/bulk-edit/types.d.ts b/test/bulk-edit/types.d.ts new file mode 100644 index 0000000000..8d5bd7d65c --- /dev/null +++ b/test/bulk-edit/types.d.ts @@ -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 + // ... + } +} diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index 9e05e56425..88b2fc62c4 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -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() diff --git a/test/versions/seed.ts b/test/versions/seed.ts index 5e255219e3..ff10f7af4e 100644 --- a/test/versions/seed.ts +++ b/test/versions/seed.ts @@ -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: { diff --git a/test/versions/shared.ts b/test/versions/shared.ts index 9d43a69d0a..e69de29bb2 100644 --- a/test/versions/shared.ts +++ b/test/versions/shared.ts @@ -1 +0,0 @@ -export const titleToDelete = 'Title To Delete' diff --git a/tsconfig.base.json b/tsconfig.base.json index e2b64e3bc8..b870e53742 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,7 +31,7 @@ } ], "paths": { - "@payload-config": ["./test/form-state/config.ts"], + "@payload-config": ["./test/bulk-edit/config.ts"], "@payloadcms/admin-bar": ["./packages/admin-bar/src"], "@payloadcms/live-preview": ["./packages/live-preview/src"], "@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],