feat(medusa,dashboard,admin-sdk): Run admin dashboard from Medusa instance (#7330)

This commit is contained in:
Kasper Fabricius Kristensen authored and GitHub committed 2024-05-15 19:52:09 +02:00
1 parent ec5415ea1a
commit 490586f566
82 files changed
+3946 -788

No files matched your search

@@ -1 +0,0 @@
# cli
@@ -1,6 +0,0 @@
#!/usr/bin/env node
function start() {
return import("../dist/cli/index.mjs");
}
start();
@@ -1,34 +0,0 @@
{
"name": "@medusajs/admin-bundler",
"version": "0.0.0",
"scripts": {
"build": "rimraf dist && tsup"
},
"bin": {
"medusa-admin": "./bin/medusa-admin.js"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"module": "dist/index.mjs",
"files": [
"dist"
],
"devDependencies": {
"rimraf": "5.0.1",
"tsup": "^8.0.1",
"typescript": "^5.3.3"
},
"dependencies": {
"@medusajs/ui-preset": "^1.1.3",
"@medusajs/vite-plugin-extension": "*",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"commander": "^11.1.0",
"deepmerge": "^4.3.1",
"glob": "^7.1.6",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6",
"vite": "5.0.10"
},
"packageManager": "yarn@3.2.1"
}
@@ -1,22 +0,0 @@
import { resolve } from "path"
import { build as command } from "vite"
import { createViteConfig } from "./create-vite-config"
type BuildArgs = {
root?: string
}
export async function build({ root }: BuildArgs) {
const config = await createViteConfig({
build: {
outDir: resolve(process.cwd(), "build"),
},
})
if (!config) {
return
}
await command(config)
}
@@ -1,46 +0,0 @@
import { readFileSync } from "fs"
import glob from "glob"
import { relative, resolve } from "path"
import { build as command } from "vite"
type BundleArgs = {
root?: string | undefined
watch?: boolean | undefined
}
export async function bundle({ watch, root }: BundleArgs) {
const resolvedRoot = root
? resolve(process.cwd(), root)
: resolve(process.cwd(), "src", "admin")
const files = glob.sync(`${resolvedRoot}/**/*.{ts,tsx,js,jsx}`)
const input: Record<string, string> = {}
for (const file of files) {
const relativePath = relative(resolvedRoot, file)
input[relativePath] = file
}
const packageJson = JSON.parse(
readFileSync(resolve(process.cwd(), "package.json"), "utf-8")
)
const external = [
...Object.keys(packageJson.dependencies),
"@medusajs/ui",
"@medusajs/ui-preset",
"react",
"react-dom",
"react-router-dom",
"react-hook-form",
]
await command({
build: {
watch: watch ? {} : undefined,
rollupOptions: {
input: input,
external: external,
},
},
})
}
@@ -1,253 +0,0 @@
import inject from "@medusajs/vite-plugin-extension"
import react from "@vitejs/plugin-react"
import deepmerge from "deepmerge"
import { createRequire } from "module"
import path from "path"
import { type Config } from "tailwindcss"
import { ContentConfig } from "tailwindcss/types/config"
import { InlineConfig, Logger, createLogger, mergeConfig } from "vite"
const require = createRequire(import.meta.url)
export async function createViteConfig(
inline: InlineConfig
): Promise<InlineConfig | null> {
const root = process.cwd()
const logger = createCustomLogger()
let dashboardRoot: string | null = null
try {
dashboardRoot = path.dirname(require.resolve("@medusajs/dashboard"))
} catch (err) {
dashboardRoot = null
}
if (!dashboardRoot) {
logger.error(
"Unable to find @medusajs/dashboard. Please install it in your project, or specify the root directory."
)
return null
}
const { plugins, userConfig } = (await loadConfig(root, logger)) ?? {}
let viteConfig: InlineConfig = mergeConfig(inline, {
plugins: [
react(),
inject({
sources: plugins,
}),
],
configFile: false,
root: dashboardRoot,
css: {
postcss: {
plugins: [
require("tailwindcss")({
config: createTwConfig(process.cwd(), dashboardRoot),
}),
require("autoprefixer"),
],
},
},
} satisfies InlineConfig)
if (userConfig) {
viteConfig = await userConfig(viteConfig)
}
return viteConfig
}
function mergeTailwindConfigs(config1: Config, config2: Config): Config {
const content1 = config1.content
const content2 = config2.content
let mergedContent: ContentConfig
if (Array.isArray(content1) && Array.isArray(content2)) {
mergedContent = [...content1, ...content2]
} else if (!Array.isArray(content1) && !Array.isArray(content2)) {
mergedContent = {
files: [...content1.files, ...content2.files],
relative: content1.relative || content2.relative,
extract: { ...content1.extract, ...content2.extract },
transform: { ...content1.transform, ...content2.transform },
}
} else {
throw new Error("Cannot merge content fields of different types")
}
const mergedConfig = deepmerge(config1, config2)
mergedConfig.content = mergedContent
console.log(config1.presets, config2.presets)
// Ensure presets only contain unique values
mergedConfig.presets = config1.presets || []
return mergedConfig
}
function createTwConfig(root: string, dashboardRoot: string) {
const uiRoot = path.join(
path.dirname(require.resolve("@medusajs/ui")),
"**/*.{js,jsx,ts,tsx}"
)
const baseConfig: Config = {
presets: [require("@medusajs/ui-preset")],
content: [
`${root}/src/admin/**/*.{js,jsx,ts,tsx}`,
`${dashboardRoot}/src/**/*.{js,jsx,ts,tsx}`,
uiRoot,
],
darkMode: "class",
theme: {
extend: {},
},
plugins: [],
}
let userConfig: Config | null = null
const extensions = ["js", "cjs", "mjs", "ts", "cts", "mts"]
for (const ext of extensions) {
try {
userConfig = require(path.join(root, `tailwind.config.${ext}`))
break
} catch (err) {
console.log("Failed to load tailwind config with extension", ext, err)
userConfig = null
}
}
if (!userConfig) {
return baseConfig
}
return mergeTailwindConfigs(baseConfig, userConfig)
}
function createCustomLogger() {
const logger = createLogger("info", {
prefix: "medusa-admin",
})
const loggerInfo = logger.info
logger.info = (msg, opts) => {
if (
msg.includes("hmr invalidate") &&
msg.includes(
"Could not Fast Refresh. Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports"
)
) {
return
}
loggerInfo(msg, opts)
}
return logger
}
interface PluginOptions extends Record<string, unknown> {
enableUI?: boolean
}
type Plugin =
| string
| {
resolve: string
options?: PluginOptions
}
interface MedusaConfig extends Record<string, unknown> {
plugins?: Plugin[]
}
async function loadConfig(root: string, logger: Logger) {
const configPath = path.resolve(root, "medusa-config.js")
const config: MedusaConfig = await import(configPath)
.then((c) => c)
.catch((e) => {
if (e.code === "ERR_MODULE_NOT_FOUND") {
logger.warn(
"Root 'medusa-config.js' file not found; extensions won't load. If running Admin UI as a standalone app, use the 'standalone' option.",
{
timestamp: true,
}
)
} else {
logger.error(
`An error occured while attempting to load '${configPath}':\n${e}`,
{
timestamp: true,
}
)
}
return null
})
if (!config) {
return
}
if (!config.plugins?.length) {
logger.info(
"No plugins in 'medusa-config.js', no extensions will load. To enable Admin UI extensions, add them to the 'plugins' array in 'medusa-config.js'.",
{
timestamp: true,
}
)
return
}
const uiPlugins = config.plugins
.filter((p) => typeof p !== "string" && p.options?.enableUI)
.map((p: Plugin) => {
return typeof p === "string" ? p : p.resolve
})
const extensionSources = uiPlugins.map((p) => {
return path.resolve(require.resolve(p), "dist", "admin")
})
const rootSource = path.resolve(process.cwd(), "src", "admin")
extensionSources.push(rootSource)
const adminPlugin = config.plugins.find((p) =>
typeof p === "string"
? p === "@medusajs/admin"
: p.resolve === "@medusajs/admin"
)
if (!adminPlugin) {
logger.info(
"No @medusajs/admin in 'medusa-config.js', no extensions will load. To enable Admin UI extensions, add it to the 'plugins' array in 'medusa-config.js'.",
{
timestamp: true,
}
)
return
}
const adminPluginOptions =
typeof adminPlugin !== "string" && !!adminPlugin.options
? adminPlugin.options
: {}
const viteConfig = adminPluginOptions.withFinal as
| ((config: InlineConfig) => InlineConfig)
| ((config: InlineConfig) => Promise<InlineConfig>)
| undefined
return {
plugins: extensionSources,
userConfig: viteConfig,
}
}
@@ -1,28 +0,0 @@
import { createServer } from "vite"
// @ts-ignore
import { createViteConfig } from "./create-vite-config"
type DevArgs = {
port?: number | undefined
host?: string | boolean | undefined
}
export async function dev({ port = 5173, host }: DevArgs) {
const config = await createViteConfig({
server: {
port,
host,
},
})
if (!config) {
return
}
const server = await createServer(config)
await server.listen()
server.printUrls()
server.bindCLIShortcuts({ print: true })
}
@@ -1,28 +0,0 @@
import { Command } from "commander"
import { build } from "../api/build"
import { bundle } from "../api/bundle"
import { dev } from "../api/dev"
export async function createCli() {
const program = new Command()
program.name("medusa-admin")
program
.command("dev")
.description("Starts the development server")
.action(dev)
program
.command("build")
.description("Builds the admin dashboard")
.action(build)
program
.command("bundle")
.description("Bundles the admin dashboard")
.action(bundle)
return program
}
@@ -1,8 +0,0 @@
import { createCli } from "./create-cli"
createCli()
.then(async (cli) => cli.parseAsync(process.argv))
.catch((err) => {
console.error(err)
process.exit(1)
})
@@ -1,3 +0,0 @@
export { build } from "./api/build.js"
export { bundle } from "./api/bundle.js"
export { dev } from "./api/dev.js"
+1
View File
@@ -0,0 +1 @@
# `@medusajs/admin-sdk`
@@ -0,0 +1,44 @@
{
"name": "@medusajs/admin-sdk",
"version": "0.0.1",
"description": "Admin SDK for Medusa.",
"author": "Kasper Kristensen <kasper@medusajs.com>",
"scripts": {
"build": "tsup && copyfiles -f ./src/index.html ./src/entry.tsx ./src/index.css ./dist"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"package.json"
],
"devDependencies": {
"@medusajs/types": "^1.11.16",
"@types/compression": "^1.7.5",
"@types/connect-history-api-fallback": "^1.5.4",
"copyfiles": "^2.4.1",
"express": "^4.18.2",
"tsup": "^8.0.1",
"typescript": "^5.3.3"
},
"dependencies": {
"@medusajs/admin-vite-plugin": "0.0.1",
"@medusajs/dashboard": "0.0.1",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"commander": "^11.1.0",
"compression": "^1.7.4",
"connect-history-api-fallback": "^2.0.0",
"deepmerge": "^4.3.1",
"glob": "^7.1.6",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6",
"vite": "^5.2.11",
"vite-plugin-node-polyfills": "^0.21.0"
},
"peerDependencies": {
"express": "^4.18.2",
"react-dom": "^18.0.0"
},
"packageManager": "yarn@3.2.1"
}
@@ -0,0 +1,9 @@
import App from "@medusajs/dashboard"
import React from "react"
import { createRoot } from "react-dom/client"
import "./index.css"
const container = document.getElementById("root")
const root = createRoot(container!)
root.render(<App />)
@@ -0,0 +1,5 @@
@import "@medusajs/dashboard/css";
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<link rel="icon" href="data:," data-placeholder-favicon />
</head>
<body>
<div id="root"></div>
<script type="module" src="./entry.tsx"></script>
</body>
</html>
@@ -0,0 +1,5 @@
export { build } from "./lib/build"
export { develop } from "./lib/develop"
export { serve } from "./lib/serve"
export * from "./types"
@@ -0,0 +1,17 @@
import { BundlerOptions } from "../types"
import { getViteConfig } from "./config"
export async function build(options: BundlerOptions) {
const vite = await import("vite")
const viteConfig = await getViteConfig(options)
try {
await vite.build(
vite.mergeConfig(viteConfig, { mode: "production", logLevel: "silent" })
)
} catch (error) {
console.error(error)
throw new Error("Failed to build admin panel")
}
}
@@ -0,0 +1,111 @@
import path from "path"
import { Config } from "tailwindcss"
import type { InlineConfig } from "vite"
import { nodePolyfills } from "vite-plugin-node-polyfills"
import { BundlerOptions } from "../types"
export async function getViteConfig(
options: BundlerOptions
): Promise<InlineConfig> {
const { searchForWorkspaceRoot } = await import("vite")
const { default: react } = await import("@vitejs/plugin-react")
const { default: inject } = await import("@medusajs/admin-vite-plugin")
const getPort = await import("get-port")
const hmrPort = await getPort.default()
const root = path.resolve(__dirname, "./")
return {
root: path.resolve(__dirname, "./"),
base: options.path,
build: {
emptyOutDir: true,
outDir: path.resolve(process.cwd(), options.outDir),
},
optimizeDeps: {
include: ["@medusajs/dashboard", "react-dom/client"],
},
define: {
__BASE__: JSON.stringify(options.path),
/**
* TODO: Accept backend url from config to support hosting the admin elsewhere.
* The empty string should be the default value, as that ensures that requests
* are made to the server that serves the admin dashboard.
*/
__BACKEND_URL__: JSON.stringify(""),
},
server: {
open: true,
fs: {
allow: [
searchForWorkspaceRoot(process.cwd()),
path.resolve(__dirname, "../../medusa"),
path.resolve(__dirname, "../../app"),
],
},
hmr: {
port: hmrPort,
},
middlewareMode: true,
},
css: {
postcss: {
plugins: [
require("tailwindcss")({
config: createTailwindConfig(root),
}),
],
},
},
/**
* TODO: Remove polyfills, they are currently only required for the
* `axios` dependency in the dashboard. Once we have the new SDK,
* we should remove this, and leave it up to the user to include
* polyfills if they need them.
*/
plugins: [
react(),
inject(),
nodePolyfills({
include: ["crypto", "util", "stream"],
}),
],
}
}
function createTailwindConfig(entry: string) {
const root = path.join(entry, "**/*.{js,ts,jsx,tsx}")
const html = path.join(entry, "index.html")
let dashboard = ""
try {
dashboard = path.join(
path.dirname(require.resolve("@medusajs/dashboard")),
"**/*.{js,ts,jsx,tsx}"
)
} catch (_e) {
// ignore
}
let ui: string = ""
try {
ui = path.join(
path.dirname(require.resolve("@medusajs/ui")),
"**/*.{js,ts,jsx,tsx}"
)
} catch (_e) {
// ignore
}
const config: Config = {
presets: [require("@medusajs/ui-preset")],
content: [html, root, dashboard, ui],
darkMode: "class",
}
return config
}
@@ -0,0 +1,24 @@
import express from "express"
import { BundlerOptions } from "../types"
import { getViteConfig } from "./config"
const router = express.Router()
export async function develop(options: BundlerOptions) {
const vite = await import("vite")
try {
const viteConfig = await getViteConfig(options)
const server = await vite.createServer(
vite.mergeConfig(viteConfig, { logLevel: "info", mode: "development" })
)
router.use(server.middlewares)
} catch (error) {
console.error(error)
throw new Error("Could not start development server")
}
return router
}
@@ -0,0 +1,53 @@
import { Request, Response, Router, static as static_ } from "express"
import fs from "fs"
import { ServerResponse } from "http"
import path from "path"
type ServeOptions = {
outDir: string
}
const router = Router()
export async function serve(options: ServeOptions) {
const htmlPath = path.resolve(options.outDir, "index.html")
/**
* The admin UI should always be built at this point, but in the
* rare case that another plugin terminated a previous startup, the admin
* may not have been built correctly. Here we check if the admin UI
* build files exist, and if not, we throw an error, providing the
* user with instructions on how to fix their build.
*/
const indexExists = fs.existsSync(htmlPath)
if (!indexExists) {
throw new Error(
`Could not find the admin UI build files. Please run "medusa-admin build" or enable "autoRebuild" in the plugin options to build the admin UI.`
)
}
const html = fs.readFileSync(htmlPath, "utf-8")
const sendHtml = (_req: Request, res: Response) => {
res.setHeader("Cache-Control", "no-cache")
res.setHeader("Vary", "Origin, Cache-Control")
res.send(html)
}
const setStaticHeaders = (res: ServerResponse) => {
res.setHeader("Cache-Control", "max-age=31536000, immutable")
res.setHeader("Vary", "Origin, Cache-Control")
}
router.get("/", sendHtml)
router.use(
static_(options.outDir, {
setHeaders: setStaticHeaders,
})
)
router.get(`/*`, sendHtml)
return router
}
@@ -0,0 +1,4 @@
import { AdminOptions } from "@medusajs/types"
export type BundlerOptions = Required<Pick<AdminOptions, "outDir" | "path">> &
Pick<AdminOptions, "vite">
@@ -1,6 +1,8 @@
{
"compilerOptions": {
"jsx": "react",
"outDir": "dist",
"rootDir": "src",
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "bundler",
@@ -14,5 +16,6 @@
"esModuleInterop": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
"include": ["src"],
"exclude": ["tsup.config.ts", "node_modules", "dist"]
}
@@ -1,7 +1,8 @@
import { defineConfig } from "tsup"
export default defineConfig({
entry: ["./src/index.ts", "./src/cli/index.ts"],
format: ["esm"],
entry: ["src/index.ts"],
format: ["cjs"],
dts: true,
clean: true,
})
@@ -1,8 +1,14 @@
{
"name": "@medusajs/admin-shared",
"version": "0.0.0",
"description": "Shared code for Medusa admin packages.",
"version": "0.0.1",
"author": "Kasper Kristensen <kasper@medusajs.com>",
"types": "dist/index.d.ts",
"main": "dist/index.js",
"files": [
"dist",
"package.json"
],
"scripts": {
"build": "tsc"
},
@@ -1,6 +1,6 @@
{
"name": "@medusajs/vite-plugin-extension",
"version": "0.0.0",
"name": "@medusajs/admin-vite-plugin",
"version": "0.0.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"module": "dist/index.mjs",
@@ -11,19 +11,20 @@
}
},
"files": [
"dist"
"dist",
"package.json"
],
"scripts": {
"build": "tsup"
},
"devDependencies": {
"@babel/types": "7.22.5",
"@medusajs/admin-shared": "*",
"@medusajs/admin-shared": "0.0.1",
"@types/babel__traverse": "7.20.5",
"@types/node": "^20.10.4",
"tsup": "8.0.1",
"typescript": "5.3.3",
"vite": "5.0.10"
"vite": "^5.2.11"
},
"peerDependencies": {
"vite": "^5.0.0"
@@ -18,7 +18,7 @@ import { InjectionZone, injectionZones } from "@medusajs/admin-shared"
const traverse = (_traverse as any).default as typeof _traverse
const VIRTUAL_PREFIX = "/@virtual/medusajs-vite-plugin-extension/"
const VIRTUAL_PREFIX = "/@virtual/medusajs-admin-vite-plugin/"
const IMPORT_PREFIX = "medusa-admin:"
const WIDGET_MODULE = `${IMPORT_PREFIX}widgets/`
@@ -321,7 +321,7 @@ export default function inject(args?: InjectArgs): PluginOption {
async function generateWidgetEntrypoint(zone: InjectionZone) {
const files = (
await Promise.all(
Array.from(_sources).map((source) =>
Array.from(_sources).map(async (source) =>
traverseDirectory(`${source}/widgets`)
)
)
@@ -463,7 +463,7 @@ export default function inject(args?: InjectArgs): PluginOption {
async function generateRouteEntrypoint(get: "page" | "link") {
const files = (
await Promise.all(
Array.from(_sources).map((source) =>
Array.from(_sources).map(async (source) =>
traverseDirectory(`${source}/routes`, "page", { min: 1 })
)
)
@@ -496,11 +496,11 @@ export default function inject(args?: InjectArgs): PluginOption {
const exportString = `export default {
${get}s: [${validatedRoutes
.map((file, index) =>
get === "page"
.map((file, index) => {
return get === "page"
? `{ path: "${createPath(file)}", file: "${file}" }`
: `{ path: "${createPath(file)}", ...routeConfig${index}.link }`
)
})
.join(", ")}],
}`
@@ -610,7 +610,7 @@ export default function inject(args?: InjectArgs): PluginOption {
async function generateSettingEntrypoint(get: "page" | "card") {
const files = (
await Promise.all(
Array.from(_sources).map((source) =>
Array.from(_sources).map(async (source) =>
traverseDirectory(`${source}/settings`, "page", { min: 1, max: 1 })
)
)
@@ -643,11 +643,11 @@ export default function inject(args?: InjectArgs): PluginOption {
const exportString = `export default {
${get}s: [${validatedSettings
.map((file, index) =>
get === "page"
.map((file, index) => {
return get === "page"
? `{ path: "${createPath(file)}", file: "${file}" }`
: `{ path: "${createPath(file)}", ...settingConfig${index}.card }`
)
})
.join(", ")}],
}`
@@ -701,7 +701,7 @@ export default function inject(args?: InjectArgs): PluginOption {
const module = server.moduleGraph.getModuleById(moduleId)
if (module) {
server.reloadModule(module)
await server.reloadModule(module)
}
}
}
@@ -719,7 +719,7 @@ export default function inject(args?: InjectArgs): PluginOption {
const module = server.moduleGraph.getModuleById(fullModuleId)
if (module) {
server.reloadModule(module)
await server.reloadModule(module)
}
}
}
@@ -737,7 +737,7 @@ export default function inject(args?: InjectArgs): PluginOption {
const module = server.moduleGraph.getModuleById(fullModuleId)
if (module) {
server.reloadModule(module)
await server.reloadModule(module)
}
}
}
@@ -754,7 +754,7 @@ export default function inject(args?: InjectArgs): PluginOption {
if (module) {
_extensionGraph.delete(file)
server.reloadModule(module)
await server.reloadModule(module)
}
}
}
@@ -775,7 +775,7 @@ export default function inject(args?: InjectArgs): PluginOption {
}
return {
name: "@medusajs/vite-plugin-extension",
name: "@medusajs/admin-vite-plugin",
configureServer(s) {
server = s
logger = s.config.logger
@@ -835,8 +835,8 @@ export default function inject(args?: InjectArgs): PluginOption {
return
})
watcher.on("unlink", (file) => {
handleExtensionUnlink(file)
watcher.on("unlink", async (file) => {
await handleExtensionUnlink(file)
return
})
},
@@ -876,9 +876,9 @@ export default function inject(args?: InjectArgs): PluginOption {
return null
},
closeBundle() {
async closeBundle() {
if (watcher) {
watcher.close()
await watcher.close()
}
},
}
@@ -1,7 +1,7 @@
import { defineConfig } from "tsup";
import { defineConfig } from "tsup"
export default defineConfig({
entry: ["./src/index.ts"],
format: ["cjs", "esm"],
dts: true,
});
})
+26 -15
View File
@@ -1,19 +1,30 @@
{
"name": "@medusajs/dashboard",
"private": true,
"version": "0.0.0",
"version": "0.0.1",
"scripts": {
"generate:static": "node ./scripts/generate-countries.js && prettier --write ./src/lib/countries.ts && node ./scripts/generate-currencies.js && prettier --write ./src/lib/currencies.ts",
"dev": "vite",
"build": "vite build",
"build": "tsup && node ./scripts/generate-types.js",
"build:preview": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"main": "index.html",
"main": "dist/app.js",
"module": "dist/app.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/app.mjs",
"require": "./dist/app.js",
"types": "./dist/index.d.ts"
},
"./css": {
"import": "./dist/app.css",
"require": "./dist/app.css"
}
},
"files": [
"index.html",
"public",
"src",
"dist",
"package.json"
],
"dependencies": {
@@ -21,8 +32,8 @@
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@hookform/resolvers": "3.3.2",
"@medusajs/icons": "workspace:^",
"@medusajs/ui": "workspace:^",
"@medusajs/icons": "1.2.1",
"@medusajs/ui": "3.0.0",
"@radix-ui/react-collapsible": "1.0.3",
"@radix-ui/react-hover-card": "^1.0.7",
"@tanstack/react-query": "^5.28.14",
@@ -47,13 +58,12 @@
"react-jwt": "^1.2.0",
"react-resizable-panels": "^2.0.16",
"react-router-dom": "6.20.1",
"zod": "3.22.4"
"zod": "^3.22.4"
},
"devDependencies": {
"@medusajs/medusa": "workspace:^",
"@medusajs/types": "workspace:^",
"@medusajs/ui-preset": "workspace:^",
"@medusajs/vite-plugin-extension": "workspace:^",
"@medusajs/admin-vite-plugin": "0.0.1",
"@medusajs/types": "1.11.16",
"@medusajs/ui-preset": "1.1.3",
"@types/node": "^20.11.15",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
@@ -62,8 +72,9 @@
"postcss": "^8.4.33",
"prettier": "^3.1.1",
"tailwindcss": "^3.4.1",
"tsup": "^8.0.2",
"typescript": "5.2.2",
"vite": "5.0.10"
"vite": "^5.2.11"
},
"packageManager": "yarn@3.2.1"
}
@@ -0,0 +1,38 @@
/**
* We can't use the `tsc` command to generate types for the project because it
* will generate types for each file in the project, which isn't needed. We only
* need a single file that exports the App component.
*/
async function generateTypes() {
const fs = require("fs")
const path = require("path")
const distDir = path.resolve(__dirname, "../dist")
const filePath = path.join(distDir, "index.d.ts")
const fileContent = `
import * as react_jsx_runtime from "react/jsx-runtime"
declare const App: () => react_jsx_runtime.JSX.Element
export default App
`
// Ensure the dist directory exists
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir)
}
// Write the content to the index.d.ts file
fs.writeFileSync(filePath, fileContent.trim(), "utf8")
console.log(`File created at ${filePath}`)
}
;(async () => {
try {
await generateTypes()
} catch (e) {
console.error(e)
}
})()
@@ -1,14 +1,18 @@
import { Toaster } from "@medusajs/ui"
import { QueryClientProvider } from "@tanstack/react-query"
import { I18n } from "./components/utilities/i18n"
import { queryClient } from "./lib/medusa"
import { RouterProvider } from "./providers/router-provider"
import { ThemeProvider } from "./providers/theme-provider"
import "./index.css"
function App() {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<I18n />
<RouterProvider />
<Toaster />
</ThemeProvider>
@@ -64,9 +64,11 @@ export const FileUpload = ({
const fileList = Array.from(files)
const fileObj = fileList.map((file) => {
const id = Math.random().toString(36).substring(7)
const previewUrl = URL.createObjectURL(file)
return {
id: crypto.randomUUID(),
id: id,
url: previewUrl,
file,
}
@@ -13,13 +13,12 @@ import { Avatar, Text } from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible"
import { useTranslation } from "react-i18next"
import { ComponentType } from "react"
import { useStore } from "../../../hooks/api/store"
import { Skeleton } from "../../common/skeleton"
import { NavItem, NavItemProps } from "../../layout/nav-item"
import { Shell } from "../../layout/shell"
import extensions from "medusa-admin:routes/links"
export const MainLayout = () => {
return (
<Shell>
@@ -167,6 +166,10 @@ const CoreRouteSection = () => {
)
}
const extensions = {
links: null as { path: string; label: string; icon?: ComponentType }[] | null,
}
const ExtensionRouteSection = () => {
if (!extensions.links || extensions.links.length === 0) {
return null
@@ -1,7 +1,7 @@
import { DatePicker } from "@medusajs/ui"
import { ComponentPropsWithoutRef } from "react"
import { useTranslation } from "react-i18next"
import { languages } from "../../../i18n/config"
import { languages } from "../../../i18n/languages"
type LocalizedDatePickerProps = Omit<
ComponentPropsWithoutRef<typeof DatePicker>,
@@ -14,8 +14,9 @@ export const LocalizedDatePicker = ({
}: LocalizedDatePickerProps) => {
const { i18n, t } = useTranslation()
const locale = languages.find((lang) => lang.code === i18n.language)
?.date_locale
const locale = languages.find(
(lang) => lang.code === i18n.language
)?.date_locale
const translations = {
cancel: t("actions.cancel"),
@@ -0,0 +1,23 @@
import i18n from "i18next"
import LanguageDetector from "i18next-browser-languagedetector"
import { initReactI18next } from "react-i18next"
import { defaultI18nOptions } from "../../../i18n/config"
export const I18n = () => {
if (i18n.isInitialized) {
return null
}
i18n
.use(
new LanguageDetector(null, {
lookupCookie: "lng",
lookupLocalStorage: "lng",
})
)
.use(initReactI18next)
.init(defaultI18nOptions)
return null
}
@@ -0,0 +1 @@
export * from "./i18n"
@@ -2,7 +2,7 @@ import { format, formatDistance, sub } from "date-fns"
import { enUS } from "date-fns/locale"
import { useTranslation } from "react-i18next"
import { languages } from "../i18n/config"
import { languages } from "../i18n/languages"
export const useDate = () => {
const { i18n } = useTranslation()
@@ -1,34 +1,19 @@
import { enUS } from "date-fns/locale"
import i18n from "i18next"
import LanguageDetector from "i18next-browser-languagedetector"
import Backend, { type HttpBackendOptions } from "i18next-http-backend"
import { initReactI18next } from "react-i18next"
import { InitOptions } from "i18next"
import { Language } from "./types"
import translations from "./translations"
void i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init<HttpBackendOptions>({
fallbackLng: "en-US",
load: "languageOnly",
debug: process.env.NODE_ENV === "development",
interpolation: {
escapeValue: false,
},
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
})
export const languages: Language[] = [
{
code: "en-US",
display_name: "English (US)",
ltr: true,
date_locale: enUS,
export const defaultI18nOptions: InitOptions = {
debug: process.env.NODE_ENV === "development",
detection: {
caches: ["cookie", "localStorage", "header"],
lookupCookie: "lng",
lookupLocalStorage: "lng",
order: ["cookie", "localStorage", "header"],
},
]
export default i18n
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
resources: translations,
supportedLngs: Object.keys(translations),
}
@@ -0,0 +1,11 @@
import { enUS } from "date-fns/locale"
import { Language } from "./types"
export const languages: Language[] = [
{
code: "en-US",
display_name: "English (US)",
ltr: true,
date_locale: enUS,
},
]
@@ -0,0 +1,315 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"general": {
"type": "object",
"properties": {
"cancel": {
"type": "string"
},
"save": {
"type": "string"
},
"create": {
"type": "string"
},
"delete": {
"type": "string"
},
"edit": {
"type": "string"
},
"extensions": {
"type": "string"
},
"details": {
"type": "string"
}
},
"required": [
"cancel",
"save",
"create",
"createItem",
"delete",
"deleteItem",
"edit",
"editItem",
"extensions",
"details"
]
},
"products": {
"type": "object",
"properties": {
"domain": {
"type": "string"
},
"variants": {
"type": "string"
},
"availableInSalesChannels": {
"type": "string"
}
},
"required": ["domain", "variants", "availableInSalesChannels"]
},
"categories": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"collections": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"inventory": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"customers": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"customerGroups": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"orders": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"draftOrders": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"discounts": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"promotions": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"taxRegions": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"taxRates": {
"type": "object",
"properties": {
"domain": {
"type": "string"
},
"fields": {
"type": "object"
}
},
"required": ["domain"]
},
"campaigns": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"giftCards": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"pricing": {
"type": "object",
"properties": {
"domain": {
"type": "string"
}
},
"required": ["domain"]
},
"users": {
"type": "object",
"properties": {
"domain": {
"type": "string"
},
"role": {
"type": "string"
},
"roles": {
"type": "object",
"properties": {
"admin": {
"type": "string"
},
"member": {
"type": "string"
},
"developer": {
"type": "string"
}
},
"required": ["admin", "member", "developer"]
}
},
"required": ["domain", "role", "roles"]
},
"statuses": {
"type": "object",
"properties": {
"scheduled": {
"type": "string"
},
"expired": {
"type": "string"
},
"active": {
"type": "string"
},
"disabled": {
"type": "string"
}
}
},
"fields": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"password": {
"type": "string"
},
"subtitle": {
"type": "string"
},
"handle": {
"type": "string"
},
"type": {
"type": "string"
},
"category": {
"type": "string"
},
"categories": {
"type": "string"
},
"collection": {
"type": "string"
},
"discountable": {
"type": "string"
},
"tags": {
"type": "string"
},
"sales_channels": {
"type": "string"
}
},
"required": [
"title",
"description",
"name",
"email",
"password",
"subtitle",
"handle",
"type",
"category",
"categories",
"collection",
"discountable",
"tags",
"sales_channels"
]
}
},
"required": [
"general",
"products",
"categories",
"collections",
"inventory",
"customers",
"customerGroups",
"orders",
"draftOrders",
"discounts",
"giftCards",
"pricing",
"users",
"fields"
]
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,7 @@
import en from "./en.json"
export default {
en: {
translation: en,
},
}
@@ -1,8 +1,8 @@
import type { Locale } from "date-fns"
import en from "../../public/locales/en-US/translation.json"
import enUS from "./translations/en.json"
const resources = {
translation: en,
translation: enUS,
} as const
export type Resources = typeof resources
@@ -1,7 +1,6 @@
import { stringify } from "qs"
const baseUrl =
import.meta.env.VITE_MEDUSA_ADMIN_BACKEND_URL || "http://localhost:9000"
const baseUrl = __BACKEND_URL__ ?? "http://localhost:9000"
const commonHeaders: HeadersInit = {
Accept: "application/json",
@@ -1,8 +1,7 @@
import Medusa from "@medusajs/medusa-js"
import { QueryClient } from "@tanstack/react-query"
export const MEDUSA_BACKEND_URL =
import.meta.env.VITE_MEDUSA_ADMIN_BACKEND_URL || "http://localhost:9000"
export const MEDUSA_BACKEND_URL = __BACKEND_URL__ ?? "http://localhost:9000"
export const queryClient = new QueryClient({
defaultOptions: {
@@ -1,8 +1,6 @@
import React from "react"
import ReactDOM from "react-dom/client"
import App from "./app.js"
import "./i18n/config.js"
import "./index.css"
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
@@ -1,16 +1,18 @@
import { RouteObject } from "react-router-dom"
import routes from "medusa-admin:routes/pages"
// import routes from "medusa-admin:routes/pages"
export const RouteExtensions: RouteObject[] = []
/**
* UI Route extensions.
*/
export const RouteExtensions: RouteObject[] = routes.pages.map((ext) => {
return {
path: ext.path,
async lazy() {
const { default: Component } = await import(/* @vite-ignore */ ext.file)
return { Component }
},
}
})
// export const RouteExtensions: RouteObject[] = routes.pages.map((ext) => {
// return {
// path: ext.path,
// async lazy() {
// const { default: Component } = await import(/* @vite-ignore */ ext.file)
// return { Component }
// },
// }
// })
@@ -15,9 +15,9 @@ import {
import { Outlet, RouteObject } from "react-router-dom"
import { ProtectedRoute } from "../../components/authentication/protected-route"
import { ErrorBoundary } from "../../components/error/error-boundary"
import { MainLayout } from "../../components/layout/main-layout"
import { SettingsLayout } from "../../components/layout/settings-layout"
import { ErrorBoundary } from "../../components/utilities/error-boundary"
import { InventoryItemRes, PriceListRes } from "../../types/api-responses"
import { RouteExtensions } from "./route-extensions"
@@ -5,7 +5,9 @@ import {
import { RouteMap } from "./route-map"
const router = createBrowserRouter(RouteMap)
const router = createBrowserRouter(RouteMap, {
basename: __BASE__ || "/",
})
export const RouterProvider = () => {
return <Provider router={router} />
@@ -1,15 +1,19 @@
import settings from "medusa-admin:settings/pages"
import { RouteObject } from "react-router-dom"
// import settings from "medusa-admin:settings/pages"
/**
* UI Settings extensions.
*/
export const SettingsExtensions: RouteObject[] = settings.pages.map((ext) => {
return {
path: `/settings${ext.path}`,
async lazy() {
const { default: Component } = await import(/* @vite-ignore */ ext.file)
return { Component }
},
}
})
export const SettingsExtensions: RouteObject[] = []
// export const SettingsExtensions: RouteObject[] = settings.pages.map((ext) => {
// return {
// path: `/settings${ext.path}`,
// async lazy() {
// const { default: Component } = await import(/* @vite-ignore */ ext.file)
// return { Component }
// },
// }
// })
@@ -1,28 +1,30 @@
import { Container, Heading } from "@medusajs/ui";
import { Container, Heading } from "@medusajs/ui"
import after from "medusa-admin:widgets/product_category/details/after";
import before from "medusa-admin:widgets/product_category/details/before";
// import after from "medusa-admin:widgets/product_category/details/after"
// import before from "medusa-admin:widgets/product_category/details/before"
export const CategoryDetails = () => {
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
{/* {before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
);
})}
)
})} */}
<Container>
<Heading>Category</Heading>
</Container>
{after.widgets.map((w, i) => {
{/* {after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
);
})}
)
})} */}
</div>
);
};
)
}
@@ -1,28 +1,30 @@
import { Container, Heading } from "@medusajs/ui";
import { Container, Heading } from "@medusajs/ui"
import after from "medusa-admin:widgets/product_category/list/after";
import before from "medusa-admin:widgets/product_category/list/before";
// import after from "medusa-admin:widgets/product_category/list/after"
// import before from "medusa-admin:widgets/product_category/list/before"
export const CategoriesList = () => {
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
{/* {before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
);
})}
)
})} */}
<Container>
<Heading>Categories</Heading>
</Container>
{after.widgets.map((w, i) => {
{/* {after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
);
})}
)
})} */}
</div>
);
};
)
}
@@ -10,8 +10,8 @@ import { DiscountConditionsSection } from "./components/discount-conditions-sect
import { RedemptionsSection } from "./components/discount-redemptions-section"
import { discountLoader, expand } from "./loader"
import after from "medusa-admin:widgets/discount/details/after"
import before from "medusa-admin:widgets/discount/details/before"
// import after from "medusa-admin:widgets/discount/details/after"
// import before from "medusa-admin:widgets/discount/details/before"
export const DiscountDetail = () => {
const initialData = useLoaderData() as Awaited<
@@ -33,13 +33,14 @@ export const DiscountDetail = () => {
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
{/* {before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
})} */}
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2">
<DiscountGeneralSection discount={discount} />
@@ -49,13 +50,15 @@ export const DiscountDetail = () => {
<RedemptionsSection redemptions={discount.usage_count} />
<DetailsSection discount={discount} />
</div>
{after.widgets.map((w, i) => {
{/* {after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
})} */}
<JsonViewSection data={discount} />
</div>
<div className="hidden w-full max-w-[400px] flex-col gap-y-2 xl:flex">
@@ -1,18 +1,20 @@
import after from "medusa-admin:widgets/discount/list/after"
import before from "medusa-admin:widgets/discount/list/before"
// import after from "medusa-admin:widgets/discount/list/after"
// import before from "medusa-admin:widgets/discount/list/before"
import { DiscountListTable } from "./components/discount-list-table"
export const DiscountsList = () => {
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => (
{/* {before.widgets.map((w, i) => (
<w.Component key={i} />
))}
))} */}
<DiscountListTable />
{after.widgets.map((w, i) => (
{/* {after.widgets.map((w, i) => (
<w.Component key={i} />
))}
))} */}
</div>
)
}
@@ -9,12 +9,13 @@ import { ProductSalesChannelSection } from "./components/product-sales-channel-s
import { ProductVariantSection } from "./components/product-variant-section"
import { productLoader } from "./loader"
import after from "medusa-admin:widgets/product/details/after"
import before from "medusa-admin:widgets/product/details/before"
import sideAfter from "medusa-admin:widgets/product/details/side/after"
import sideBefore from "medusa-admin:widgets/product/details/side/before"
import { ProductOrganizationSection } from "./components/product-organization-section"
// import after from "medusa-admin:widgets/product/details/after"
// import before from "medusa-admin:widgets/product/details/before"
// import sideAfter from "medusa-admin:widgets/product/details/side/after"
// import sideBefore from "medusa-admin:widgets/product/details/side/before"
import { useProduct } from "../../../hooks/api/products"
import { ProductOrganizationSection } from "./components/product-organization-section"
// TODO: Use product domain translations only
export const ProductDetail = () => {
@@ -37,50 +38,50 @@ export const ProductDetail = () => {
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
{/* {before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
})} */}
<div className="flex flex-col gap-x-4 lg:flex-row lg:items-start">
<div className="w-full flex flex-col gap-y-2">
<div className="flex w-full flex-col gap-y-2">
<ProductGeneralSection product={product} />
<ProductMediaSection product={product} />
<ProductOptionSection product={product} />
<ProductVariantSection product={product} />
{after.widgets.map((w, i) => {
{/* {after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
})} */}
<div className="hidden lg:block">
<JsonViewSection data={product} root="product" />
</div>
</div>
<div className="w-full lg:max-w-[400px] max-w-[100%] mt-2 lg:mt-0 flex flex-col gap-y-2">
{sideBefore.widgets.map((w, i) => {
<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) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
})} */}
<ProductSalesChannelSection product={product} />
<ProductOrganizationSection product={product} />
<ProductAttributeSection product={product} />
{sideAfter.widgets.map((w, i) => {
{/* {sideAfter.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
})} */}
<div className="lg:hidden">
<JsonViewSection data={product} root="product" />
@@ -1,18 +1,9 @@
import after from "medusa-admin:widgets/product/list/after"
import before from "medusa-admin:widgets/product/list/before"
import { ProductListTable } from "./components/product-list-table"
export const ProductList = () => {
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => (
<w.Component key={i} />
))}
<ProductListTable />
{after.widgets.map((w, i) => (
<w.Component key={i} />
))}
</div>
)
}
@@ -9,15 +9,15 @@ import { useTranslation } from "react-i18next"
import { z } from "zod"
import { Link } from "react-router-dom"
import {
FileType,
FileUpload,
} from "../../../../../components/common/file-upload"
import { Form } from "../../../../../components/common/form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import {
FileType,
FileUpload,
} from "../../../../../components/common/file-upload"
import { useUpdateProduct } from "../../../../../hooks/api/products"
type ProductMediaViewProps = {
@@ -400,8 +400,10 @@ const getDefaultValues = (images: Image[] | null, thumbnail: string | null) => {
})) || []
if (thumbnail && !media.some((mediaItem) => mediaItem.url === thumbnail)) {
const id = Math.random().toString(36).substring(7)
media.unshift({
id: crypto.randomUUID(),
id: id,
url: thumbnail,
isThumbnail: true,
file: null,
@@ -2,7 +2,7 @@ import { UserDTO } from "@medusajs/types"
import { Button, Container, Heading, StatusBadge, Text } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { languages } from "../../../../../i18n/config"
import { languages } from "../../../../../i18n/languages"
type ProfileGeneralSectionProps = {
user: UserDTO
@@ -11,7 +11,7 @@ import {
useRouteModal,
} from "../../../../../components/route-modal"
import { useUpdateUser } from "../../../../../hooks/api/users"
import { languages } from "../../../../../i18n/config"
import { languages } from "../../../../../i18n/languages"
type EditProfileProps = {
user: Partial<Omit<UserDTO, "password_hash">>
@@ -7,9 +7,6 @@ import { PromotionConditionsSection } from "./components/promotion-conditions-se
import { PromotionGeneralSection } from "./components/promotion-general-section"
import { promotionLoader } from "./loader"
import after from "medusa-admin:widgets/promotion/details/after"
import before from "medusa-admin:widgets/promotion/details/before"
export const PromotionDetail = () => {
const initialData = useLoaderData() as Awaited<
ReturnType<typeof promotionLoader>
@@ -27,14 +24,6 @@ export const PromotionDetail = () => {
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2">
<PromotionGeneralSection promotion={promotion} />
@@ -57,14 +46,6 @@ export const PromotionDetail = () => {
<CampaignSection campaign={promotion.campaign!} />
</div>
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
<JsonViewSection data={promotion as any} />
</div>
@@ -1,20 +1,9 @@
import after from "medusa-admin:widgets/promotion/list/after"
import before from "medusa-admin:widgets/promotion/list/before"
import { PromotionListTable } from "./components/promotion-list-table"
export const PromotionsList = () => {
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => (
<w.Component key={i} />
))}
<PromotionListTable />
{after.widgets.map((w, i) => (
<w.Component key={i} />
))}
</div>
)
}
+3
View File
@@ -8,3 +8,6 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv
}
declare const __BACKEND_URL__: string | undefined
declare const __BASE__: string
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noImplicitAny": false,
"composite": true
}
}
@@ -0,0 +1,23 @@
import { defineConfig } from "tsup"
export default defineConfig({
entry: ["./src/app.tsx"],
format: ["cjs", "esm"],
external: [
"medusa-admin:settings/pages",
"medusa-admin:routes/pages",
"medusa-admin:widgets/promotion/list/after",
"medusa-admin:widgets/promotion/list/before",
"medusa-admin:widgets/promotion/details/after",
"medusa-admin:widgets/promotion/details/before",
"medusa-admin:widgets/product/list/after",
"medusa-admin:widgets/product/list/before",
"medusa-admin:widgets/product/details/after",
"medusa-admin:widgets/product/details/before",
"medusa-admin:widgets/product/details/side/after",
"medusa-admin:widgets/product/details/side/before",
"medusa-admin:routes/links",
],
tsconfig: "tsconfig.build.json",
clean: true,
})
+8 -12
View File
@@ -1,22 +1,18 @@
import inject from "@medusajs/vite-plugin-extension"
import inject from "@medusajs/admin-vite-plugin"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
const BASE = "/"
const BACKEND_URL = "http://localhost:9000"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), inject()],
define: {
__BASE__: JSON.stringify(BASE),
__BACKEND_URL__: JSON.stringify(BACKEND_URL),
},
server: {
open: true,
},
build: {
rollupOptions: {
output: {
manualChunks: {
react: ["react"],
"react-dom": ["react-dom"],
"react-router-dom": ["react-router-dom"],
},
},
},
},
})