feat(medusa,dashboard,admin-sdk): Run admin dashboard from Medusa instance (#7330)
This commit is contained in:
committed by
GitHub
parent
ec5415ea1a
commit
490586f566
@@ -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
|
||||
},
|
||||
|
||||
51
.eslintrc.js
51
.eslintrc.js
@@ -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: "^_",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
2
.github/workflows/test-cli-with-database.yml
vendored
2
.github/workflows/test-cli-with-database.yml
vendored
@@ -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
|
||||
|
||||
@@ -17,6 +17,9 @@ process.env.LOG_LEVEL = "error"
|
||||
|
||||
module.exports = {
|
||||
plugins: [],
|
||||
admin: {
|
||||
disable: true,
|
||||
},
|
||||
projectConfig: {
|
||||
redis_url: redisUrl,
|
||||
database_url: DB_URL,
|
||||
|
||||
@@ -31,6 +31,9 @@ const customFulfillmentProvider = {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
admin: {
|
||||
disable: true,
|
||||
},
|
||||
plugins: [],
|
||||
projectConfig: {
|
||||
database_url: DB_URL,
|
||||
|
||||
@@ -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
packages/admin-next/admin-sdk/README.md
Normal file
1
packages/admin-next/admin-sdk/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# `@medusajs/admin-sdk`
|
||||
44
packages/admin-next/admin-sdk/package.json
Normal file
44
packages/admin-next/admin-sdk/package.json
Normal 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"
|
||||
}
|
||||
9
packages/admin-next/admin-sdk/src/entry.tsx
Normal file
9
packages/admin-next/admin-sdk/src/entry.tsx
Normal 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 />)
|
||||
5
packages/admin-next/admin-sdk/src/index.css
Normal file
5
packages/admin-next/admin-sdk/src/index.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@import "@medusajs/dashboard/css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
13
packages/admin-next/admin-sdk/src/index.html
Normal file
13
packages/admin-next/admin-sdk/src/index.html
Normal 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>
|
||||
5
packages/admin-next/admin-sdk/src/index.ts
Normal file
5
packages/admin-next/admin-sdk/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { build } from "./lib/build"
|
||||
export { develop } from "./lib/develop"
|
||||
export { serve } from "./lib/serve"
|
||||
|
||||
export * from "./types"
|
||||
17
packages/admin-next/admin-sdk/src/lib/build.ts
Normal file
17
packages/admin-next/admin-sdk/src/lib/build.ts
Normal 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")
|
||||
}
|
||||
}
|
||||
111
packages/admin-next/admin-sdk/src/lib/config.ts
Normal file
111
packages/admin-next/admin-sdk/src/lib/config.ts
Normal 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
|
||||
}
|
||||
24
packages/admin-next/admin-sdk/src/lib/develop.ts
Normal file
24
packages/admin-next/admin-sdk/src/lib/develop.ts
Normal 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
|
||||
}
|
||||
53
packages/admin-next/admin-sdk/src/lib/serve.ts
Normal file
53
packages/admin-next/admin-sdk/src/lib/serve.ts
Normal 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
|
||||
}
|
||||
4
packages/admin-next/admin-sdk/src/types.ts
Normal file
4
packages/admin-next/admin-sdk/src/types.ts
Normal file
@@ -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,
|
||||
});
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
38
packages/admin-next/dashboard/scripts/generate-types.js
Normal file
38
packages/admin-next/dashboard/scripts/generate-types.js
Normal 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)
|
||||
}
|
||||
})()
|
||||
@@ -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",
|
||||
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),
|
||||
}
|
||||
|
||||
11
packages/admin-next/dashboard/src/i18n/languages.ts
Normal file
11
packages/admin-next/dashboard/src/i18n/languages.ts
Normal 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,
|
||||
},
|
||||
]
|
||||
315
packages/admin-next/dashboard/src/i18n/translations/$schema.json
Normal file
315
packages/admin-next/dashboard/src/i18n/translations/$schema.json
Normal 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"
|
||||
]
|
||||
}
|
||||
1682
packages/admin-next/dashboard/src/i18n/translations/en.json
Normal file
1682
packages/admin-next/dashboard/src/i18n/translations/en.json
Normal file
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,3 +8,6 @@ interface ImportMetaEnv {
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
declare const __BACKEND_URL__: string | undefined
|
||||
declare const __BASE__: string
|
||||
|
||||
7
packages/admin-next/dashboard/tsconfig.build.json
Normal file
7
packages/admin-next/dashboard/tsconfig.build.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"composite": true
|
||||
}
|
||||
}
|
||||
23
packages/admin-next/dashboard/tsup.config.ts
Normal file
23
packages/admin-next/dashboard/tsup.config.ts
Normal 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,
|
||||
})
|
||||
@@ -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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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).`,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
156
packages/medusa/src/commands/build.ts
Normal file
156
packages/medusa/src/commands/build.ts
Normal 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`)
|
||||
}
|
||||
51
packages/medusa/src/loaders/admin.ts
Normal file
51
packages/medusa/src/loaders/admin.ts
Normal 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
|
||||
}
|
||||
@@ -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 ?? [],
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user