From 6dea111d28374b25c0ebc24f3e3377f73986736e Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:18:11 +0200 Subject: [PATCH] feat: expose `req` to `defaultValue` function arguments (#9937) Rework of https://github.com/payloadcms/payload/pull/5912 ### What? Now, when `defaultValue` is defined as function you can receive the `req` argument: ```ts { name: 'defaultValueFromReq', type: 'text', defaultValue: async ({ req, user, locale }) => { return Promise.resolve(req.context.defaultValue) }, }, ``` `user` and `locale` even though are repeated in `req`, this potentially leaves some room to add more args in the future without removing them now. This also improves type for `defaultValue`: ```ts type SerializableValue = boolean | number | object | string export type DefaultValue = | ((args: { locale?: TypedLocale req: PayloadRequest user: PayloadRequest['user'] }) => SerializableValue) | SerializableValue ``` ### Why? To access the current URL / search params / Local API and other things directly in `defaultValue`. ### How? Passes `req` through everywhere where we call `defaultValue()` --- docs/fields/overview.mdx | 5 +++-- packages/payload/src/fields/config/types.ts | 6 +++--- packages/payload/src/fields/getDefaultValue.ts | 8 +++++--- .../payload/src/fields/hooks/afterRead/promise.ts | 1 + .../src/fields/hooks/beforeValidate/promise.ts | 1 + packages/payload/src/types/index.ts | 9 +++++++++ .../calculateDefaultValues/index.ts | 5 ++++- .../calculateDefaultValues/iterateFields.ts | 5 ++++- .../calculateDefaultValues/promise.ts | 12 ++++++++++-- .../ui/src/forms/fieldSchemasToFormState/index.tsx | 1 + test/fields/collections/Text/index.ts | 7 +++++++ test/fields/int.spec.ts | 14 ++++++++++++++ test/fields/payload-types.ts | 2 ++ 13 files changed, 64 insertions(+), 12 deletions(-) diff --git a/docs/fields/overview.mdx b/docs/fields/overview.mdx index 09352b0deb3..94687f51d1a 100644 --- a/docs/fields/overview.mdx +++ b/docs/fields/overview.mdx @@ -212,6 +212,7 @@ Functions can be written to make use of the following argument properties: - `user` - the authenticated user object - `locale` - the currently selected locale string +- `req` - the `PayloadRequest` object Here is an example of a `defaultValue` function: @@ -227,7 +228,7 @@ export const myField: Field = { name: 'attribution', type: 'text', // highlight-start - defaultValue: ({ user, locale }) => + defaultValue: ({ user, locale, req }) => `${translation[locale]} ${user.name}`, // highlight-end } @@ -235,7 +236,7 @@ export const myField: Field = { Tip: - You can use async `defaultValue` functions to fill fields with data from API requests. + You can use async `defaultValue` functions to fill fields with data from API requests or Local API using `req.payload`. ### Validation diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index 565eaeb6675..8eff3f25f2d 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -127,7 +127,7 @@ import type { TextareaFieldValidation, } from '../../index.js' import type { DocumentPreferences } from '../../preferences/types.js' -import type { Operation, PayloadRequest, Where } from '../../types/index.js' +import type { DefaultValue, Operation, PayloadRequest, Where } from '../../types/index.js' import type { NumberFieldManyValidation, NumberFieldSingleValidation, @@ -395,7 +395,7 @@ export interface FieldBase { admin?: Admin /** Extension point to add your custom data. Server only. */ custom?: Record - defaultValue?: any + defaultValue?: DefaultValue hidden?: boolean hooks?: { afterChange?: FieldHook[] @@ -1338,7 +1338,7 @@ export type BlocksField = { isSortable?: boolean } & Admin blocks: Block[] - defaultValue?: unknown + defaultValue?: DefaultValue labels?: Labels maxRows?: number minRows?: number diff --git a/packages/payload/src/fields/getDefaultValue.ts b/packages/payload/src/fields/getDefaultValue.ts index 583945aa2db..6b0d4a8e82f 100644 --- a/packages/payload/src/fields/getDefaultValue.ts +++ b/packages/payload/src/fields/getDefaultValue.ts @@ -1,10 +1,11 @@ -import type { JsonValue, PayloadRequest } from '../types/index.js' +import type { DefaultValue, JsonValue, PayloadRequest } from '../types/index.js' import { deepCopyObjectSimple } from '../utilities/deepCopyObject.js' type Args = { - defaultValue: ((args: any) => JsonValue) | any + defaultValue: DefaultValue locale: string | undefined + req: PayloadRequest user: PayloadRequest['user'] value?: JsonValue } @@ -12,6 +13,7 @@ type Args = { export const getDefaultValue = async ({ defaultValue, locale, + req, user, value, }: Args): Promise => { @@ -20,7 +22,7 @@ export const getDefaultValue = async ({ } if (defaultValue && typeof defaultValue === 'function') { - return await defaultValue({ locale, user }) + return await defaultValue({ locale, req, user }) } if (typeof defaultValue === 'object') { diff --git a/packages/payload/src/fields/hooks/afterRead/promise.ts b/packages/payload/src/fields/hooks/afterRead/promise.ts index 00fd083dbf3..509160a4df9 100644 --- a/packages/payload/src/fields/hooks/afterRead/promise.ts +++ b/packages/payload/src/fields/hooks/afterRead/promise.ts @@ -322,6 +322,7 @@ export const promise = async ({ siblingDoc[field.name] = await getDefaultValue({ defaultValue: field.defaultValue, locale, + req, user: req.user, value: siblingDoc[field.name], }) diff --git a/packages/payload/src/fields/hooks/beforeValidate/promise.ts b/packages/payload/src/fields/hooks/beforeValidate/promise.ts index 68e19c0584d..6334234fb9c 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/promise.ts @@ -316,6 +316,7 @@ export const promise = async ({ siblingData[field.name] = await getDefaultValue({ defaultValue: field.defaultValue, locale: req.locale, + req, user: req.user, value: siblingData[field.name], }) diff --git a/packages/payload/src/types/index.ts b/packages/payload/src/types/index.ts index 5c88ee67cda..1b52efb661a 100644 --- a/packages/payload/src/types/index.ts +++ b/packages/payload/src/types/index.ts @@ -121,6 +121,15 @@ export type Where = { export type Sort = Array | string +type SerializableValue = boolean | number | object | string +export type DefaultValue = + | ((args: { + locale?: TypedLocale + req: PayloadRequest + user: PayloadRequest['user'] + }) => SerializableValue) + | SerializableValue + /** * Applies pagination for join fields for including collection relationships */ diff --git a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/index.ts b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/index.ts index 2f4e67436ae..a9287deda29 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/index.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/index.ts @@ -1,4 +1,4 @@ -import type { Data, Field as FieldSchema, User } from 'payload' +import type { Data, Field as FieldSchema, PayloadRequest, User } from 'payload' import { iterateFields } from './iterateFields.js' @@ -7,6 +7,7 @@ type Args = { fields: FieldSchema[] id?: number | string locale: string | undefined + req: PayloadRequest siblingData: Data user: User } @@ -16,6 +17,7 @@ export const calculateDefaultValues = async ({ data, fields, locale, + req, user, }: Args): Promise => { await iterateFields({ @@ -23,6 +25,7 @@ export const calculateDefaultValues = async ({ data, fields, locale, + req, siblingData: data, user, }) diff --git a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/iterateFields.ts b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/iterateFields.ts index 7cb52a4f641..91505a593e9 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/iterateFields.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/iterateFields.ts @@ -1,4 +1,4 @@ -import type { Data, Field, TabAsField, User } from 'payload' +import type { Data, Field, PayloadRequest, TabAsField, User } from 'payload' import { defaultValuePromise } from './promise.js' @@ -7,6 +7,7 @@ type Args = { fields: (Field | TabAsField)[] id?: number | string locale: string | undefined + req: PayloadRequest siblingData: Data user: User } @@ -16,6 +17,7 @@ export const iterateFields = async ({ data, fields, locale, + req, siblingData, user, }: Args): Promise => { @@ -28,6 +30,7 @@ export const iterateFields = async ({ data, field, locale, + req, siblingData, user, }), diff --git a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/promise.ts b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/promise.ts index e9637a5f10b..6d5aafbe63a 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/promise.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/promise.ts @@ -1,4 +1,4 @@ -import type { Data, Field, TabAsField, User } from 'payload' +import type { Data, Field, PayloadRequest, TabAsField, User } from 'payload' import { getDefaultValue } from 'payload' import { fieldAffectsData, tabHasName } from 'payload/shared' @@ -10,6 +10,7 @@ type Args = { field: Field | TabAsField id?: number | string locale: string | undefined + req: PayloadRequest siblingData: Data user: User } @@ -20,6 +21,7 @@ export const defaultValuePromise = async ({ data, field, locale, + req, siblingData, user, }: Args): Promise => { @@ -31,6 +33,7 @@ export const defaultValuePromise = async ({ siblingData[field.name] = await getDefaultValue({ defaultValue: field.defaultValue, locale, + req, user, value: siblingData[field.name], }) @@ -52,6 +55,7 @@ export const defaultValuePromise = async ({ data, fields: field.fields, locale, + req, siblingData: row, user, }), @@ -81,6 +85,7 @@ export const defaultValuePromise = async ({ data, fields: block.fields, locale, + req, siblingData: row, user, }), @@ -94,13 +99,13 @@ export const defaultValuePromise = async ({ } case 'collapsible': - case 'row': { await iterateFields({ id, data, fields: field.fields, locale, + req, siblingData, user, }) @@ -119,6 +124,7 @@ export const defaultValuePromise = async ({ data, fields: field.fields, locale, + req, siblingData: groupData, user, }) @@ -143,6 +149,7 @@ export const defaultValuePromise = async ({ data, fields: field.fields, locale, + req, siblingData: tabSiblingData, user, }) @@ -156,6 +163,7 @@ export const defaultValuePromise = async ({ data, fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), locale, + req, siblingData, user, }) diff --git a/packages/ui/src/forms/fieldSchemasToFormState/index.tsx b/packages/ui/src/forms/fieldSchemasToFormState/index.tsx index f1a83694024..15fe1acc12c 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/index.tsx +++ b/packages/ui/src/forms/fieldSchemasToFormState/index.tsx @@ -84,6 +84,7 @@ export const fieldSchemasToFormState = async (args: Args): Promise => data: dataWithDefaultValues, fields, locale: req.locale, + req, siblingData: dataWithDefaultValues, user: req.user, }) diff --git a/test/fields/collections/Text/index.ts b/test/fields/collections/Text/index.ts index 58cfa252719..4f3fcf4e896 100644 --- a/test/fields/collections/Text/index.ts +++ b/test/fields/collections/Text/index.ts @@ -151,6 +151,13 @@ const TextFields: CollectionConfig = { hasMany: true, maxRows: 4, }, + { + name: 'defaultValueFromReq', + type: 'text', + defaultValue: async ({ req }) => { + return Promise.resolve(req.context.defaultValue) + }, + }, { name: 'disableListColumnText', type: 'text', diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 9f349a33faf..b2fedd36565 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -97,6 +97,20 @@ describe('Fields', () => { expect(fieldWithDefaultValue).toEqual(dependentOnFieldWithDefaultValue) }) + it('should populate function default values from req', async () => { + const text = await payload.create({ + req: { + context: { + defaultValue: 'from-context', + }, + }, + collection: 'text-fields', + data: { text: 'required' }, + }) + + expect(text.defaultValueFromReq).toBe('from-context') + }) + it('should localize an array of strings using hasMany', async () => { const localizedHasMany = ['hello', 'world'] const { id } = await payload.create({ diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index ccaf7608351..40cd06ddafb 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -783,6 +783,7 @@ export interface TextField { localizedHasMany?: string[] | null; withMinRows?: string[] | null; withMaxRows?: string[] | null; + defaultValueFromReq?: string | null; disableListColumnText?: string | null; disableListFilterText?: string | null; array?: @@ -2997,6 +2998,7 @@ export interface TextFieldsSelect { localizedHasMany?: T; withMinRows?: T; withMaxRows?: T; + defaultValueFromReq?: T; disableListColumnText?: T; disableListFilterText?: T; array?: