feat(dashboard,admin-vite-plugin,admin-bundler,admin-sdk): Rework admin extensions and introduce custom fields API (#9338)

This commit is contained in:
Kasper Fabricius Kristensen
2024-10-09 11:44:40 +00:00
committed by GitHub
parent 35e69d32f2
commit d71343d6ab
159 changed files with 5264 additions and 2224 deletions
+3 -2
View File
@@ -1,2 +1,3 @@
export * from "./types"
export * from "./utils"
// We don't export anything related to CustomFields for the time being
export type { RouteConfig, WidgetConfig } from "./types"
export { defineRouteConfig, defineWidgetConfig } from "./utils"
+136 -3
View File
@@ -1,11 +1,144 @@
import type { InjectionZone } from "@medusajs/admin-shared"
import type {
CustomFieldFormKeys,
CustomFieldModel,
CustomFieldModelContainerMap,
CustomFieldModelFormTabsMap,
InjectionZone,
} from "@medusajs/admin-shared"
import type { ComponentType } from "react"
import { ZodFirstPartySchemaTypes } from "zod"
export type WidgetConfig = {
export interface WidgetConfig {
/**
* The injection zone or zones that the widget should be injected into.
*/
zone: InjectionZone | InjectionZone[]
}
export type RouteConfig = {
export interface RouteConfig {
/**
* An optional label to display in the sidebar. If not provided, the route will not be displayed in the sidebar.
*/
label?: string
/**
* An optional icon to display in the sidebar together with the label. If no label is provided, the icon will be ignored.
*/
icon?: ComponentType
}
export type CustomFormField<
TData = unknown,
TValidation extends ZodFirstPartySchemaTypes = ZodFirstPartySchemaTypes
> = {
/**
* The rules that the field should be validated against.
*
* @example
* ```ts
* rules: z.string().email() // The field must be a valid email
* ```
*/
validation: TValidation
/**
* The default value of the field.
*/
defaultValue: ((data: TData) => any) | any
/**
* The label of the field. If not provided, the label will be inferred from the field name.
*/
label?: string
/**
* The description of the field.
*/
description?: string
/**
* The placeholder of the field.
*/
placeholder?: string
/**
* Custom component to render the field. If not provided, the field will be rendered using the
* default component for the field type, which is determined by the field's validation schema.
*/
component?: ComponentType
}
// Define the main configuration type
export interface CustomFieldConfig<TModel extends CustomFieldModel> {
/**
* The name of the model that the custom models are linked to.
* This should be the name of one of the built-in models, such as `product` or `customer`.
*
* @example
* ```ts
* model: "product"
* ```
*/
model: TModel
/**
* The name of the custom model(s) that the custom fields belong to.
* This is used to ensure that the custom fields are fetched when
* querying the entrypoint model.
*
* @example
* ```ts
* export default unstable_defineCustomFieldsConfig({
* model: "product",
* link: "brand"
* // ...
* })
* ```
* or
* ```ts
* export default unstable_defineCustomFieldsConfig({
* model: "product",
* link: ["brand", "seller"]
* // ...
* })
* ```
*/
link: string | string[]
forms: Array<
{
[K in CustomFieldFormKeys<TModel> &
keyof CustomFieldModelFormTabsMap[TModel]]: {
/**
* The form to extend.
*
* @example
* ```ts
* export default unstable_defineCustomFieldsConfig({
* model: "product",
* link: "brand",
* forms: [
* {
* zone: "create",
* // ...
* }
* ],
* // ...
* })
* ```
*/
zone: K
fields: Record<string, CustomFormField<any, any>>
} & (CustomFieldModelFormTabsMap[TModel][K] extends never
? {}
: { tab: CustomFieldModelFormTabsMap[TModel][K] })
}[CustomFieldFormKeys<TModel> & keyof CustomFieldModelFormTabsMap[TModel]]
>
/**
* Optionally define how to display the custom fields, in an existing container on the entity details page.
* Alternatively, you can create a new widget to display the custom fields.
*/
displays?: Array<{
/**
* The identifier of the container that the custom fields should be injected into.
*/
zone: CustomFieldModelContainerMap[TModel]
/**
* The component that should be rendered to display the custom fields.
* This component will receive the entity data as a prop.
*/
component: ComponentType
}>
}
+83 -4
View File
@@ -1,8 +1,13 @@
import { RouteConfig, WidgetConfig } from "./types"
import type { CustomFieldModelFormMap } from "@medusajs/admin-shared"
import { z, ZodFirstPartySchemaTypes } from "zod"
import {
CustomFieldConfig,
CustomFormField,
RouteConfig,
WidgetConfig,
} from "./types"
function createConfigHelper<TConfig extends Record<string, unknown>>(
config: TConfig
): TConfig {
function createConfigHelper<TConfig>(config: TConfig): TConfig {
return {
...config,
/**
@@ -35,3 +40,77 @@ export function defineWidgetConfig(config: WidgetConfig) {
export function defineRouteConfig(config: RouteConfig) {
return createConfigHelper(config)
}
/**
* Define a custom fields configuration.
*
* @param config The custom fields configuration.
* @returns The custom fields configuration.
*
* @experimental This API is experimental and may change in the future.
*/
export function unstable_defineCustomFieldsConfig<
TModel extends keyof CustomFieldModelFormMap
>(config: CustomFieldConfig<TModel>) {
return createConfigHelper(config)
}
/**
* Creates a type-safe form builder.
*
* @returns The form helper.
*
* @example
* ```ts
* import { unstable_createFormHelper, unstable_defineCustomFieldsConfig } from "@medusajs/admin-sdk"
* import type { HttpTypes } from "@medusajs/types"
* import type { Brand } from "../../types/brand"
*
* type ExtendedProduct = HttpTypes.Product & {
* brand: Brand | null
* }
*
* const form = unstable_createFormHelper<ExtendedProduct>()
*
* export default unstable_defineCustomFieldsConfig({
* entryPoint: "product",
* link: "brand",
* forms: [{
* form: "create",
* fields: {
* brand_id: form.define({
* rules: form.string().nullish(),
* defaultValue: "",
* }),
* }
* }]
* })
* ```
*
* @experimental This API is experimental and may change in the future.
*/
export function unstable_createFormHelper<TData>() {
return {
/**
* Define a custom form field.
*
* @param field The field to define.
* @returns The field.
*/
define: <T extends ZodFirstPartySchemaTypes>(
field: Omit<CustomFormField<TData, T>, "validation"> & { validation: T }
): CustomFormField<TData, T> => {
return field as CustomFormField<TData, T>
},
string: () => z.string(),
number: () => z.number(),
boolean: () => z.boolean(),
date: () => z.date(),
array: z.array,
object: z.object,
null: () => z.null(),
nullable: z.nullable,
undefined: () => z.undefined(),
coerce: z.coerce,
}
}