feat(dashboard,admin-vite-plugin): Add product zones and fix zone change effect (#7427)
**What** - Adds injection zones to the product domain. - Fixes an issue where changing the `zone` in a widget config to another valid widget would not trigger a HMR event. - Fixes an issue where UI Routes would not work in production.
This commit is contained in:
@@ -424,8 +424,7 @@ function createRoutePath(file: string) {
|
|||||||
|
|
||||||
async function generateRouteEntrypoint(
|
async function generateRouteEntrypoint(
|
||||||
sources: Set<string>,
|
sources: Set<string>,
|
||||||
type: "page" | "link",
|
type: "page" | "link"
|
||||||
base = ""
|
|
||||||
) {
|
) {
|
||||||
const files = (
|
const files = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -464,7 +463,7 @@ async function generateRouteEntrypoint(
|
|||||||
${type}s: [${validatedRoutes
|
${type}s: [${validatedRoutes
|
||||||
.map((file, index) => {
|
.map((file, index) => {
|
||||||
return type === "page"
|
return type === "page"
|
||||||
? `{ path: "${createRoutePath(file)}", file: "${base + file}" }`
|
? `{ path: "${createRoutePath(file)}", Component: RouteExt${index} }`
|
||||||
: `{ path: "${createRoutePath(file)}", ...routeConfig${index} }`
|
: `{ path: "${createRoutePath(file)}", ...routeConfig${index} }`
|
||||||
})
|
})
|
||||||
.join(", ")}],
|
.join(", ")}],
|
||||||
@@ -496,7 +495,6 @@ export type MedusaVitePlugin = (config?: MedusaVitePluginOptions) => Vite.Plugin
|
|||||||
export const medusaVitePlugin: MedusaVitePlugin = (options) => {
|
export const medusaVitePlugin: MedusaVitePlugin = (options) => {
|
||||||
const _extensionGraph = new Map<string, Set<string>>()
|
const _extensionGraph = new Map<string, Set<string>>()
|
||||||
const _sources = new Set<string>(options?.sources ?? [])
|
const _sources = new Set<string>(options?.sources ?? [])
|
||||||
let _base = ""
|
|
||||||
|
|
||||||
let server: Vite.ViteDevServer | undefined
|
let server: Vite.ViteDevServer | undefined
|
||||||
let watcher: Vite.FSWatcher | undefined
|
let watcher: Vite.FSWatcher | undefined
|
||||||
@@ -507,7 +505,7 @@ export const medusaVitePlugin: MedusaVitePlugin = (options) => {
|
|||||||
return await generateWidgetEntrypoint(_sources, options.get)
|
return await generateWidgetEntrypoint(_sources, options.get)
|
||||||
}
|
}
|
||||||
case "route":
|
case "route":
|
||||||
return await generateRouteEntrypoint(_sources, options.get, _base)
|
return await generateRouteEntrypoint(_sources, options.get)
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -580,6 +578,55 @@ export const medusaVitePlugin: MedusaVitePlugin = (options) => {
|
|||||||
|
|
||||||
_extensionGraph.set(file, imports)
|
_extensionGraph.set(file, imports)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_extensionGraph.has(file)) {
|
||||||
|
const modules = _extensionGraph.get(file)
|
||||||
|
|
||||||
|
if (!modules) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const moduleId of modules) {
|
||||||
|
const module = server?.moduleGraph.getModuleById(moduleId)
|
||||||
|
|
||||||
|
if (!module || !module.id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedInjectionZone = getWidgetZone(module.id)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the widget is imported in a module that does not match the new
|
||||||
|
* zone value, we need to reload the module, so the widget will be removed.
|
||||||
|
*/
|
||||||
|
if (!zoneValues.includes(matchedInjectionZone)) {
|
||||||
|
modules.delete(moduleId)
|
||||||
|
await server?.reloadModule(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imports = new Set<string>(modules)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the widget is not currently being imported by the virtual module that
|
||||||
|
* matches its zone value, we need to reload the module, so the widget will be added.
|
||||||
|
*/
|
||||||
|
for (const zoneValue of zoneValues) {
|
||||||
|
const zonePath = getWidgetImport(zoneValue)
|
||||||
|
const moduleId = getVirtualId(zonePath)
|
||||||
|
const resolvedModuleId = resolveVirtualId(moduleId)
|
||||||
|
|
||||||
|
if (!modules.has(resolvedModuleId)) {
|
||||||
|
const module = server?.moduleGraph.getModuleById(resolvedModuleId)
|
||||||
|
if (module) {
|
||||||
|
imports.add(resolvedModuleId)
|
||||||
|
await server?.reloadModule(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_extensionGraph.set(file, imports)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event === "add") {
|
if (event === "add") {
|
||||||
@@ -732,16 +779,6 @@ export const medusaVitePlugin: MedusaVitePlugin = (options) => {
|
|||||||
return {
|
return {
|
||||||
name: "@medusajs/admin-vite-plugin",
|
name: "@medusajs/admin-vite-plugin",
|
||||||
enforce: "pre",
|
enforce: "pre",
|
||||||
configResolved(config) {
|
|
||||||
if (config.server?.middlewareMode) {
|
|
||||||
/**
|
|
||||||
* If we are in middleware mode, we need to set the base to the <base> + "@fs".
|
|
||||||
*
|
|
||||||
* This ensures that the page components are lazy-loaded correctly.
|
|
||||||
*/
|
|
||||||
_base = `${config.base}@fs`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
configureServer(_server) {
|
configureServer(_server) {
|
||||||
server = _server
|
server = _server
|
||||||
watcher = _server.watcher
|
watcher = _server.watcher
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { RouteObject } from "react-router-dom"
|
|||||||
export const settingsRouteRegex = /^\/settings\//
|
export const settingsRouteRegex = /^\/settings\//
|
||||||
|
|
||||||
export const createRouteMap = (
|
export const createRouteMap = (
|
||||||
routes: { path: string; file: string }[],
|
routes: { path: string; Component: () => JSX.Element }[],
|
||||||
ignore?: string
|
ignore?: string
|
||||||
): RouteObject[] => {
|
): RouteObject[] => {
|
||||||
const root: RouteObject[] = []
|
const root: RouteObject[] = []
|
||||||
|
|
||||||
const addRoute = (
|
const addRoute = (
|
||||||
pathSegments: string[],
|
pathSegments: string[],
|
||||||
file: string,
|
Component: () => JSX.Element,
|
||||||
currentLevel: RouteObject[]
|
currentLevel: RouteObject[]
|
||||||
) => {
|
) => {
|
||||||
if (!pathSegments.length) {
|
if (!pathSegments.length) {
|
||||||
@@ -33,23 +33,22 @@ export const createRouteMap = (
|
|||||||
route.children.push({
|
route.children.push({
|
||||||
path: "",
|
path: "",
|
||||||
async lazy() {
|
async lazy() {
|
||||||
const { default: Component } = await import(/* @vite-ignore */ file)
|
|
||||||
return { Component }
|
return { Component }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
route.children ||= []
|
route.children ||= []
|
||||||
addRoute(remainingSegments, file, route.children)
|
addRoute(remainingSegments, Component, route.children)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
routes.forEach(({ path, file }) => {
|
routes.forEach(({ path, Component }) => {
|
||||||
// Remove the ignore segment from the path if it is provided
|
// Remove the ignore segment from the path if it is provided
|
||||||
const cleanedPath = ignore
|
const cleanedPath = ignore
|
||||||
? path.replace(ignore, "").replace(/^\/+/, "")
|
? path.replace(ignore, "").replace(/^\/+/, "")
|
||||||
: path.replace(/^\/+/, "")
|
: path.replace(/^\/+/, "")
|
||||||
const pathSegments = cleanedPath.split("/").filter(Boolean)
|
const pathSegments = cleanedPath.split("/").filter(Boolean)
|
||||||
addRoute(pathSegments, file, root)
|
addRoute(pathSegments, Component, root)
|
||||||
})
|
})
|
||||||
|
|
||||||
return root
|
return root
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ declare module "virtual:medusa/widgets/*" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare module "virtual:medusa/routes/pages" {
|
declare module "virtual:medusa/routes/pages" {
|
||||||
const pages: { path: string; file: string }[]
|
const pages: { path: string; Component: () => JSX.Element }[]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
pages,
|
pages,
|
||||||
|
|||||||
+11
-16
@@ -1,23 +1,20 @@
|
|||||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import { useProduct } from "../../../hooks/api/products"
|
||||||
import { ProductAttributeSection } from "./components/product-attribute-section"
|
import { ProductAttributeSection } from "./components/product-attribute-section"
|
||||||
import { ProductGeneralSection } from "./components/product-general-section"
|
import { ProductGeneralSection } from "./components/product-general-section"
|
||||||
import { ProductMediaSection } from "./components/product-media-section"
|
import { ProductMediaSection } from "./components/product-media-section"
|
||||||
import { ProductOptionSection } from "./components/product-option-section"
|
import { ProductOptionSection } from "./components/product-option-section"
|
||||||
|
import { ProductOrganizationSection } from "./components/product-organization-section"
|
||||||
import { ProductSalesChannelSection } from "./components/product-sales-channel-section"
|
import { ProductSalesChannelSection } from "./components/product-sales-channel-section"
|
||||||
import { ProductVariantSection } from "./components/product-variant-section"
|
import { ProductVariantSection } from "./components/product-variant-section"
|
||||||
import { productLoader } from "./loader"
|
import { productLoader } from "./loader"
|
||||||
|
|
||||||
// import after from "medusa-admin:widgets/product/details/after"
|
import after from "virtual:medusa/widgets/product/details/after"
|
||||||
// @ts-ignore - virtual module
|
|
||||||
// import obj from "virtual:config"
|
|
||||||
import before from "virtual:medusa/widgets/product/details/before"
|
import before from "virtual:medusa/widgets/product/details/before"
|
||||||
// import sideAfter from "medusa-admin:widgets/product/details/side/after"
|
import sideAfter from "virtual:medusa/widgets/product/details/side/after"
|
||||||
// import sideBefore from "medusa-admin:widgets/product/details/side/before"
|
import sideBefore from "virtual:medusa/widgets/product/details/side/before"
|
||||||
|
|
||||||
import { useProduct } from "../../../hooks/api/products"
|
|
||||||
import { ProductOrganizationSection } from "./components/product-organization-section"
|
|
||||||
|
|
||||||
// TODO: Use product domain translations only
|
// TODO: Use product domain translations only
|
||||||
export const ProductDetail = () => {
|
export const ProductDetail = () => {
|
||||||
@@ -53,37 +50,35 @@ export const ProductDetail = () => {
|
|||||||
<ProductMediaSection product={product} />
|
<ProductMediaSection product={product} />
|
||||||
<ProductOptionSection product={product} />
|
<ProductOptionSection product={product} />
|
||||||
<ProductVariantSection product={product} />
|
<ProductVariantSection product={product} />
|
||||||
{/* {after.widgets.map((w, i) => {
|
{after.widgets.map((w, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
<w.Component />
|
<w.Component />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})} */}
|
})}
|
||||||
|
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
<JsonViewSection data={product} root="product" />
|
<JsonViewSection data={product} root="product" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 lg:mt-0 lg:max-w-[400px]">
|
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 lg:mt-0 lg:max-w-[400px]">
|
||||||
{/* {sideBefore.widgets.map((w, i) => {
|
{sideBefore.widgets.map((w, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
<w.Component />
|
<w.Component />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})} */}
|
})}
|
||||||
<ProductSalesChannelSection product={product} />
|
<ProductSalesChannelSection product={product} />
|
||||||
<ProductOrganizationSection product={product} />
|
<ProductOrganizationSection product={product} />
|
||||||
<ProductAttributeSection product={product} />
|
<ProductAttributeSection product={product} />
|
||||||
{/* {sideAfter.widgets.map((w, i) => {
|
{sideAfter.widgets.map((w, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
<w.Component />
|
<w.Component />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})} */}
|
})}
|
||||||
|
|
||||||
<div className="lg:hidden">
|
<div className="lg:hidden">
|
||||||
<JsonViewSection data={product} root="product" />
|
<JsonViewSection data={product} root="product" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
import { ProductListTable } from "./components/product-list-table"
|
import { ProductListTable } from "./components/product-list-table"
|
||||||
|
|
||||||
|
import after from "virtual:medusa/widgets/product/list/after"
|
||||||
|
import before from "virtual:medusa/widgets/product/list/before"
|
||||||
|
|
||||||
export const ProductList = () => {
|
export const ProductList = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
|
{before.widgets.map((w, i) => {
|
||||||
|
return (
|
||||||
|
<div key={i}>
|
||||||
|
<w.Component />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
<ProductListTable />
|
<ProductListTable />
|
||||||
|
{after.widgets.map((w, i) => {
|
||||||
|
return (
|
||||||
|
<div key={i}>
|
||||||
|
<w.Component />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
import inject from "@medusajs/admin-vite-plugin"
|
import inject from "@medusajs/admin-vite-plugin"
|
||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react"
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig, loadEnv } from "vite"
|
||||||
|
|
||||||
const BASE = "/"
|
|
||||||
const BACKEND_URL = "http://localhost:9000"
|
|
||||||
|
|
||||||
console.log(inject)
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
plugins: [
|
const env = loadEnv(mode, process.cwd())
|
||||||
react(),
|
|
||||||
inject({
|
const BASE = env.VITE_MEDUSA_BASE || "/"
|
||||||
debug: true,
|
const BACKEND_URL = env.VITE_MEDUSA_BACKEND_URL || "http://localhost:9000"
|
||||||
sources: ["/Users/kasperkristensen/work/v2-server/src/admin"],
|
|
||||||
}),
|
/**
|
||||||
],
|
* Add this to your .env file to specify the project to load admin extensions from.
|
||||||
define: {
|
*/
|
||||||
__BASE__: JSON.stringify(BASE),
|
const MEDUSA_PROJECT = env.VITE_MEDUSA_PROJECT || null
|
||||||
__BACKEND_URL__: JSON.stringify(BACKEND_URL),
|
const sources = MEDUSA_PROJECT ? [MEDUSA_PROJECT] : []
|
||||||
},
|
|
||||||
server: {
|
return {
|
||||||
open: true,
|
plugins: [
|
||||||
},
|
react(),
|
||||||
|
inject({
|
||||||
|
sources,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
define: {
|
||||||
|
__BASE__: JSON.stringify(BASE),
|
||||||
|
__BACKEND_URL__: JSON.stringify(BACKEND_URL),
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user