feat: add support for interfaceName on radio and select fields to create reusable top level types (#11277)

Adds support for `interfaceName` on radio and select fields. Adding this
property will extract your provided options into a top level type for
re-use.


![image](https://github.com/user-attachments/assets/be5c3e17-5127-4546-a778-d3aa801dec90)

Added types test to make sure assignment is consistent.
This commit is contained in:
Paul
2025-02-19 13:51:03 +00:00
committed by GitHub
parent bf103cc025
commit acead1083b
7 changed files with 103 additions and 3 deletions

View File

@@ -50,6 +50,7 @@ export const MyRadioField: Field = {
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |

View File

@@ -53,6 +53,7 @@ export const MySelectField: Field = {
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |

View File

@@ -1045,6 +1045,13 @@ export type SelectField = {
*/
enumName?: DBIdentifierName
hasMany?: boolean
/** Customize generated GraphQL and Typescript schema names.
* By default, it is bound to the collection.
*
* This is useful if you would like to generate a top level type to share amongst collections/fields.
* **Note**: Top level types can collide, ensure they are unique amongst collections, arrays, groups, blocks, tabs.
*/
interfaceName?: string
options: Option[]
type: 'select'
} & (
@@ -1062,7 +1069,7 @@ export type SelectField = {
export type SelectFieldClient = {
admin?: AdminClient & Pick<SelectField['admin'], 'isClearable' | 'isSortable'>
} & FieldBaseClient &
Pick<SelectField, 'hasMany' | 'options' | 'type'>
Pick<SelectField, 'hasMany' | 'interfaceName' | 'options' | 'type'>
type SharedRelationshipProperties = {
filterOptions?: FilterOptions
@@ -1269,6 +1276,13 @@ export type RadioField = {
* Customize the DB enum name
*/
enumName?: DBIdentifierName
/** Customize generated GraphQL and Typescript schema names.
* By default, it is bound to the collection.
*
* This is useful if you would like to generate a top level type to share amongst collections/fields.
* **Note**: Top level types can collide, ensure they are unique amongst collections, arrays, groups, blocks, tabs.
*/
interfaceName?: string
options: Option[]
type: 'radio'
validate?: RadioFieldValidation
@@ -1277,7 +1291,7 @@ export type RadioField = {
export type RadioFieldClient = {
admin?: AdminClient & Pick<RadioField['admin'], 'layout'>
} & FieldBaseClient &
Pick<RadioField, 'options' | 'type'>
Pick<RadioField, 'interfaceName' | 'options' | 'type'>
type BlockFields = {
[key: string]: any

View File

@@ -496,6 +496,14 @@ export function fieldsToJSONSchema(
enum: buildOptionEnums(field.options),
}
if (field.interfaceName) {
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
fieldSchema = {
$ref: `#/definitions/${field.interfaceName}`,
}
}
break
}
@@ -657,6 +665,15 @@ export function fieldsToJSONSchema(
fieldSchema.enum = optionEnums
}
}
if (field.interfaceName) {
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
fieldSchema = {
$ref: `#/definitions/${field.interfaceName}`,
}
}
break
}
break

View File

@@ -22,6 +22,38 @@ export default buildConfigWithDefaults({
type: 'text',
name: 'title',
},
{
name: 'selectField',
type: 'select',
required: true,
interfaceName: 'MySelectOptions',
options: [
{
label: 'Option 1',
value: 'option-1',
},
{
label: 'Option 2',
value: 'option-2',
},
],
},
{
name: 'radioField',
type: 'radio',
required: true,
interfaceName: 'MyRadioOptions',
options: [
{
label: 'Option 1',
value: 'option-1',
},
{
label: 'Option 2',
value: 'option-2',
},
],
},
],
},
{

View File

@@ -6,6 +6,16 @@
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "MySelectOptions".
*/
export type MySelectOptions = 'option-1' | 'option-2';
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "MyRadioOptions".
*/
export type MyRadioOptions = 'option-1' | 'option-2';
/**
* Supported timezones in IANA format.
*
@@ -132,6 +142,8 @@ export interface Post {
id: string;
text?: string | null;
title?: string | null;
selectField: MySelectOptions;
radioField: MyRadioOptions;
updatedAt: string;
createdAt: string;
}
@@ -249,6 +261,8 @@ export interface PayloadMigration {
export interface PostsSelect<T extends boolean = true> {
text?: T;
title?: T;
selectField?: T;
radioField?: T;
updatedAt?: T;
createdAt?: T;
}

View File

@@ -10,7 +10,14 @@ import type {
import payload from 'payload'
import { describe, expect, test } from 'tstyche'
import type { Menu, Post, User } from './payload-types.js'
import type {
Menu,
MyRadioOptions,
MySelectOptions,
Post,
SupportedTimezones,
User,
} from './payload-types.js'
const asType = <T>() => {
return '' as T
@@ -124,4 +131,18 @@ describe('Types testing', () => {
}>()
})
})
describe('generated types', () => {
test('has SupportedTimezones', () => {
expect<SupportedTimezones>().type.toBeAssignableTo<string>()
})
test('has global generated options interface based on select field', () => {
expect(asType<Post['selectField']>()).type.toBe<MySelectOptions>()
})
test('has global generated options interface based on radio field', () => {
expect(asType<Post['radioField']>()).type.toBe<MyRadioOptions>()
})
})
})