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()`
This commit is contained in:
Sasha
2024-12-16 20:18:11 +02:00
committed by GitHub
parent 26a10ed071
commit 6dea111d28
13 changed files with 64 additions and 12 deletions

View File

@@ -212,6 +212,7 @@ Functions can be written to make use of the following argument properties:
- `user` - the authenticated user object - `user` - the authenticated user object
- `locale` - the currently selected locale string - `locale` - the currently selected locale string
- `req` - the `PayloadRequest` object
Here is an example of a `defaultValue` function: Here is an example of a `defaultValue` function:
@@ -227,7 +228,7 @@ export const myField: Field = {
name: 'attribution', name: 'attribution',
type: 'text', type: 'text',
// highlight-start // highlight-start
defaultValue: ({ user, locale }) => defaultValue: ({ user, locale, req }) =>
`${translation[locale]} ${user.name}`, `${translation[locale]} ${user.name}`,
// highlight-end // highlight-end
} }
@@ -235,7 +236,7 @@ export const myField: Field = {
<Banner type="success"> <Banner type="success">
<strong>Tip:</strong> <strong>Tip:</strong>
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`.
</Banner> </Banner>
### Validation ### Validation

View File

@@ -127,7 +127,7 @@ import type {
TextareaFieldValidation, TextareaFieldValidation,
} from '../../index.js' } from '../../index.js'
import type { DocumentPreferences } from '../../preferences/types.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 { import type {
NumberFieldManyValidation, NumberFieldManyValidation,
NumberFieldSingleValidation, NumberFieldSingleValidation,
@@ -395,7 +395,7 @@ export interface FieldBase {
admin?: Admin admin?: Admin
/** Extension point to add your custom data. Server only. */ /** Extension point to add your custom data. Server only. */
custom?: Record<string, any> custom?: Record<string, any>
defaultValue?: any defaultValue?: DefaultValue
hidden?: boolean hidden?: boolean
hooks?: { hooks?: {
afterChange?: FieldHook[] afterChange?: FieldHook[]
@@ -1338,7 +1338,7 @@ export type BlocksField = {
isSortable?: boolean isSortable?: boolean
} & Admin } & Admin
blocks: Block[] blocks: Block[]
defaultValue?: unknown defaultValue?: DefaultValue
labels?: Labels labels?: Labels
maxRows?: number maxRows?: number
minRows?: number minRows?: number

View File

@@ -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' import { deepCopyObjectSimple } from '../utilities/deepCopyObject.js'
type Args = { type Args = {
defaultValue: ((args: any) => JsonValue) | any defaultValue: DefaultValue
locale: string | undefined locale: string | undefined
req: PayloadRequest
user: PayloadRequest['user'] user: PayloadRequest['user']
value?: JsonValue value?: JsonValue
} }
@@ -12,6 +13,7 @@ type Args = {
export const getDefaultValue = async ({ export const getDefaultValue = async ({
defaultValue, defaultValue,
locale, locale,
req,
user, user,
value, value,
}: Args): Promise<JsonValue> => { }: Args): Promise<JsonValue> => {
@@ -20,7 +22,7 @@ export const getDefaultValue = async ({
} }
if (defaultValue && typeof defaultValue === 'function') { if (defaultValue && typeof defaultValue === 'function') {
return await defaultValue({ locale, user }) return await defaultValue({ locale, req, user })
} }
if (typeof defaultValue === 'object') { if (typeof defaultValue === 'object') {

View File

@@ -322,6 +322,7 @@ export const promise = async ({
siblingDoc[field.name] = await getDefaultValue({ siblingDoc[field.name] = await getDefaultValue({
defaultValue: field.defaultValue, defaultValue: field.defaultValue,
locale, locale,
req,
user: req.user, user: req.user,
value: siblingDoc[field.name], value: siblingDoc[field.name],
}) })

View File

@@ -316,6 +316,7 @@ export const promise = async <T>({
siblingData[field.name] = await getDefaultValue({ siblingData[field.name] = await getDefaultValue({
defaultValue: field.defaultValue, defaultValue: field.defaultValue,
locale: req.locale, locale: req.locale,
req,
user: req.user, user: req.user,
value: siblingData[field.name], value: siblingData[field.name],
}) })

View File

@@ -121,6 +121,15 @@ export type Where = {
export type Sort = Array<string> | string export type Sort = Array<string> | 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 * Applies pagination for join fields for including collection relationships
*/ */

View File

@@ -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' import { iterateFields } from './iterateFields.js'
@@ -7,6 +7,7 @@ type Args = {
fields: FieldSchema[] fields: FieldSchema[]
id?: number | string id?: number | string
locale: string | undefined locale: string | undefined
req: PayloadRequest
siblingData: Data siblingData: Data
user: User user: User
} }
@@ -16,6 +17,7 @@ export const calculateDefaultValues = async ({
data, data,
fields, fields,
locale, locale,
req,
user, user,
}: Args): Promise<Data> => { }: Args): Promise<Data> => {
await iterateFields({ await iterateFields({
@@ -23,6 +25,7 @@ export const calculateDefaultValues = async ({
data, data,
fields, fields,
locale, locale,
req,
siblingData: data, siblingData: data,
user, user,
}) })

View File

@@ -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' import { defaultValuePromise } from './promise.js'
@@ -7,6 +7,7 @@ type Args<T> = {
fields: (Field | TabAsField)[] fields: (Field | TabAsField)[]
id?: number | string id?: number | string
locale: string | undefined locale: string | undefined
req: PayloadRequest
siblingData: Data siblingData: Data
user: User user: User
} }
@@ -16,6 +17,7 @@ export const iterateFields = async <T>({
data, data,
fields, fields,
locale, locale,
req,
siblingData, siblingData,
user, user,
}: Args<T>): Promise<void> => { }: Args<T>): Promise<void> => {
@@ -28,6 +30,7 @@ export const iterateFields = async <T>({
data, data,
field, field,
locale, locale,
req,
siblingData, siblingData,
user, user,
}), }),

View File

@@ -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 { getDefaultValue } from 'payload'
import { fieldAffectsData, tabHasName } from 'payload/shared' import { fieldAffectsData, tabHasName } from 'payload/shared'
@@ -10,6 +10,7 @@ type Args<T> = {
field: Field | TabAsField field: Field | TabAsField
id?: number | string id?: number | string
locale: string | undefined locale: string | undefined
req: PayloadRequest
siblingData: Data siblingData: Data
user: User user: User
} }
@@ -20,6 +21,7 @@ export const defaultValuePromise = async <T>({
data, data,
field, field,
locale, locale,
req,
siblingData, siblingData,
user, user,
}: Args<T>): Promise<void> => { }: Args<T>): Promise<void> => {
@@ -31,6 +33,7 @@ export const defaultValuePromise = async <T>({
siblingData[field.name] = await getDefaultValue({ siblingData[field.name] = await getDefaultValue({
defaultValue: field.defaultValue, defaultValue: field.defaultValue,
locale, locale,
req,
user, user,
value: siblingData[field.name], value: siblingData[field.name],
}) })
@@ -52,6 +55,7 @@ export const defaultValuePromise = async <T>({
data, data,
fields: field.fields, fields: field.fields,
locale, locale,
req,
siblingData: row, siblingData: row,
user, user,
}), }),
@@ -81,6 +85,7 @@ export const defaultValuePromise = async <T>({
data, data,
fields: block.fields, fields: block.fields,
locale, locale,
req,
siblingData: row, siblingData: row,
user, user,
}), }),
@@ -94,13 +99,13 @@ export const defaultValuePromise = async <T>({
} }
case 'collapsible': case 'collapsible':
case 'row': { case 'row': {
await iterateFields({ await iterateFields({
id, id,
data, data,
fields: field.fields, fields: field.fields,
locale, locale,
req,
siblingData, siblingData,
user, user,
}) })
@@ -119,6 +124,7 @@ export const defaultValuePromise = async <T>({
data, data,
fields: field.fields, fields: field.fields,
locale, locale,
req,
siblingData: groupData, siblingData: groupData,
user, user,
}) })
@@ -143,6 +149,7 @@ export const defaultValuePromise = async <T>({
data, data,
fields: field.fields, fields: field.fields,
locale, locale,
req,
siblingData: tabSiblingData, siblingData: tabSiblingData,
user, user,
}) })
@@ -156,6 +163,7 @@ export const defaultValuePromise = async <T>({
data, data,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
locale, locale,
req,
siblingData, siblingData,
user, user,
}) })

View File

@@ -84,6 +84,7 @@ export const fieldSchemasToFormState = async (args: Args): Promise<FormState> =>
data: dataWithDefaultValues, data: dataWithDefaultValues,
fields, fields,
locale: req.locale, locale: req.locale,
req,
siblingData: dataWithDefaultValues, siblingData: dataWithDefaultValues,
user: req.user, user: req.user,
}) })

View File

@@ -151,6 +151,13 @@ const TextFields: CollectionConfig = {
hasMany: true, hasMany: true,
maxRows: 4, maxRows: 4,
}, },
{
name: 'defaultValueFromReq',
type: 'text',
defaultValue: async ({ req }) => {
return Promise.resolve(req.context.defaultValue)
},
},
{ {
name: 'disableListColumnText', name: 'disableListColumnText',
type: 'text', type: 'text',

View File

@@ -97,6 +97,20 @@ describe('Fields', () => {
expect(fieldWithDefaultValue).toEqual(dependentOnFieldWithDefaultValue) 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 () => { it('should localize an array of strings using hasMany', async () => {
const localizedHasMany = ['hello', 'world'] const localizedHasMany = ['hello', 'world']
const { id } = await payload.create({ const { id } = await payload.create({

View File

@@ -783,6 +783,7 @@ export interface TextField {
localizedHasMany?: string[] | null; localizedHasMany?: string[] | null;
withMinRows?: string[] | null; withMinRows?: string[] | null;
withMaxRows?: string[] | null; withMaxRows?: string[] | null;
defaultValueFromReq?: string | null;
disableListColumnText?: string | null; disableListColumnText?: string | null;
disableListFilterText?: string | null; disableListFilterText?: string | null;
array?: array?:
@@ -2997,6 +2998,7 @@ export interface TextFieldsSelect<T extends boolean = true> {
localizedHasMany?: T; localizedHasMany?: T;
withMinRows?: T; withMinRows?: T;
withMaxRows?: T; withMaxRows?: T;
defaultValueFromReq?: T;
disableListColumnText?: T; disableListColumnText?: T;
disableListFilterText?: T; disableListFilterText?: T;
array?: array?: