From ac1e3cf69e61b389907631af280ac7f69093e1ee Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Mon, 10 Mar 2025 21:25:14 -0400 Subject: [PATCH] feat(ui): form state queues (#11579) Implements a form state task queue. This will prevent onChange handlers within the form component from processing unnecessarily often, sometimes long after the user has stopped making changes. This leads to a potentially huge number of network requests if those changes were made slower than the debounce rate. This is especially noticeable on slow networks. Does so through a new `useQueue` hook. This hook maintains a stack of events that need processing but only processes the final event to arrive. Every time a new event is pushed to the stack, the currently running process is aborted (if any), and that event becomes the next in the queue. This results in a shocking reduction in the time it takes between final change to form state and the final network response, from ~1.5 minutes to ~3 seconds (depending on the scenario, see below). This likely fixes a number of existing open issues. I will link those issues here once they are identified and verifiably fixed. Before: I'm typing slowly here to ensure my changes aren't debounce by the form. There are a total of 60 characters typed, triggering 58 network requests and taking around 1.5 minutes to complete after the final change was made. https://github.com/user-attachments/assets/49ba0790-a8f8-4390-8421-87453ff8b650 After: Here there are a total of 69 characters typed, triggering 11 network requests and taking only about 3 seconds to complete after the final change was made. https://github.com/user-attachments/assets/447f8303-0957-41bd-bb2d-9e1151ed9ec3 --- .github/workflows/main.yml | 1 + .../jsx/converter/converters/upload.tsx | 2 +- packages/ui/src/forms/Form/index.tsx | 43 +- packages/ui/src/hooks/useQueues.ts | 48 + test/admin/collections/Posts.ts | 19 - test/admin/e2e/document-view/e2e.spec.ts | 25 - test/eslint.config.js | 7 +- test/fields-relationship/e2e.spec.ts | 4 +- test/fields/collections/Array/e2e.spec.ts | 2 +- .../Lexical/e2e/blocks/e2e.spec.ts | 25 +- test/fields/collections/Number/e2e.spec.ts | 1 - test/form-state/.gitignore | 2 + test/form-state/collections/Posts/index.ts | 59 + test/form-state/config.ts | 37 + test/form-state/e2e.spec.ts | 126 ++ test/form-state/eslint.config.js | 19 + test/form-state/int.spec.ts | 47 + test/form-state/payload-types.ts | 306 +++ test/form-state/schema.graphql | 1902 +++++++++++++++++ test/form-state/tsconfig.eslint.json | 13 + test/form-state/tsconfig.json | 3 + test/form-state/types.d.ts | 9 + ...rkRequests.ts => assertNetworkRequests.ts} | 32 +- test/versions/e2e.spec.ts | 5 +- 24 files changed, 2641 insertions(+), 96 deletions(-) create mode 100644 packages/ui/src/hooks/useQueues.ts create mode 100644 test/form-state/.gitignore create mode 100644 test/form-state/collections/Posts/index.ts create mode 100644 test/form-state/config.ts create mode 100644 test/form-state/e2e.spec.ts create mode 100644 test/form-state/eslint.config.js create mode 100644 test/form-state/int.spec.ts create mode 100644 test/form-state/payload-types.ts create mode 100644 test/form-state/schema.graphql create mode 100644 test/form-state/tsconfig.eslint.json create mode 100644 test/form-state/tsconfig.json create mode 100644 test/form-state/types.d.ts rename test/helpers/e2e/{trackNetworkRequests.ts => assertNetworkRequests.ts} (50%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e3da5ddd0d..4870e707e5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -308,6 +308,7 @@ jobs: - fields__collections__Text - fields__collections__UI - fields__collections__Upload + - form-state - live-preview - localization - locked-documents diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/upload.tsx b/packages/richtext-lexical/src/features/converters/jsx/converter/converters/upload.tsx index 16664bc1f9..d2ba6a6200 100644 --- a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/upload.tsx +++ b/packages/richtext-lexical/src/features/converters/jsx/converter/converters/upload.tsx @@ -12,7 +12,7 @@ export const UploadJSXConverter: JSXConverters = { return null } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const uploadDoc = uploadNode.value as FileData & TypeWithID const url = uploadDoc.url diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index 4a9975a0b3..0aa670042e 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -10,7 +10,7 @@ import { reduceFieldsToValues, wait, } from 'payload/shared' -import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' import { toast } from 'sonner' import type { @@ -24,6 +24,7 @@ import type { import { FieldErrorsToast } from '../../elements/Toasts/fieldErrors.js' import { useDebouncedEffect } from '../../hooks/useDebouncedEffect.js' import { useEffectEvent } from '../../hooks/useEffectEvent.js' +import { useQueues } from '../../hooks/useQueues.js' import { useThrottledEffect } from '../../hooks/useThrottledEffect.js' import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' @@ -91,6 +92,7 @@ export const Form: React.FC = (props) => { const { i18n, t } = useTranslation() const { refreshCookie, user } = useAuth() const operation = useOperation() + const { queueTask } = useQueues() const { getFormState } = useServerFunctions() const { startRouteTransition } = useRouteTransition() @@ -101,6 +103,7 @@ export const Form: React.FC = (props) => { const [disabled, setDisabled] = useState(disabledFromProps || false) const [isMounted, setIsMounted] = useState(false) const [modified, setModified] = useState(false) + /** * Tracks wether the form state passes validation. * For example the state could be submitted but invalid as field errors have been returned. @@ -114,17 +117,16 @@ export const Form: React.FC = (props) => { const formRef = useRef(null) const contextRef = useRef({} as FormContextType) const abortResetFormRef = useRef(null) + const isFirstRenderRef = useRef(true) const fieldsReducer = useReducer(fieldReducer, {}, () => initialState) - /** - * `fields` is the current, up-to-date state/data of all fields in the form. It can be modified by using dispatchFields, - * which calls the fieldReducer, which then updates the state. - */ const [fields, dispatchFields] = fieldsReducer contextRef.current.fields = fields + const prevFields = useRef(fields) + const validateForm = useCallback(async () => { const validatedFieldState = {} let isValid = true @@ -718,11 +720,15 @@ export const Form: React.FC = (props) => { const classes = [className, baseClass].filter(Boolean).join(' ') - const executeOnChange = useEffectEvent(async (submitted: boolean) => { + const executeOnChange = useEffectEvent(async (submitted: boolean, signal: AbortSignal) => { if (Array.isArray(onChange)) { let revalidatedFormState: FormState = contextRef.current.fields for (const onChangeFn of onChange) { + if (signal.aborted) { + return + } + // Edit view default onChange is in packages/ui/src/views/Edit/index.tsx. This onChange usually sends a form state request revalidatedFormState = await onChangeFn({ formState: deepCopyObjectSimpleWithoutReactComponents(contextRef.current.fields), @@ -739,7 +745,9 @@ export const Form: React.FC = (props) => { incomingState: revalidatedFormState, }) - if (changed) { + if (changed && !signal.aborted) { + prevFields.current = newState + dispatchFields({ type: 'REPLACE_STATE', optimize: false, @@ -749,29 +757,16 @@ export const Form: React.FC = (props) => { } }) - const prevFields = useRef(contextRef.current.fields) - const isFirstRenderRef = useRef(true) - useDebouncedEffect( () => { - if (isFirstRenderRef.current || !dequal(contextRef.current.fields, prevFields.current)) { - if (modified) { - void executeOnChange(submitted) - } + if ((isFirstRenderRef.current || !dequal(fields, prevFields.current)) && modified) { + queueTask(async (signal) => executeOnChange(submitted, signal)) } + prevFields.current = fields isFirstRenderRef.current = false - prevFields.current = contextRef.current.fields }, - /* - Make sure we trigger this whenever modified changes (not just when `fields` changes), - otherwise we will miss merging server form state for the first form update/onChange. - - Here's why: - `fields` updates before `modified`, because setModified is in a setTimeout. - So on the first change, modified is false, so we don't trigger the effect even though we should. - **/ - [modified, submitted, contextRef.current.fields], + [modified, submitted, fields, queueTask], 250, ) diff --git a/packages/ui/src/hooks/useQueues.ts b/packages/ui/src/hooks/useQueues.ts new file mode 100644 index 0000000000..2dddb0181d --- /dev/null +++ b/packages/ui/src/hooks/useQueues.ts @@ -0,0 +1,48 @@ +import { useRef } from 'react' + +export function useQueues(): { + queueTask: (fn: (signal: AbortSignal) => Promise) => void +} { + const runningTaskRef = useRef>(null) + const queuedTask = useRef<((signal: AbortSignal) => Promise) | null>(null) + const abortControllerRef = useRef(null) + + const queueTask = (fn: (signal: AbortSignal) => Promise) => { + // Overwrite the queued task every time a new one arrives + queuedTask.current = fn + + // If a task is already running, abort it and return + if (runningTaskRef.current !== null) { + if (abortControllerRef.current) { + abortControllerRef.current.abort() + } + + return + } + + const executeTask = async () => { + while (queuedTask.current) { + const taskToRun = queuedTask.current + queuedTask.current = null // Reset latest task before running + + const controller = new AbortController() + abortControllerRef.current = controller + + try { + runningTaskRef.current = taskToRun(controller.signal) + await runningTaskRef.current // Wait for the task to complete + } catch (err) { + if (err.name !== 'AbortError') { + console.error('Error in queued function:', err) // eslint-disable-line no-console + } + } finally { + runningTaskRef.current = null + } + } + } + + void executeTask() + } + + return { queueTask } +} diff --git a/test/admin/collections/Posts.ts b/test/admin/collections/Posts.ts index 2ecacf2224..88fe04328a 100644 --- a/test/admin/collections/Posts.ts +++ b/test/admin/collections/Posts.ts @@ -263,25 +263,6 @@ export const Posts: CollectionConfig = { position: 'sidebar', }, }, - { - name: 'validateUsingEvent', - type: 'text', - admin: { - description: - 'This field should only validate on submit. Try typing "Not allowed" and submitting the form.', - }, - validate: (value, { event }) => { - if (event === 'onChange') { - return true - } - - if (value === 'Not allowed') { - return 'This field has been validated only on submit' - } - - return true - }, - }, ], labels: { plural: slugPluralLabel, diff --git a/test/admin/e2e/document-view/e2e.spec.ts b/test/admin/e2e/document-view/e2e.spec.ts index 6dcdeeb123..92e838c2bc 100644 --- a/test/admin/e2e/document-view/e2e.spec.ts +++ b/test/admin/e2e/document-view/e2e.spec.ts @@ -174,36 +174,12 @@ describe('Document View', () => { }) }) - describe('form state', () => { - test('collection — should re-enable fields after save', async () => { - await page.goto(postsUrl.create) - await page.locator('#field-title').fill(title) - await saveDocAndAssert(page) - await expect(page.locator('#field-title')).toBeEnabled() - }) - - test('global — should re-enable fields after save', async () => { - await page.goto(globalURL.global(globalSlug)) - await page.locator('#field-title').fill(title) - await saveDocAndAssert(page) - await expect(page.locator('#field-title')).toBeEnabled() - }) - - test('should thread proper event argument to validation functions', async () => { - await page.goto(postsUrl.create) - await page.locator('#field-title').fill(title) - await page.locator('#field-validateUsingEvent').fill('Not allowed') - await saveDocAndAssert(page, '#action-save', 'error') - }) - }) - describe('document titles', () => { test('collection — should render fallback titles when creating new', async () => { await page.goto(postsUrl.create) await checkPageTitle(page, '[Untitled]') await checkBreadcrumb(page, 'Create New') await saveDocAndAssert(page) - expect(true).toBe(true) }) test('collection — should render `useAsTitle` field', async () => { @@ -213,7 +189,6 @@ describe('Document View', () => { await wait(500) await checkPageTitle(page, title) await checkBreadcrumb(page, title) - expect(true).toBe(true) }) test('collection — should render `id` as `useAsTitle` fallback', async () => { diff --git a/test/eslint.config.js b/test/eslint.config.js index 127d1ddbea..a39b80786b 100644 --- a/test/eslint.config.js +++ b/test/eslint.config.js @@ -65,7 +65,12 @@ export const testEslintConfig = [ 'playwright/expect-expect': [ 'error', { - assertFunctionNames: ['assertToastErrors', 'saveDocAndAssert', 'runFilterOptionsTest'], + assertFunctionNames: [ + 'assertToastErrors', + 'saveDocAndAssert', + 'runFilterOptionsTest', + 'assertNetworkRequests', + ], }, ], }, diff --git a/test/fields-relationship/e2e.spec.ts b/test/fields-relationship/e2e.spec.ts index 1374642641..ef3d48d692 100644 --- a/test/fields-relationship/e2e.spec.ts +++ b/test/fields-relationship/e2e.spec.ts @@ -23,7 +23,7 @@ import type { import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js' import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' -import { trackNetworkRequests } from '../helpers/e2e/trackNetworkRequests.js' +import { assertNetworkRequests } from '../helpers/e2e/assertNetworkRequests.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js' import { @@ -176,7 +176,7 @@ describe('Relationship Field', () => { await expect(options).toHaveCount(2) // two docs await options.nth(0).click() await expect(field).toContainText(relationOneDoc.id) - await trackNetworkRequests(page, `/api/${relationOneSlug}`, async () => { + await assertNetworkRequests(page, `/api/${relationOneSlug}`, async () => { await saveDocAndAssert(page) await wait(200) }) diff --git a/test/fields/collections/Array/e2e.spec.ts b/test/fields/collections/Array/e2e.spec.ts index 076e1b134d..72573dc97c 100644 --- a/test/fields/collections/Array/e2e.spec.ts +++ b/test/fields/collections/Array/e2e.spec.ts @@ -114,7 +114,7 @@ describe('Array', () => { await expect(page.locator('#field-customArrayField__0__text')).toBeVisible() }) - // eslint-disable-next-line playwright/expect-expect + test('should bypass min rows validation when no rows present and field is not required', async () => { await page.goto(url.create) await saveDocAndAssert(page) diff --git a/test/fields/collections/Lexical/e2e/blocks/e2e.spec.ts b/test/fields/collections/Lexical/e2e/blocks/e2e.spec.ts index dabc868982..b9ea5af62f 100644 --- a/test/fields/collections/Lexical/e2e/blocks/e2e.spec.ts +++ b/test/fields/collections/Lexical/e2e/blocks/e2e.spec.ts @@ -25,7 +25,7 @@ import { } from '../../../../../helpers.js' import { AdminUrlUtil } from '../../../../../helpers/adminUrlUtil.js' import { assertToastErrors } from '../../../../../helpers/assertToastErrors.js' -import { trackNetworkRequests } from '../../../../../helpers/e2e/trackNetworkRequests.js' +import { assertNetworkRequests } from '../../../../../helpers/e2e/assertNetworkRequests.js' import { initPayloadE2ENoConfig } from '../../../../../helpers/initPayloadE2ENoConfig.js' import { reInitializeDB } from '../../../../../helpers/reInitializeDB.js' import { RESTClient } from '../../../../../helpers/rest.js' @@ -400,11 +400,12 @@ describe('lexicalBlocks', () => { await dependsOnBlockData.locator('.rs__control').click() // Fill and wait for form state to come back - await trackNetworkRequests(page, '/admin/collections/lexical-fields', async () => { + await assertNetworkRequests(page, '/admin/collections/lexical-fields', async () => { await topLevelDocTextField.fill('invalid') }) + // Ensure block form state is updated and comes back (=> filter options are updated) - await trackNetworkRequests( + await assertNetworkRequests( page, '/admin/collections/lexical-fields', async () => { @@ -442,7 +443,7 @@ describe('lexicalBlocks', () => { topLevelDocTextField, } = await setupFilterOptionsTests() - await trackNetworkRequests( + await assertNetworkRequests( page, '/admin/collections/lexical-fields', async () => { @@ -478,7 +479,7 @@ describe('lexicalBlocks', () => { topLevelDocTextField, } = await setupFilterOptionsTests() - await trackNetworkRequests( + await assertNetworkRequests( page, '/admin/collections/lexical-fields', async () => { @@ -571,19 +572,21 @@ describe('lexicalBlocks', () => { await topLevelDocTextField.fill('invalid') await saveDocAndAssert(page, '#action-save', 'error') + await assertToastErrors({ page, errors: ['Lexical With Blocks', 'Lexical With Blocks → Group → Text Depends On Doc Data'], }) + await expect(page.locator('.payload-toast-container .payload-toast-item')).toBeHidden() - await trackNetworkRequests( + await assertNetworkRequests( page, '/admin/collections/lexical-fields', async () => { await topLevelDocTextField.fill('Rich Text') // Default value }, - { allowedNumberOfRequests: 2 }, + { allowedNumberOfRequests: 1 }, ) await saveDocAndAssert(page) @@ -604,13 +607,13 @@ describe('lexicalBlocks', () => { }) await expect(page.locator('.payload-toast-container .payload-toast-item')).toBeHidden() - await trackNetworkRequests( + await assertNetworkRequests( page, '/admin/collections/lexical-fields', async () => { await blockGroupTextField.fill('') }, - { allowedNumberOfRequests: 3 }, + { allowedNumberOfRequests: 2 }, ) await saveDocAndAssert(page) @@ -628,13 +631,13 @@ describe('lexicalBlocks', () => { }) await expect(page.locator('.payload-toast-container .payload-toast-item')).toBeHidden() - await trackNetworkRequests( + await assertNetworkRequests( page, '/admin/collections/lexical-fields', async () => { await blockTextField.fill('') }, - { allowedNumberOfRequests: 3 }, + { allowedNumberOfRequests: 2 }, ) await saveDocAndAssert(page) diff --git a/test/fields/collections/Number/e2e.spec.ts b/test/fields/collections/Number/e2e.spec.ts index 9452025d86..0ff48f4754 100644 --- a/test/fields/collections/Number/e2e.spec.ts +++ b/test/fields/collections/Number/e2e.spec.ts @@ -111,7 +111,6 @@ describe('Number', () => { test('should bypass min rows validation when no rows present and field is not required', async () => { await page.goto(url.create) await saveDocAndAssert(page) - expect(true).toBe(true) // the above fn contains the assertion }) test('should fail min rows validation when rows are present', async () => { diff --git a/test/form-state/.gitignore b/test/form-state/.gitignore new file mode 100644 index 0000000000..cce01755f4 --- /dev/null +++ b/test/form-state/.gitignore @@ -0,0 +1,2 @@ +/media +/media-gif diff --git a/test/form-state/collections/Posts/index.ts b/test/form-state/collections/Posts/index.ts new file mode 100644 index 0000000000..0180ee63b2 --- /dev/null +++ b/test/form-state/collections/Posts/index.ts @@ -0,0 +1,59 @@ +import type { CollectionConfig } from 'payload' + +export const postsSlug = 'posts' + +export const PostsCollection: CollectionConfig = { + slug: postsSlug, + admin: { + useAsTitle: 'title', + }, + fields: [ + { + name: 'title', + type: 'text', + }, + { + name: 'validateUsingEvent', + type: 'text', + admin: { + description: + 'This field should only validate on submit. Try typing "Not allowed" and submitting the form.', + }, + validate: (value, { event }) => { + if (event === 'onChange') { + return true + } + + if (value === 'Not allowed') { + return 'This field has been validated only on submit' + } + + return true + }, + }, + { + name: 'blocks', + type: 'blocks', + blocks: [ + { + slug: 'text', + fields: [ + { + name: 'text', + type: 'text', + }, + ], + }, + { + slug: 'number', + fields: [ + { + name: 'number', + type: 'number', + }, + ], + }, + ], + }, + ], +} diff --git a/test/form-state/config.ts b/test/form-state/config.ts new file mode 100644 index 0000000000..07145a7052 --- /dev/null +++ b/test/form-state/config.ts @@ -0,0 +1,37 @@ +import { fileURLToPath } from 'node:url' +import path from 'path' + +import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' +import { devUser } from '../credentials.js' +import { PostsCollection, postsSlug } from './collections/Posts/index.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/form-state/e2e.spec.ts b/test/form-state/e2e.spec.ts new file mode 100644 index 0000000000..ce68b4770b --- /dev/null +++ b/test/form-state/e2e.spec.ts @@ -0,0 +1,126 @@ +import type { BrowserContext, Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +import { addBlock } from 'helpers/e2e/addBlock.js' +import { assertNetworkRequests } from 'helpers/e2e/assertNetworkRequests.js' +import * as path from 'path' +import { fileURLToPath } from 'url' + +import { + ensureCompilationIsDone, + initPageConsoleErrorCatch, + saveDocAndAssert, + throttleTest, +} from '../helpers.js' +import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' +import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' +import { TEST_TIMEOUT_LONG } from '../playwright.config.js' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +const title = 'Title' +let context: BrowserContext + +test.describe('Form State', () => { + let page: Page + let postsUrl: AdminUrlUtil + + test.beforeAll(async ({ browser }, testInfo) => { + testInfo.setTimeout(TEST_TIMEOUT_LONG) + + const { payload, serverURL } = await initPayloadE2ENoConfig({ dirname }) + postsUrl = new AdminUrlUtil(serverURL, 'posts') + + context = await browser.newContext() + page = await context.newPage() + initPageConsoleErrorCatch(page) + await ensureCompilationIsDone({ page, serverURL }) + }) + + test('collection — should re-enable fields after save', async () => { + await page.goto(postsUrl.create) + await page.locator('#field-title').fill(title) + await saveDocAndAssert(page) + await expect(page.locator('#field-title')).toBeEnabled() + }) + + test('should thread proper event argument to validation functions', async () => { + await page.goto(postsUrl.create) + await page.locator('#field-title').fill(title) + await page.locator('#field-validateUsingEvent').fill('Not allowed') + await saveDocAndAssert(page, '#action-save', 'error') + }) + + test('should fire a single network request for onChange events when manipulating blocks', async () => { + await page.goto(postsUrl.create) + + await assertNetworkRequests( + page, + postsUrl.create, + async () => { + await addBlock({ + page, + blockLabel: 'Text', + fieldName: 'blocks', + }) + }, + { + allowedNumberOfRequests: 1, + }, + ) + }) + + test('should debounce onChange events', async () => { + await page.goto(postsUrl.create) + const field = page.locator('#field-title') + + await assertNetworkRequests( + page, + postsUrl.create, + async () => { + // Need to type _faster_ than the debounce rate (250ms) + await field.pressSequentially('Some text to type', { delay: 50 }) + }, + { + allowedNumberOfRequests: 1, + }, + ) + }) + + test('should queue onChange functions', async () => { + await page.goto(postsUrl.create) + const field = page.locator('#field-title') + await field.fill('Test') + + // only throttle test after initial load to avoid timeouts + const cdpSession = await throttleTest({ + page, + context, + delay: 'Slow 3G', + }) + + await assertNetworkRequests( + page, + postsUrl.create, + async () => { + await field.fill('') + // Need to type into a _slower_ than the debounce rate (250ms), but _faster_ than the network request + await field.pressSequentially('Some text to type', { delay: 300 }) + }, + { + allowedNumberOfRequests: 1, + timeout: 10000, // watch network for 10 seconds to allow requests to build up + }, + ) + + await cdpSession.send('Network.emulateNetworkConditions', { + offline: false, + latency: 0, + downloadThroughput: -1, + uploadThroughput: -1, + }) + + await cdpSession.detach() + }) +}) diff --git a/test/form-state/eslint.config.js b/test/form-state/eslint.config.js new file mode 100644 index 0000000000..f295df083f --- /dev/null +++ b/test/form-state/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/form-state/int.spec.ts b/test/form-state/int.spec.ts new file mode 100644 index 0000000000..8f21bd02e3 --- /dev/null +++ b/test/form-state/int.spec.ts @@ -0,0 +1,47 @@ +import type { Payload } from 'payload' + +import path from 'path' +import { fileURLToPath } from 'url' + +import type { NextRESTClient } from '../helpers/NextRESTClient.js' + +import { devUser } from '../credentials.js' +import { initPayloadInt } from '../helpers/initPayloadInt.js' +import { postsSlug } from './collections/Posts/index.js' + +let payload: Payload +let token: string +let restClient: NextRESTClient + +const { email, password } = devUser +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +describe('Form State', () => { + // --__--__--__--__--__--__--__--__--__ + // Boilerplate test setup/teardown + // --__--__--__--__--__--__--__--__--__ + beforeAll(async () => { + const initialized = await initPayloadInt(dirname) + ;({ payload, restClient } = initialized) + + const data = await restClient + .POST('/users/login', { + body: JSON.stringify({ + email, + password, + }), + }) + .then((res) => res.json()) + + token = data.token + }) + + afterAll(async () => { + if (typeof payload.db.destroy === 'function') { + await payload.db.destroy() + } + }) + + it.todo('should execute form state endpoint') +}) diff --git a/test/form-state/payload-types.ts b/test/form-state/payload-types.ts new file mode 100644 index 0000000000..fbeaf91611 --- /dev/null +++ b/test/form-state/payload-types.ts @@ -0,0 +1,306 @@ +/* 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/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; + /** + * This field should only validate on submit. Try typing "Not allowed" and submitting the form. + */ + validateUsingEvent?: string | null; + blocks?: + | ( + | { + text?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'text'; + } + | { + number?: number | null; + id?: string | null; + blockName?: string | null; + blockType: 'number'; + } + )[] + | null; + updatedAt: string; + createdAt: string; +} +/** + * 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; + validateUsingEvent?: T; + blocks?: + | T + | { + text?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + number?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + }; + updatedAt?: T; + createdAt?: 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/form-state/schema.graphql b/test/form-state/schema.graphql new file mode 100644 index 0000000000..1038d843d6 --- /dev/null +++ b/test/form-state/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/form-state/tsconfig.eslint.json b/test/form-state/tsconfig.eslint.json new file mode 100644 index 0000000000..b34cc7afbb --- /dev/null +++ b/test/form-state/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/form-state/tsconfig.json b/test/form-state/tsconfig.json new file mode 100644 index 0000000000..3c43903cfd --- /dev/null +++ b/test/form-state/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} diff --git a/test/form-state/types.d.ts b/test/form-state/types.d.ts new file mode 100644 index 0000000000..8d5bd7d65c --- /dev/null +++ b/test/form-state/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/helpers/e2e/trackNetworkRequests.ts b/test/helpers/e2e/assertNetworkRequests.ts similarity index 50% rename from test/helpers/e2e/trackNetworkRequests.ts rename to test/helpers/e2e/assertNetworkRequests.ts index 64c684b2c4..878f2ce24b 100644 --- a/test/helpers/e2e/trackNetworkRequests.ts +++ b/test/helpers/e2e/assertNetworkRequests.ts @@ -2,23 +2,37 @@ import type { Page, Request } from '@playwright/test' import { expect } from '@playwright/test' -// Allows you to test the number of network requests triggered by an action -// This can be used to ensure various actions do not trigger unnecessary requests -// For example, an effect within a component might fetch data multiple times unnecessarily -export const trackNetworkRequests = async ( +/** + * Counts the number of network requests every `interval` milliseconds until `timeout` is reached. + * Useful to ensure unexpected network requests are not triggered by an action. + * For example, an effect within a component might fetch data multiple times unnecessarily. + * @param page The Playwright page + * @param url The URL to match in the network requests + * @param action The action to perform + * @param options Options + * @param options.allowedNumberOfRequests The number of requests that are allowed to be made, defaults to 1 + * @param options.beforePoll A function to run before polling the network requests + * @param options.interval The interval in milliseconds to poll the network requests, defaults to 1000 + * @param options.timeout The timeout in milliseconds to poll the network requests, defaults to 5000 + * @returns The matched network requests + */ +export const assertNetworkRequests = async ( page: Page, url: string, action: () => Promise, - options?: { + { + beforePoll, + allowedNumberOfRequests = 1, + timeout = 5000, + interval = 1000, + }: { allowedNumberOfRequests?: number beforePoll?: () => Promise | void interval?: number timeout?: number - }, + } = {}, ): Promise> => { - const { beforePoll, allowedNumberOfRequests = 1, timeout = 5000, interval = 1000 } = options || {} - - const matchedRequests = [] + const matchedRequests: Request[] = [] // begin tracking network requests page.on('request', (request) => { diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index 8d70fc5028..9e05e56425 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -42,7 +42,7 @@ import { selectTableRow, } from '../helpers.js' import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' -import { trackNetworkRequests } from '../helpers/e2e/trackNetworkRequests.js' +import { assertNetworkRequests } from '../helpers/e2e/assertNetworkRequests.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { reInitializeDB } from '../helpers/reInitializeDB.js' import { waitForAutoSaveToRunAndComplete } from '../helpers/waitForAutoSaveToRunAndComplete.js' @@ -416,7 +416,7 @@ describe('Versions', () => { await page.goto(postURL.edit(postID)) - await trackNetworkRequests( + await assertNetworkRequests( page, `${serverURL}/admin/collections/${postCollectionSlug}/${postID}`, async () => { @@ -1224,6 +1224,7 @@ describe('Versions', () => { test('should render diff', async () => { await navigateToVersionDiff() + expect(true).toBe(true) }) test('should render diff for nested fields', async () => {