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

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

View File

@@ -7,14 +7,7 @@
"access": "public",
"baseBranch": "develop",
"updateInternalDependencies": "patch",
"ignore": [
"integration-tests-api",
"integration-tests-modules",
"@medusajs/dashboard",
"@medusajs/admin-shared",
"@medusajs/admin-bundler",
"@medusajs/vite-plugin-extension"
],
"ignore": ["integration-tests-api", "integration-tests-modules"],
"snapshot": {
"useCalculatedVersion": true
},

View File

@@ -83,6 +83,8 @@ module.exports = {
"./packages/medusa/tsconfig.json",
"./packages/admin-next/dashboard/tsconfig.json",
"./packages/admin-next/admin-sdk/tsconfig.json",
"./packages/admin-next/admin-vite-plugin/tsconfig.json",
"./packages/inventory/tsconfig.spec.json",
"./packages/stock-location/tsconfig.spec.json",
@@ -267,5 +269,54 @@ module.exports = {
],
},
},
{
files: [
"packages/admin-next/app/**/*.ts",
"packages/admin-next/app/**/*.tsx",
],
plugins: ["unused-imports", "react-refresh"],
extends: [
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: "module", // Allows for the use of imports
project: "./packages/admin-next/app/tsconfig.json",
},
globals: {
__BASE__: "readonly",
},
env: {
browser: true,
},
rules: {
"prettier/prettier": "error",
"react/prop-types": "off",
"new-cap": "off",
"require-jsdoc": "off",
"valid-jsdoc": "off",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"no-unused-expressions": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
vars: "all",
varsIgnorePattern: "^_",
args: "after-used",
argsIgnorePattern: "^_",
},
],
},
},
],
}

View File

@@ -45,7 +45,7 @@ jobs:
uses: ./.github/actions/setup-server
with:
cache-extension: "cli-test"
node-version: "16.14"
node-version: 18
- name: Install Medusa cli
run: npm i -g @medusajs/medusa-cli@preview

View File

@@ -17,6 +17,9 @@ process.env.LOG_LEVEL = "error"
module.exports = {
plugins: [],
admin: {
disable: true,
},
projectConfig: {
redis_url: redisUrl,
database_url: DB_URL,

View File

@@ -31,6 +31,9 @@ const customFulfillmentProvider = {
}
module.exports = {
admin: {
disable: true,
},
plugins: [],
projectConfig: {
database_url: DB_URL,

View File

@@ -1 +0,0 @@
# cli

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env node
function start() {
return import("../dist/cli/index.mjs");
}
start();

View File

@@ -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"
}

View File

@@ -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)
}

View File

@@ -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,
},
},
})
}

View File

@@ -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,
}
}

View File

@@ -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 })
}

View File

@@ -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
}

View File

@@ -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)
})

View File

@@ -1,3 +0,0 @@
export { build } from "./api/build.js"
export { bundle } from "./api/bundle.js"
export { dev } from "./api/dev.js"

View File

@@ -0,0 +1 @@
# `@medusajs/admin-sdk`

View File

@@ -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"
}

View File

@@ -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 />)

View File

@@ -0,0 +1,5 @@
@import "@medusajs/dashboard/css";
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -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>

View File

@@ -0,0 +1,5 @@
export { build } from "./lib/build"
export { develop } from "./lib/develop"
export { serve } from "./lib/serve"
export * from "./types"

View File

@@ -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")
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,4 @@
import { AdminOptions } from "@medusajs/types"
export type BundlerOptions = Required<Pick<AdminOptions, "outDir" | "path">> &
Pick<AdminOptions, "vite">

View File

@@ -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"]
}

View File

@@ -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,
})

View File

@@ -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"
},

View File

@@ -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"

View File

@@ -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()
}
},
}

View File

@@ -1,7 +1,7 @@
import { defineConfig } from "tsup";
import { defineConfig } from "tsup"
export default defineConfig({
entry: ["./src/index.ts"],
format: ["cjs", "esm"],
dts: true,
});
})

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"
}

View File

@@ -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)
}
})()

View File

@@ -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>

View File

@@ -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,
}

View 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

View File

@@ -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"),

View File

@@ -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
}

View File

@@ -0,0 +1 @@
export * from "./i18n"

View File

@@ -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()

View File

@@ -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",
export const defaultI18nOptions: InitOptions = {
debug: process.env.NODE_ENV === "development",
detection: {
caches: ["cookie", "localStorage", "header"],
lookupCookie: "lng",
lookupLocalStorage: "lng",
order: ["cookie", "localStorage", "header"],
},
fallbackLng: "en",
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 default i18n
resources: translations,
supportedLngs: Object.keys(translations),
}

View File

@@ -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,
},
]

View File

@@ -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

View File

@@ -0,0 +1,7 @@
import en from "./en.json"
export default {
en: {
translation: en,
},
}

View File

@@ -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

View File

@@ -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",

View File

@@ -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: {

View File

@@ -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>

View File

@@ -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 }
// },
// }
// })

View File

@@ -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"

View File

@@ -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} />

View File

@@ -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 }
// },
// }
// })

View File

@@ -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>
);
};
)
}

View File

@@ -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>
);
};
)
}

View File

@@ -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">

View File

@@ -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>
)
}

View File

@@ -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" />

View File

@@ -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>
)
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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">>

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -8,3 +8,6 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv
}
declare const __BACKEND_URL__: string | undefined
declare const __BASE__: string

View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noImplicitAny": false,
"composite": true
}
}

View File

@@ -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,
})

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"],
},
},
},
},
})

View File

@@ -117,7 +117,7 @@ function buildLocalCommands(cli, isLocalProject) {
.option(`v2`, {
type: `boolean`,
describe: `Install Medusa with the V2 feature flag enabled. WARNING: Medusa V2 is still in development and shouldn't be used in production.`,
default: false
default: false,
}),
desc: `Create a new Medusa project.`,
handler: handlerP(newStarter),
@@ -238,6 +238,19 @@ function buildLocalCommands(cli, isLocalProject) {
})
),
})
.command({
command: `build`,
desc: `Build your project.`,
builder: (_) => _,
handler: handlerP(
getCommandHandler(`build`, (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
return new Promise((resolve) => {})
})
),
})
.command({
command: `start-cluster`,
desc: `Start development server in cluster mode (beta).`,

View File

@@ -28,6 +28,7 @@
"rimraf": "^5.0.1",
"typeorm": "^0.3.16",
"typescript": "^5.1.6",
"vite": "^5.2.11",
"winston": "^3.8.2"
},
"scripts": {

View File

@@ -3,8 +3,42 @@ import {
InternalModuleDeclaration,
} from "../modules-sdk"
import { RedisOptions } from "ioredis"
import { LoggerOptions } from "typeorm"
import type { RedisOptions } from "ioredis"
import type { LoggerOptions } from "typeorm"
import type { InlineConfig } from "vite"
/**
* @interface
*
* Admin dashboard configurations.
*/
export type AdminOptions = {
/**
* Whether to disable the admin dashboard. If set to `true`, the admin dashboard is disabled,
* in both development and production environments. The default value is `false`.
*/
disable?: boolean
/**
* The path to the admin dashboard. The default value is `/app`.
*
* The value cannot be one of the reserved paths:
* - `/admin`
* - `/store`
* - `/auth`
* - `/`
*/
path?: `/${string}`
/**
* The directory where the admin build is output. This is where the build process will place the generated files.
* The default value is `./build`.
*/
outDir?: string
/**
* Configure the Vite configuration for the admin dashboard. This function receives the default Vite configuration
* and returns the modified configuration. The default value is `undefined`.
*/
vite?: (config: InlineConfig) => InlineConfig
}
/**
* @interface
@@ -632,6 +666,11 @@ export type ConfigModule = {
*/
projectConfig: ProjectConfigOptions
/**
* Admin dashboard configurations.
*/
admin?: AdminOptions
/**
* On your Medusa backend, you can use [Plugins](https://docs.medusajs.com/development/plugins/overview) to add custom features or integrate third-party services.
* For example, installing a plugin to use Stripe as a payment processor.

View File

@@ -23,14 +23,12 @@
"license": "MIT",
"devDependencies": {
"@medusajs/types": "^1.11.16",
"@swc/core": "^1.4.8",
"@swc/jest": "^0.2.36",
"@types/express": "^4.17.17",
"@types/ioredis": "^4.28.10",
"@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191",
"@types/multer": "^1.4.7",
"@types/papaparse": "^5.3.7",
"cross-env": "^5.2.1",
"jest": "^25.5.4",
"medusa-interfaces": "^1.3.9",
@@ -51,6 +49,7 @@
"medusa-interfaces": "^1.3.7"
},
"dependencies": {
"@medusajs/admin-sdk": "0.0.1",
"@medusajs/core-flows": "^0.0.9",
"@medusajs/link-modules": "^0.2.11",
"@medusajs/medusa-cli": "^1.3.22",
@@ -58,6 +57,7 @@
"@medusajs/orchestration": "^0.5.7",
"@medusajs/utils": "^1.11.9",
"@medusajs/workflows-sdk": "^0.1.6",
"@swc/core": "^1.4.8",
"awilix": "^8.0.0",
"body-parser": "^1.19.0",
"boxen": "^5.0.1",

View File

@@ -0,0 +1,156 @@
import { ConfigModule } from "@medusajs/types"
import { transformFile } from "@swc/core"
import { getConfigFile } from "medusa-core-utils"
import fs from "node:fs/promises"
import path from "path"
type BuildArgs = {
directory: string
}
type FileConfig = {
inputDir: string
outputDir: string
targetExtension?: string
}
const INPUT_DIR = "./src"
const OUTPUT_DIR = "./dist"
const COMPILE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"]
const IGNORE_EXTENSIONS = [".md"]
async function clean(path: string) {
await fs.rm(path, { recursive: true }).catch(() => {})
}
async function findFiles(dir: string): Promise<string[]> {
try {
const files = await fs.readdir(dir, { withFileTypes: true })
const paths = await Promise.all(
files.map(async (file) => {
const res = path.join(dir, file.name)
return file.isDirectory() ? findFiles(res) : res
})
)
return paths.flat()
} catch (e) {
console.log(`Failed to read directory ${dir}`)
throw e
}
}
const getOutputPath = (file: string, config: FileConfig) => {
const { inputDir, outputDir, targetExtension } = config
const inputDirName = path.basename(inputDir)
const outputDirName = path.basename(outputDir)
const relativePath = file.replace(inputDirName, outputDirName)
let outputPath = relativePath
if (targetExtension) {
const currentExtension = path.extname(outputPath)
outputPath = outputPath.replace(currentExtension, targetExtension)
}
return outputPath
}
const writeToOut = async (
file: string,
content: string,
config: FileConfig
) => {
const outputPath = getOutputPath(file, config)
await fs.mkdir(outputPath.replace(/\/[^/]+$/, ""), { recursive: true })
await fs.writeFile(outputPath, content)
}
async function copyToOut(file: string, config: FileConfig) {
const outputPath = getOutputPath(file, config)
const dirNameRegex = new RegExp("\\" + path.sep + "([^\\" + path.sep + "]+)$")
await fs.mkdir(outputPath.replace(dirNameRegex, ""), { recursive: true })
await fs.copyFile(file, outputPath)
}
const medusaTransform = async (file: string) => {
if (COMPILE_EXTENSIONS.some((ext) => file.endsWith(ext))) {
const outputPath = getOutputPath(file, {
inputDir: INPUT_DIR,
outputDir: OUTPUT_DIR,
})
const output = await transformFile(file, {
sourceFileName: path.relative(path.dirname(outputPath), file),
sourceMaps: "inline",
module: {
type: "commonjs",
},
jsc: {
parser: {
syntax: "typescript",
decorators: true,
},
transform: {
decoratorMetadata: true,
},
},
})
await writeToOut(file, output.code, {
inputDir: INPUT_DIR,
outputDir: OUTPUT_DIR,
targetExtension: ".js",
})
} else if (!IGNORE_EXTENSIONS.some((ext) => file.endsWith(ext))) {
// Copy non-ts files
await copyToOut(file, { inputDir: INPUT_DIR, outputDir: OUTPUT_DIR })
}
}
export default async function ({ directory }: BuildArgs) {
const started = Date.now()
const { configModule, error } = getConfigFile<ConfigModule>(
directory,
"medusa-config"
)
if (error) {
console.log(`Failed to load medusa-config.js`)
process.exit(1)
}
const input = path.join(directory, INPUT_DIR)
const dist = path.join(directory, OUTPUT_DIR)
await clean(dist)
const files = await findFiles(input)
await Promise.all(files.map(medusaTransform))
const adminOptions = {
disable: false,
path: "/app" as const,
outDir: "./build",
...configModule.admin,
}
if (!adminOptions.disable) {
try {
const { build: buildProductionBuild } = await import(
"@medusajs/admin-sdk"
)
await buildProductionBuild(adminOptions)
} catch (error) {
console.log("Failed to build admin")
}
}
const time = Date.now() - started
console.log(`Build completed in ${time}ms`)
}

View File

@@ -0,0 +1,51 @@
import { AdminOptions, ConfigModule } from "@medusajs/types"
import { Express } from "express"
type Options = {
app: Express
configModule: ConfigModule
}
type IntializedOptions = Required<
Pick<AdminOptions, "path" | "disable" | "outDir">
> &
AdminOptions
export default async function adminLoader({ app, configModule }: Options) {
const { admin } = configModule
const adminOptions: IntializedOptions = {
disable: false,
path: "/app",
outDir: "./build",
...admin,
}
if (admin?.disable) {
return app
}
if (process.env.COMMAND_INITIATED_BY === "develop") {
return initDevelopmentServer(app, adminOptions)
}
return serveProductionBuild(app, adminOptions)
}
async function initDevelopmentServer(app: Express, options: IntializedOptions) {
const { develop } = await import("@medusajs/admin-sdk")
const adminMiddleware = await develop(options)
app.use(options.path, adminMiddleware)
return app
}
async function serveProductionBuild(app: Express, options: IntializedOptions) {
const { serve } = await import("@medusajs/admin-sdk")
const adminRoute = await serve(options)
app.use(options.path, adminRoute)
return app
}

View File

@@ -77,6 +77,7 @@ export default (rootDirectory: string): ConfigModule => {
...configModule?.projectConfig,
worker_mode,
},
admin: configModule?.admin ?? {},
modules: configModule.modules ?? {},
featureFlags: configModule?.featureFlags ?? {},
plugins: configModule?.plugins ?? [],

View File

@@ -7,6 +7,7 @@ import { createMedusaContainer } from "medusa-core-utils"
import requestIp from "request-ip"
import { v4 } from "uuid"
import { MedusaContainer } from "../types/global"
import adminLoader from "./admin"
import apiLoader from "./api"
import loadConfig from "./config"
import expressLoader from "./express"
@@ -54,6 +55,8 @@ async function loadEntrypoints(
next()
})
await adminLoader({ app: expressApp, configModule })
// subscribersLoader({ container })
await apiLoader({

1137
yarn.lock

File diff suppressed because it is too large Load Diff