From a44a252f314fd38283a11d5beb685332d970c607 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Tue, 18 Mar 2025 13:31:51 -0400 Subject: [PATCH] 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. --- .github/workflows/main.yml | 1 + test/admin/collections/Posts.ts | 58 - test/admin/e2e/general/e2e.spec.ts | 204 --- test/admin/payload-types.ts | 56 +- test/bulk-edit/.gitignore | 2 + test/bulk-edit/collections/Posts/index.ts | 86 + test/bulk-edit/config.ts | 38 + test/bulk-edit/e2e.spec.ts | 444 +++++ test/bulk-edit/eslint.config.js | 19 + test/bulk-edit/payload-types.ts | 327 ++++ test/bulk-edit/schema.graphql | 1902 +++++++++++++++++++++ test/bulk-edit/shared.ts | 1 + test/bulk-edit/tsconfig.eslint.json | 13 + test/bulk-edit/tsconfig.json | 3 + test/bulk-edit/types.d.ts | 9 + test/versions/e2e.spec.ts | 127 -- test/versions/seed.ts | 13 - test/versions/shared.ts | 1 - tsconfig.base.json | 2 +- 19 files changed, 2847 insertions(+), 459 deletions(-) create mode 100644 test/bulk-edit/.gitignore create mode 100644 test/bulk-edit/collections/Posts/index.ts create mode 100644 test/bulk-edit/config.ts create mode 100644 test/bulk-edit/e2e.spec.ts create mode 100644 test/bulk-edit/eslint.config.js create mode 100644 test/bulk-edit/payload-types.ts create mode 100644 test/bulk-edit/schema.graphql create mode 100644 test/bulk-edit/shared.ts create mode 100644 test/bulk-edit/tsconfig.eslint.json create mode 100644 test/bulk-edit/tsconfig.json create mode 100644 test/bulk-edit/types.d.ts 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"],