feat(dashboard,admin-sdk,admin-shared,admin-vite-plugin): Add support for UI extensions (#7383)

* intial work

* update lock

* add routes and fix HMR of configs

* cleanup

* rm imports

* rm debug from plugin

* address feedback

* address feedback
This commit is contained in:
Kasper Fabricius Kristensen
2024-05-23 14:02:19 +02:00
committed by GitHub
parent 521c252dee
commit f1176a0673
50 changed files with 1366 additions and 1098 deletions

View File

@@ -0,0 +1,2 @@
export * from "./types"
export * from "./utils"

View File

@@ -0,0 +1,12 @@
import type { ComponentType } from "react"
import { InjectionZone } from "../widgets"
export type WidgetConfig = {
zone: InjectionZone | InjectionZone[]
}
export type RouteConfig = {
label?: string
icon?: ComponentType
}

View File

@@ -0,0 +1,37 @@
import { RouteConfig, WidgetConfig } from "./types"
function createConfigHelper<TConfig extends Record<string, unknown>>(
config: TConfig
): TConfig {
return {
...config,
/**
* This property is required to allow the config to be exported,
* while still allowing HMR to work correctly.
*
* It tricks Fast Refresh into thinking that the config is a React component,
* which allows it to be updated without a full page reload.
*/
$$typeof: Symbol.for("react.memo"),
}
}
/**
* Define a widget configuration.
*
* @param config The widget configuration.
* @returns The widget configuration.
*/
export function defineWidgetConfig(config: WidgetConfig) {
return createConfigHelper(config)
}
/**
* Define a route configuration.
*
* @param config The route configuration.
* @returns The route configuration.
*/
export function defineRouteConfig(config: RouteConfig) {
return createConfigHelper(config)
}

View File

@@ -0,0 +1 @@
export const ROUTE_IMPORTS = ["routes/pages", "routes/links"] as const

View File

@@ -0,0 +1,2 @@
export * from "./constants"
export * from "./types"

View File

@@ -0,0 +1,3 @@
import { ROUTE_IMPORTS } from "./constants"
export type RouteImport = (typeof ROUTE_IMPORTS)[number]

View File

@@ -0,0 +1,40 @@
import { ROUTE_IMPORTS } from "../routes"
import { INJECTION_ZONES } from "../widgets"
import { getVirtualId, getWidgetImport, resolveVirtualId } from "./utils"
const VIRTUAL_WIDGET_MODULES = INJECTION_ZONES.map((zone) => {
return getVirtualId(getWidgetImport(zone))
})
const VIRTUAL_ROUTE_MODULES = ROUTE_IMPORTS.map((route) => {
return getVirtualId(route)
})
/**
* All virtual modules that are used in the admin panel. Virtual modules are used
* to inject custom widgets, routes and settings. A virtual module is imported using
* a string that corresponds to the id of the virtual module.
*
* @example
* ```ts
* import ProductDetailsBefore from "virtual:medusa/widgets/product/details/before"
* ```
*/
export const VIRTUAL_MODULES = [
...VIRTUAL_WIDGET_MODULES,
...VIRTUAL_ROUTE_MODULES,
]
/**
* Reolved paths to all virtual widget modules.
*/
export const RESOLVED_WIDGET_MODULES = VIRTUAL_WIDGET_MODULES.map((id) => {
return resolveVirtualId(id)
})
/**
* Reolved paths to all virtual route modules.
*/
export const RESOLVED_ROUTE_MODULES = VIRTUAL_ROUTE_MODULES.map((id) => {
return resolveVirtualId(id)
})

View File

@@ -0,0 +1,2 @@
export * from "./constants"
export * from "./utils"

View File

@@ -0,0 +1,25 @@
import { InjectionZone } from "../widgets"
const PREFIX = "virtual:medusa/"
export const getVirtualId = (name: string) => {
return `${PREFIX}${name}`
}
export const resolveVirtualId = (id: string) => {
return `\0${id}`
}
export const getWidgetImport = (zone: InjectionZone) => {
return `widgets/${zone.replace(/\./g, "/")}`
}
export const getWidgetZone = (resolvedId: string): InjectionZone => {
const virtualPrefix = `\0${PREFIX}widgets/`
const zone = resolvedId
.replace(virtualPrefix, "")
.replace(/\//g, ".") as InjectionZone
return zone as InjectionZone
}

View File

@@ -1,64 +1,101 @@
export const injectionZones = [
// Order injection zones
const ORDER_INJECTION_ZONES = [
"order.details.before",
"order.details.after",
"order.list.before",
"order.list.after",
// Draft order injection zones
] as const
const DRAFT_ORDER_INJECTION_ZONES = [
"draft_order.list.before",
"draft_order.list.after",
"draft_order.details.before",
"draft_order.details.after",
// Customer injection zones
] as const
const CUSTOMER_INJECTION_ZONES = [
"customer.details.before",
"customer.details.after",
"customer.list.before",
"customer.list.after",
// Customer group injection zones
] as const
const CUSTOMER_GROUP_INJECTION_ZONES = [
"customer_group.details.before",
"customer_group.details.after",
"customer_group.list.before",
"customer_group.list.after",
// Product injection zones
] as const
const PRODUCT_INJECTION_ZONES = [
"product.details.before",
"product.details.after",
"product.list.before",
"product.list.after",
"product.details.side.before",
"product.details.side.after",
// Product collection injection zones
] as const
const PRODUCT_COLLECTION_INJECTION_ZONES = [
"product_collection.details.before",
"product_collection.details.after",
"product_collection.list.before",
"product_collection.list.after",
// Product category injection zones
] as const
const PRODUCT_CATEGORY_INJECTION_ZONES = [
"product_category.details.before",
"product_category.details.after",
"product_category.list.before",
"product_category.list.after",
// Price list injection zones
] as const
const PRICE_LIST_INJECTION_ZONES = [
"price_list.details.before",
"price_list.details.after",
"price_list.list.before",
"price_list.list.after",
// Discount injection zones
] as const
const DISCOUNT_INJECTION_ZONES = [
"discount.details.before",
"discount.details.after",
"discount.list.before",
"discount.list.after",
// Promotion injection zones
] as const
const PROMOTION_INJECTION_ZONES = [
"promotion.details.before",
"promotion.details.after",
"promotion.list.before",
"promotion.list.after",
// Gift card injection zones
] as const
const GIFT_CARD_INJECTION_ZONES = [
"gift_card.details.before",
"gift_card.details.after",
"gift_card.list.before",
"gift_card.list.after",
"custom_gift_card.before",
"custom_gift_card.after",
// Login
"login.before",
"login.after",
] as const
const LOGIN_INJECTION_ZONES = ["login.before", "login.after"] as const
/**
* All valid injection zones in the admin panel. An injection zone is a specific place
* in the admin panel where a plugin can inject custom widgets.
*/
export const INJECTION_ZONES = [
...ORDER_INJECTION_ZONES,
...DRAFT_ORDER_INJECTION_ZONES,
...CUSTOMER_INJECTION_ZONES,
...CUSTOMER_GROUP_INJECTION_ZONES,
...PRODUCT_INJECTION_ZONES,
...PRODUCT_COLLECTION_INJECTION_ZONES,
...PRODUCT_CATEGORY_INJECTION_ZONES,
...PRICE_LIST_INJECTION_ZONES,
...DISCOUNT_INJECTION_ZONES,
...PROMOTION_INJECTION_ZONES,
...GIFT_CARD_INJECTION_ZONES,
...LOGIN_INJECTION_ZONES,
] as const

View File

@@ -0,0 +1,3 @@
export * from "./constants"
export * from "./types"
export * from "./utils"

View File

@@ -0,0 +1,3 @@
import { INJECTION_ZONES } from "./constants"
export type InjectionZone = (typeof INJECTION_ZONES)[number]

View File

@@ -0,0 +1,9 @@
import { INJECTION_ZONES } from "./constants"
import { InjectionZone } from "./types"
/**
* Validates that the provided zone is a valid injection zone for a widget.
*/
export function isValidInjectionZone(zone: any): zone is InjectionZone {
return INJECTION_ZONES.includes(zone)
}

View File

@@ -1,2 +1,3 @@
export * from "./constants"
export * from "./types"
export * from "./extensions/config"
export * from "./extensions/virtual"
export * from "./extensions/widgets"

View File

@@ -1,3 +0,0 @@
import { injectionZones } from "./constants"
export type InjectionZone = (typeof injectionZones)[number]