feat(admin, admin-ui, medusa-js, medusa-react, medusa): Support Admin Extensions (#4761)

Co-authored-by: Rares Stefan <948623+StephixOne@users.noreply.github.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Kasper Fabricius Kristensen
2023-08-17 14:14:45 +02:00
committed by GitHub
parent 26c78bbc03
commit f1a05f4725
189 changed files with 14570 additions and 12773 deletions
@@ -0,0 +1,133 @@
import { isForbiddenRoute } from "../constants/forbidden-routes"
import { Link, Route, RouteExtension, RouteSegment } from "../types/extensions"
type RouteNode = {
[pathSegment: string]: RouteNode | Route
}
class RouteRegistry {
private routes: RouteNode
private links: Link[]
constructor() {
this.routes = {}
this.links = []
}
register(origin: string, { Component, config }: RouteExtension) {
const { path, link } = config
const pathSegments = path.split("/").filter(Boolean)
let currentNode = this.routes
let finalSegment = ""
for (const segment of pathSegments) {
if (!currentNode[segment]) {
currentNode[segment] = {}
}
currentNode = currentNode[segment] as RouteNode
finalSegment = segment
}
if (currentNode.__route) {
if (process.env.NODE_ENV !== "production") {
console.warn(
`Route with path ${path} already registered by ${currentNode.__route.origin}.`
)
}
return
}
currentNode.__route = {
origin,
path: `/${finalSegment}`,
Page: Component,
}
if (link) {
this.links.push({
path,
label: link.label,
icon: link.icon,
})
}
}
getTopLevelRoutes(): (Route | RouteSegment)[] {
const topLevelRoutes = Object.entries(this.routes)
.filter(([key, _]) => !isForbiddenRoute(key))
.map(([key, node]) => {
return "__route" in node
? ((node as RouteNode).__route as Route)
: ({
path: `/${key}`,
} as RouteSegment)
})
return topLevelRoutes
}
getNestedRoutes(path: string) {
const segments = path.split("/").filter((s) => !!s)
const routes = this.routes
function getNode(segments: string[]) {
let currentNode: RouteNode | null = routes
for (const segment of segments) {
if (currentNode && currentNode[segment]) {
currentNode = currentNode[segment] as RouteNode
} else {
currentNode = null
break
}
}
return currentNode
}
const lastKnownNode = getNode(segments)
if (!lastKnownNode) {
return []
}
const nestedRoutes: Route[] = []
function getChildren(node: RouteNode, parent: string = "") {
const possiblePaths = Object.entries(node).filter(
([key, _value]) => key !== "__route"
)
for (const [key, value] of possiblePaths) {
const currentPath = `${parent}/${key}`
if (value && "__route" in value) {
const page = (value.__route as Route).Page
const origin = (value.__route as Route).origin
nestedRoutes.push({
origin,
path: currentPath,
Page: page,
})
}
if (value && typeof value === "object") {
getChildren(value as RouteNode, currentPath)
}
}
}
getChildren(lastKnownNode)
return nestedRoutes
}
getLinks(): Link[] {
return this.links
}
}
export default RouteRegistry
@@ -0,0 +1,60 @@
import { Card, Setting, SettingExtension } from "../types/extensions"
class SettingRegistry {
private settings: Record<string, Setting>
private cards: Card[]
constructor() {
this.settings = {}
this.cards = []
}
register(origin: string, { Component, config }: SettingExtension) {
const { path, card } = config
if (this.settings[path] !== undefined) {
if (process.env.NODE_ENV !== "production") {
console.warn(
`Settings page with path ${path} already registered by ${this.settings[path].origin}.`
)
}
return
}
this.settings[path] = {
origin,
path: path,
Page: Component,
}
this.cards.push({
path: path,
label: card.label,
description: card.description,
icon: card.icon,
})
}
/**
* Returns an array of settings sorted by path
* @returns {Setting[]} An array of settings sorted by path
*/
getSettings(): Setting[] {
return Object.values(this.settings).sort((a, b) => {
return a.path.localeCompare(b.path)
})
}
/**
* Returns an array of cards sorted by path
* @returns {Card[]} An array of cards sorted by path
*/
getCards(): Card[] {
return this.cards.sort((a, b) => {
return a.path.localeCompare(b.path)
})
}
}
export default SettingRegistry
@@ -0,0 +1,26 @@
import { InjectionZone, Widget, WidgetExtension } from "../types/extensions"
type Widgets = Map<InjectionZone, Widget[]>
class WidgetRegistry {
private widgets: Widgets = new Map()
public register(origin: string, widget: WidgetExtension) {
const { zone } = widget.config
const zones = Array.isArray(zone) ? zone : [zone]
for (const widgetZone of zones) {
const widgets = this.widgets.get(widgetZone) || []
widgets.push({ origin, Widget: widget.Component })
this.widgets.set(widgetZone, widgets)
}
}
public getWidgets(zone: InjectionZone) {
return this.widgets.get(zone) || []
}
}
export default WidgetRegistry