diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index 8dd7c38b44..82692a9a4d 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -175,20 +175,22 @@ export type Labels = { singular: LabelFunction | LabelStatic } -export type BaseValidateOptions = { +export type BaseValidateOptions = { data: Partial id?: number | string operation?: Operation preferences: DocumentPreferences + previousValue?: TValue req: PayloadRequest siblingData: Partial } -export type ValidateOptions = BaseValidateOptions< +export type ValidateOptions< TData, - TSiblingData -> & - TFieldConfig + TSiblingData, + TFieldConfig extends object, + TValue, +> = BaseValidateOptions & TFieldConfig export type Validate< TValue = any, @@ -197,7 +199,7 @@ export type Validate< TFieldConfig extends object = object, > = ( value: TValue, - options: ValidateOptions, + options: ValidateOptions, ) => Promise | string | true export type ClientValidate = Omit diff --git a/packages/payload/src/fields/hooks/beforeChange/promise.ts b/packages/payload/src/fields/hooks/beforeChange/promise.ts index 614a2775d5..ebee543b0f 100644 --- a/packages/payload/src/fields/hooks/beforeChange/promise.ts +++ b/packages/payload/src/fields/hooks/beforeChange/promise.ts @@ -140,9 +140,10 @@ export const promise = async ({ jsonError, operation, preferences: { fields: {} }, + previousValue: siblingDoc[field.name], req, siblingData: deepMergeWithSourceArrays(siblingDoc, siblingData), - } as ValidateOptions) + } as ValidateOptions) if (typeof validationResult === 'string') { errors.push({ diff --git a/packages/richtext-lexical/src/features/typesServer.ts b/packages/richtext-lexical/src/features/typesServer.ts index 994d183347..124f3b43d3 100644 --- a/packages/richtext-lexical/src/features/typesServer.ts +++ b/packages/richtext-lexical/src/features/typesServer.ts @@ -73,7 +73,7 @@ export type NodeValidation> validation: { - options: ValidateOptions + options: ValidateOptions value: SerializedEditorState } }) => Promise | string | true diff --git a/packages/richtext-lexical/src/validate/validateNodes.ts b/packages/richtext-lexical/src/validate/validateNodes.ts index 73933218e9..40cff519db 100644 --- a/packages/richtext-lexical/src/validate/validateNodes.ts +++ b/packages/richtext-lexical/src/validate/validateNodes.ts @@ -11,7 +11,7 @@ export async function validateNodes({ nodeValidations: Map> nodes: SerializedLexicalNode[] validation: { - options: ValidateOptions + options: ValidateOptions value: SerializedEditorState } }): Promise { diff --git a/test/field-error-states/collections/PrevValue/index.ts b/test/field-error-states/collections/PrevValue/index.ts new file mode 100644 index 0000000000..b277ef23bb --- /dev/null +++ b/test/field-error-states/collections/PrevValue/index.ts @@ -0,0 +1,59 @@ +import type { CollectionConfig } from 'payload' + +import * as QueryString from 'qs-esm' + +import { collectionSlugs } from '../../shared.js' + +export const PrevValue: CollectionConfig = { + slug: collectionSlugs.prevValue, + fields: [ + { + name: 'title', + type: 'text', + required: true, + validate: async (value, options) => { + if (options.operation === 'create') return true + + const query = QueryString.stringify( + { + where: { + previousValueRelation: { + in: [options.id], + }, + }, + }, + { + addQueryPrefix: true, + }, + ) + + try { + const relatedDocs = await fetch( + `http://localhost:3000/api/${collectionSlugs.prevValueRelation}${query}`, + { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }, + ).then((res) => res.json()) + if (relatedDocs.docs.length > 0 && value !== options.previousValue) { + console.log({ + value, + prev: options.previousValue, + }) + return 'Doc is being referenced, cannot change title' + } + } catch (e) { + console.log(e) + } + + return true + }, + }, + { + name: 'description', + type: 'text', + }, + ], +} diff --git a/test/field-error-states/collections/PrevValueRelation/index.ts b/test/field-error-states/collections/PrevValueRelation/index.ts new file mode 100644 index 0000000000..eea539c126 --- /dev/null +++ b/test/field-error-states/collections/PrevValueRelation/index.ts @@ -0,0 +1,14 @@ +import type { CollectionConfig } from 'payload' + +import { collectionSlugs } from '../../shared.js' + +export const PrevValueRelation: CollectionConfig = { + slug: collectionSlugs.prevValueRelation, + fields: [ + { + relationTo: collectionSlugs.prevValue, + name: 'previousValueRelation', + type: 'relationship', + }, + ], +} diff --git a/test/field-error-states/collections/ValidateDraftsOff/index.ts b/test/field-error-states/collections/ValidateDraftsOff/index.ts index 1ad125f9f5..a41a088e41 100644 --- a/test/field-error-states/collections/ValidateDraftsOff/index.ts +++ b/test/field-error-states/collections/ValidateDraftsOff/index.ts @@ -1,11 +1,11 @@ import type { CollectionConfig } from 'payload' -import { slugs } from '../../shared.js' +import { collectionSlugs } from '../../shared.js' import { ValidateDraftsOn } from '../ValidateDraftsOn/index.js' export const ValidateDraftsOff: CollectionConfig = { ...ValidateDraftsOn, - slug: slugs.validateDraftsOff, + slug: collectionSlugs.validateDraftsOff, versions: { drafts: true, }, diff --git a/test/field-error-states/collections/ValidateDraftsOn/index.ts b/test/field-error-states/collections/ValidateDraftsOn/index.ts index bf1709b0d5..770c40e192 100644 --- a/test/field-error-states/collections/ValidateDraftsOn/index.ts +++ b/test/field-error-states/collections/ValidateDraftsOn/index.ts @@ -1,9 +1,9 @@ import type { CollectionConfig } from 'payload' -import { slugs } from '../../shared.js' +import { collectionSlugs } from '../../shared.js' export const ValidateDraftsOn: CollectionConfig = { - slug: slugs.validateDraftsOn, + slug: collectionSlugs.validateDraftsOn, fields: [ { name: 'title', diff --git a/test/field-error-states/collections/ValidateDraftsOnAutosave/index.ts b/test/field-error-states/collections/ValidateDraftsOnAutosave/index.ts index 235726445f..750354285b 100644 --- a/test/field-error-states/collections/ValidateDraftsOnAutosave/index.ts +++ b/test/field-error-states/collections/ValidateDraftsOnAutosave/index.ts @@ -1,11 +1,11 @@ import type { CollectionConfig } from 'payload' -import { slugs } from '../../shared.js' +import { collectionSlugs } from '../../shared.js' import { ValidateDraftsOn } from '../ValidateDraftsOn/index.js' export const ValidateDraftsOnAndAutosave: CollectionConfig = { ...ValidateDraftsOn, - slug: slugs.validateDraftsOnAutosave, + slug: collectionSlugs.validateDraftsOnAutosave, versions: { drafts: { autosave: true, diff --git a/test/field-error-states/config.ts b/test/field-error-states/config.ts index b402efe062..0a7d5d07a3 100644 --- a/test/field-error-states/config.ts +++ b/test/field-error-states/config.ts @@ -5,6 +5,8 @@ const dirname = path.dirname(filename) import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { devUser } from '../credentials.js' import { ErrorFieldsCollection } from './collections/ErrorFields/index.js' +import { PrevValue } from './collections/PrevValue/index.js' +import { PrevValueRelation } from './collections/PrevValueRelation/index.js' import Uploads from './collections/Upload/index.js' import { ValidateDraftsOff } from './collections/ValidateDraftsOff/index.js' import { ValidateDraftsOn } from './collections/ValidateDraftsOn/index.js' @@ -18,6 +20,8 @@ export default buildConfigWithDefaults({ ValidateDraftsOn, ValidateDraftsOff, ValidateDraftsOnAndAutosave, + PrevValue, + PrevValueRelation, ], globals: [GlobalValidateDraftsOn], onInit: async (payload) => { diff --git a/test/field-error-states/e2e.spec.ts b/test/field-error-states/e2e.spec.ts index 616270901e..bc40bf01d4 100644 --- a/test/field-error-states/e2e.spec.ts +++ b/test/field-error-states/e2e.spec.ts @@ -8,7 +8,7 @@ import { fileURLToPath } from 'url' import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { TEST_TIMEOUT_LONG } from '../playwright.config.js' -import { slugs } from './shared.js' +import { collectionSlugs } from './shared.js' const { beforeAll, describe } = test const filename = fileURLToPath(import.meta.url) @@ -20,13 +20,17 @@ describe('field error states', () => { let validateDraftsOff: AdminUrlUtil let validateDraftsOn: AdminUrlUtil let validateDraftsOnAutosave: AdminUrlUtil + let prevValue: AdminUrlUtil + let prevValueRelation: AdminUrlUtil beforeAll(async ({ browser }, testInfo) => { testInfo.setTimeout(TEST_TIMEOUT_LONG) ;({ serverURL } = await initPayloadE2ENoConfig({ dirname })) - validateDraftsOff = new AdminUrlUtil(serverURL, slugs.validateDraftsOff) - validateDraftsOn = new AdminUrlUtil(serverURL, slugs.validateDraftsOn) - validateDraftsOnAutosave = new AdminUrlUtil(serverURL, slugs.validateDraftsOnAutosave) + validateDraftsOff = new AdminUrlUtil(serverURL, collectionSlugs.validateDraftsOff) + validateDraftsOn = new AdminUrlUtil(serverURL, collectionSlugs.validateDraftsOn) + validateDraftsOnAutosave = new AdminUrlUtil(serverURL, collectionSlugs.validateDraftsOnAutosave) + prevValue = new AdminUrlUtil(serverURL, collectionSlugs.prevValue) + prevValueRelation = new AdminUrlUtil(serverURL, collectionSlugs.prevValueRelation) const context = await browser.newContext() page = await context.newPage() initPageConsoleErrorCatch(page) @@ -87,4 +91,33 @@ describe('field error states', () => { await saveDocAndAssert(page, '#action-save', 'error') }) }) + + describe('previous values', () => { + test('should pass previous value into validate function', async () => { + // save original + await page.goto(prevValue.create) + await page.locator('#field-title').fill('original value') + await saveDocAndAssert(page) + await page.locator('#field-title').fill('original value 2') + await saveDocAndAssert(page) + + // create relation to doc + await page.goto(prevValueRelation.create) + await page.locator('#field-previousValueRelation .react-select').click() + await page.locator('#field-previousValueRelation .rs__option').first().click() + await saveDocAndAssert(page) + + // go back to doc + await page.goto(prevValue.list) + await page.locator('.row-1 a').click() + await page.locator('#field-description').fill('some description') + await saveDocAndAssert(page) + await page.locator('#field-title').fill('changed') + await saveDocAndAssert(page, '#action-save', 'error') + + // ensure value is the value before relationship association + await page.reload() + await expect(page.locator('#field-title')).toHaveValue('original value 2') + }) + }) }) diff --git a/test/field-error-states/globals/ValidateDraftsOn/index.ts b/test/field-error-states/globals/ValidateDraftsOn/index.ts index 9a702b96c0..fa92ebedd8 100644 --- a/test/field-error-states/globals/ValidateDraftsOn/index.ts +++ b/test/field-error-states/globals/ValidateDraftsOn/index.ts @@ -1,9 +1,9 @@ import type { GlobalConfig } from 'payload' -import { slugs } from '../../shared.js' +import { globalSlugs } from '../../shared.js' export const GlobalValidateDraftsOn: GlobalConfig = { - slug: slugs.globalValidateDraftsOn, + slug: globalSlugs.globalValidateDraftsOn, fields: [ { name: 'group', diff --git a/test/field-error-states/payload-types.ts b/test/field-error-states/payload-types.ts index f28635dd99..f2c4f3f91f 100644 --- a/test/field-error-states/payload-types.ts +++ b/test/field-error-states/payload-types.ts @@ -16,10 +16,15 @@ export interface Config { 'validate-drafts-on': ValidateDraftsOn; 'validate-drafts-off': ValidateDraftsOff; 'validate-drafts-on-autosave': ValidateDraftsOnAutosave; + 'prev-value': PrevValue; + 'prev-value-relation': PrevValueRelation; users: User; 'payload-preferences': PayloadPreference; 'payload-migrations': PayloadMigration; }; + db: { + defaultIDType: string; + }; globals: { 'global-validate-drafts-on': GlobalValidateDraftsOn; }; @@ -33,13 +38,16 @@ export interface UserAuthOperations { email: string; }; login: { - password: string; email: string; + password: string; }; registerFirstUser: { email: string; password: string; }; + unlock: { + email: string; + }; } /** * This interface was referenced by `Config`'s JSON-Schema @@ -312,6 +320,27 @@ export interface ValidateDraftsOnAutosave { createdAt: string; _status?: ('draft' | 'published') | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "prev-value". + */ +export interface PrevValue { + id: string; + title: string; + description?: string | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "prev-value-relation". + */ +export interface PrevValueRelation { + id: string; + previousValueRelation?: (string | null) | PrevValue; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-preferences". diff --git a/test/field-error-states/shared.ts b/test/field-error-states/shared.ts index 95e8fa464d..9941f1bcd7 100644 --- a/test/field-error-states/shared.ts +++ b/test/field-error-states/shared.ts @@ -1,6 +1,17 @@ -export const slugs = { - globalValidateDraftsOn: 'global-validate-drafts-on', +import type { CollectionSlug, GlobalSlug } from 'payload' + +export const collectionSlugs: { + [key: string]: CollectionSlug +} = { validateDraftsOff: 'validate-drafts-off', validateDraftsOn: 'validate-drafts-on', validateDraftsOnAutosave: 'validate-drafts-on-autosave', + prevValue: 'prev-value', + prevValueRelation: 'prev-value-relation', +} + +export const globalSlugs: { + [key: string]: GlobalSlug +} = { + globalValidateDraftsOn: 'global-validate-drafts-on', }