feat: Init. v2 implementation in admin (#6715)
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { ICurrencyModuleService, IStoreModuleService } from "@medusajs/types"
|
||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
env,
|
||||||
|
testSuite: ({ getContainer }) => {
|
||||||
|
describe("Link: Store Currency", () => {
|
||||||
|
let appContainer
|
||||||
|
let storeModuleService: IStoreModuleService
|
||||||
|
let currencyModuleService: ICurrencyModuleService
|
||||||
|
let remoteQuery
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
appContainer = getContainer()
|
||||||
|
storeModuleService = appContainer.resolve(ModuleRegistrationName.STORE)
|
||||||
|
currencyModuleService = appContainer.resolve(
|
||||||
|
ModuleRegistrationName.CURRENCY
|
||||||
|
)
|
||||||
|
remoteQuery = appContainer.resolve("remoteQuery")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should query store and default currency with remote query", async () => {
|
||||||
|
const store = await storeModuleService.create({
|
||||||
|
name: "Store",
|
||||||
|
default_currency_code: "usd",
|
||||||
|
supported_currency_codes: ["usd"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const stores = await remoteQuery({
|
||||||
|
store: {
|
||||||
|
fields: ["id"],
|
||||||
|
default_currency: {
|
||||||
|
fields: ["code"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(stores).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: store.id,
|
||||||
|
default_currency: expect.objectContaining({ code: "usd" }),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
import { IStoreModuleService } from "@medusajs/types"
|
import { IStoreModuleService } from "@medusajs/types"
|
||||||
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
|
||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
|
|
||||||
await service.delete(createdStore.id)
|
await service.delete(createdStore.id)
|
||||||
const listedStores = await api.get(`/admin/stores`, adminHeaders)
|
const listedStores = await api.get(`/admin/stores?id=${createdStore.id}`, adminHeaders)
|
||||||
expect(listedStores.data.stores).toHaveLength(0)
|
expect(listedStores.data.stores).toHaveLength(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ import { queryClient } from "../../../lib/medusa"
|
|||||||
import { useSearch } from "../../../providers/search-provider"
|
import { useSearch } from "../../../providers/search-provider"
|
||||||
import { useSidebar } from "../../../providers/sidebar-provider"
|
import { useSidebar } from "../../../providers/sidebar-provider"
|
||||||
import { useTheme } from "../../../providers/theme-provider"
|
import { useTheme } from "../../../providers/theme-provider"
|
||||||
|
import { useV2Session } from "../../../lib/api-v2"
|
||||||
|
|
||||||
|
const V2_ENABLED = import.meta.env.VITE_MEDUSA_V2 || false
|
||||||
|
|
||||||
export const Shell = ({ children }: PropsWithChildren) => {
|
export const Shell = ({ children }: PropsWithChildren) => {
|
||||||
return (
|
return (
|
||||||
@@ -116,7 +119,19 @@ const Breadcrumbs = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UserBadge = () => {
|
const UserBadge = () => {
|
||||||
const { user, isLoading, isError, error } = useAdminGetSession()
|
// Comment: Only place where we switch between the two modes inline.
|
||||||
|
// This is to avoid having to rebuild the shell for the app.
|
||||||
|
let { user, isLoading, isError, error } = {} as any
|
||||||
|
|
||||||
|
// Medusa V2 disabled
|
||||||
|
;({ user, isLoading, isError, error } = useAdminGetSession({
|
||||||
|
enabled: V2_ENABLED == "false",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Medusa V2 enabled
|
||||||
|
;({ user, isLoading, isError, error } = useV2Session({
|
||||||
|
enabled: V2_ENABLED == "true",
|
||||||
|
}))
|
||||||
|
|
||||||
const name = [user?.first_name, user?.last_name].filter(Boolean).join(" ")
|
const name = [user?.first_name, user?.last_name].filter(Boolean).join(" ")
|
||||||
const displayName = name || user?.email
|
const displayName = name || user?.email
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query"
|
||||||
|
import { adminAuthKeys, useAdminCustomQuery } from "medusa-react"
|
||||||
|
import { medusa } from "../medusa"
|
||||||
|
|
||||||
|
export const useV2Session = (options: any = {}) => {
|
||||||
|
const { data, isLoading, isError, error } = useAdminCustomQuery(
|
||||||
|
"/admin/users/me",
|
||||||
|
adminAuthKeys.details(),
|
||||||
|
{},
|
||||||
|
options
|
||||||
|
)
|
||||||
|
|
||||||
|
const user = data?.user
|
||||||
|
|
||||||
|
return { user, isLoading, isError, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useV2LoginWithSession = () => {
|
||||||
|
return useMutation(
|
||||||
|
(payload: { email: string; password: string }) =>
|
||||||
|
medusa.client.request("POST", "/auth/admin/emailpass", {
|
||||||
|
email: payload.email,
|
||||||
|
password: payload.password,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
onSuccess: async (args: { token: string }) => {
|
||||||
|
const { token } = args
|
||||||
|
|
||||||
|
// Convert the JWT to a session cookie
|
||||||
|
// TODO: Consider if the JWT is a good choice for session token
|
||||||
|
await medusa.client.request(
|
||||||
|
"POST",
|
||||||
|
"/auth/session",
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./auth"
|
||||||
|
export * from "./store"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { adminStoreKeys, useAdminCustomQuery } from "medusa-react"
|
||||||
|
|
||||||
|
export const useV2Store = ({ initialData }: { initialData?: any }) => {
|
||||||
|
const { data, isLoading, isError, error } = useAdminCustomQuery(
|
||||||
|
"/admin/stores",
|
||||||
|
adminStoreKeys.details(),
|
||||||
|
{},
|
||||||
|
{ initialData }
|
||||||
|
)
|
||||||
|
|
||||||
|
const store = data.stores[0]
|
||||||
|
|
||||||
|
return { store, isLoading, isError, error }
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import { v1Routes } from "./v1"
|
import { v1Routes } from "./v1"
|
||||||
import { v2Routes } from "./v2"
|
import { v2Routes } from "./v2"
|
||||||
|
|
||||||
const V2_ENABLED = import.meta.env.MEDUSA_V2 || false
|
const V2_ENABLED = import.meta.env.VITE_MEDUSA_V2 || false
|
||||||
|
|
||||||
const router = createBrowserRouter(V2_ENABLED ? v2Routes : v1Routes)
|
const router = createBrowserRouter(V2_ENABLED ? v2Routes : v1Routes)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,38 @@
|
|||||||
import { RouteObject } from "react-router-dom"
|
import { Navigate, RouteObject, useLocation } from "react-router-dom"
|
||||||
|
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||||
|
|
||||||
|
import { Outlet } from "react-router-dom"
|
||||||
|
|
||||||
|
import { SidebarProvider } from "../sidebar-provider"
|
||||||
|
import { SearchProvider } from "../search-provider"
|
||||||
|
import { ErrorBoundary } from "../../components/error/error-boundary"
|
||||||
|
import { Spinner } from "@medusajs/icons"
|
||||||
|
import { useV2Session } from "../../lib/api-v2"
|
||||||
|
|
||||||
|
export const ProtectedRoute = () => {
|
||||||
|
const { user, isLoading } = useV2Session()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center">
|
||||||
|
<Spinner className="text-ui-fg-interactive animate-spin" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <Navigate to="/login" state={{ from: location }} replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarProvider>
|
||||||
|
<SearchProvider>
|
||||||
|
<Outlet />
|
||||||
|
</SearchProvider>
|
||||||
|
</SidebarProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Experimental V2 routes.
|
* Experimental V2 routes.
|
||||||
@@ -10,8 +44,62 @@ export const v2Routes: RouteObject[] = [
|
|||||||
path: "/login",
|
path: "/login",
|
||||||
lazy: () => import("../../v2-routes/login"),
|
lazy: () => import("../../v2-routes/login"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
lazy: () => import("../../v2-routes/home"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
lazy: () => import("../../routes/no-match"),
|
lazy: () => import("../../routes/no-match"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
element: <ProtectedRoute />,
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/settings",
|
||||||
|
element: <SettingsLayout />,
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Settings",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
lazy: () => import("../../v2-routes/settings"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "profile",
|
||||||
|
lazy: () => import("../../v2-routes/profile/profile-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Profile",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () => import("../../v2-routes/profile/profile-edit"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "store",
|
||||||
|
lazy: () => import("../../v2-routes/store/store-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Store",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () => import("../../v2-routes/store/store-edit"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "add-currencies",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/store/store-add-currencies"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { useEffect } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
|
export const Home = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
// Currently, the home page simply redirects to the settings page
|
||||||
|
useEffect(() => {
|
||||||
|
navigate("/settings", { replace: true })
|
||||||
|
}, [navigate])
|
||||||
|
|
||||||
|
return <div />
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Home as Component } from "./home";
|
||||||
@@ -1,3 +1,143 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Button, Heading, Input, Text } from "@medusajs/ui"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
|
import { Link, useLocation, useNavigate } from "react-router-dom"
|
||||||
|
import * as z from "zod"
|
||||||
|
|
||||||
|
import { Form } from "../../components/common/form"
|
||||||
|
import { LogoBox } from "../../components/common/logo-box"
|
||||||
|
import { useV2LoginWithSession } from "../../lib/api-v2"
|
||||||
|
import { isAxiosError } from "../../lib/is-axios-error"
|
||||||
|
|
||||||
|
const LoginSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
return <div>Medusa V2 Login</div>
|
const { t } = useTranslation()
|
||||||
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const from = location.state?.from?.pathname || "/settings"
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof LoginSchema>>({
|
||||||
|
resolver: zodResolver(LoginSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Update when more than emailpass is supported
|
||||||
|
const { mutateAsync, isLoading } = useV2LoginWithSession()
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async ({ email, password }) => {
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate(from, { replace: true })
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (isAxiosError(error)) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
form.setError("email", {
|
||||||
|
type: "manual",
|
||||||
|
})
|
||||||
|
|
||||||
|
form.setError("password", {
|
||||||
|
type: "manual",
|
||||||
|
message: t("errors.invalidCredentials"),
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setError("root.serverError", {
|
||||||
|
type: "manual",
|
||||||
|
message: t("errors.serverError"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-ui-bg-base flex min-h-dvh w-dvw items-center justify-center">
|
||||||
|
<div className="m-4 flex w-full max-w-[300px] flex-col items-center">
|
||||||
|
<LogoBox className="mb-4" />
|
||||||
|
<div className="mb-4 flex flex-col items-center">
|
||||||
|
<Heading>{t("login.title")}</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle text-center">
|
||||||
|
{t("login.hint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex w-full flex-col gap-y-6"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.email")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input autoComplete="email" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.password")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full" type="submit" isLoading={isLoading}>
|
||||||
|
{t("actions.continue")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
<div className="my-6 h-px w-full border-b border-dotted" />
|
||||||
|
<span className="text-ui-fg-subtle txt-small">
|
||||||
|
<Trans
|
||||||
|
i18nKey="login.forgotPassword"
|
||||||
|
components={[
|
||||||
|
<Link
|
||||||
|
key="reset-password-link"
|
||||||
|
to="/reset-password"
|
||||||
|
className="text-ui-fg-interactive transition-fg hover:text-ui-fg-interactive-hover focus-visible:text-ui-fg-interactive-hover outline-none"
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./profile-general-section"
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
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 { UserDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
type ProfileGeneralSectionProps = {
|
||||||
|
user: Partial<Omit<UserDTO, "password_hash">>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfileGeneralSection = ({ user }: ProfileGeneralSectionProps) => {
|
||||||
|
const { i18n, t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<div>
|
||||||
|
<Heading>{t("profile.domain")}</Heading>
|
||||||
|
<Text className="text-ui-fg-subtle" size="small">
|
||||||
|
{t("profile.manageYourProfileDetails")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Link to="/settings/profile/edit">
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.edit")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("fields.name")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("fields.email")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{user.email}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("profile.language")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{languages.find((lang) => lang.code === i18n.language)
|
||||||
|
?.display_name || "-"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("profile.usageInsights")}
|
||||||
|
</Text>
|
||||||
|
<StatusBadge color="red" className="w-fit">
|
||||||
|
{t("general.disabled")}
|
||||||
|
</StatusBadge>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ProfileDetail as Component } from "./profile-detail"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Outlet, json } from "react-router-dom"
|
||||||
|
import { ProfileGeneralSection } from "./components/profile-general-section"
|
||||||
|
import { useV2Session } from "../../../lib/api-v2"
|
||||||
|
|
||||||
|
export const ProfileDetail = () => {
|
||||||
|
const { user, isLoading, isError, error } = useV2Session()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || !user) {
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
throw json("An unknown error has occured", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<ProfileGeneralSection user={user} />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Button, Input, Select, Switch } from "@medusajs/ui"
|
||||||
|
import { adminUserKeys, useAdminCustomPost } from "medusa-react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { languages } from "../../../../../i18n/config"
|
||||||
|
import { queryClient } from "../../../../../lib/medusa"
|
||||||
|
import { UserDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
type EditProfileProps = {
|
||||||
|
user: Partial<Omit<UserDTO, "password_hash">>
|
||||||
|
usageInsights: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditProfileSchema = zod.object({
|
||||||
|
first_name: zod.string().optional(),
|
||||||
|
last_name: zod.string().optional(),
|
||||||
|
language: zod.string(),
|
||||||
|
usage_insights: zod.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EditProfileForm = ({ user, usageInsights }: EditProfileProps) => {
|
||||||
|
const { t, i18n } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditProfileSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
first_name: user.first_name ?? "",
|
||||||
|
last_name: user.last_name ?? "",
|
||||||
|
language: i18n.language,
|
||||||
|
usage_insights: usageInsights,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditProfileSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const changeLanguage = (code: string) => {
|
||||||
|
i18n.changeLanguage(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedLanguages = languages.sort((a, b) =>
|
||||||
|
a.display_name.localeCompare(b.display_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminCustomPost(
|
||||||
|
`/admin/users/${user.id}`,
|
||||||
|
[...adminUserKeys.lists(), ...adminUserKeys.detail(user.id!)]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
first_name: values.first_name,
|
||||||
|
last_name: values.last_name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries([
|
||||||
|
...adminUserKeys.lists(),
|
||||||
|
...adminUserKeys.detail(user.id!),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
return
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
changeLanguage(values.language)
|
||||||
|
|
||||||
|
handleSuccess()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||||
|
<RouteDrawer.Body>
|
||||||
|
<div className="flex flex-col gap-y-8">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="first_name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.firstName")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} size="small" />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="last_name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.lastName")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} size="small" />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="language"
|
||||||
|
render={({ field: { ref, ...field } }) => (
|
||||||
|
<Form.Item className="gap-y-4">
|
||||||
|
<div>
|
||||||
|
<Form.Label>{t("profile.language")}</Form.Label>
|
||||||
|
<Form.Hint>{t("profile.languageHint")}</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Form.Control>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<Select.Trigger ref={ref} className="py-1 text-[13px]">
|
||||||
|
<Select.Value placeholder="Choose language">
|
||||||
|
{
|
||||||
|
sortedLanguages.find(
|
||||||
|
(language) => language.code === field.value
|
||||||
|
)?.display_name
|
||||||
|
}
|
||||||
|
</Select.Value>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{languages.map((language) => (
|
||||||
|
<Select.Item
|
||||||
|
key={language.code}
|
||||||
|
value={language.code}
|
||||||
|
>
|
||||||
|
{language.display_name}
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="usage_insights"
|
||||||
|
render={({ field: { value, onChange, ...rest } }) => (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Form.Label>{t("profile.usageInsights")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Switch
|
||||||
|
{...rest}
|
||||||
|
checked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.Hint>
|
||||||
|
<span>
|
||||||
|
<Trans
|
||||||
|
i18nKey="profile.userInsightsHint"
|
||||||
|
components={[
|
||||||
|
<a
|
||||||
|
key="hint-link"
|
||||||
|
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-fg underline"
|
||||||
|
href="https://docs.medusajs.com/usage#admin-analytics"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Form.Hint>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button size="small" type="submit" isLoading={isLoading}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ProfileEdit as Component } from "./profile-edit"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { EditProfileForm } from "./components/edit-profile-form/edit-profile-form"
|
||||||
|
import { useV2Session } from "../../../lib/api-v2"
|
||||||
|
|
||||||
|
export const ProfileEdit = () => {
|
||||||
|
const { user, isLoading, isError, error } = useV2Session()
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header className="capitalize">
|
||||||
|
<Heading>{t("profile.editProfile")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{!isLoading && user && (
|
||||||
|
<EditProfileForm user={user} usageInsights={false} />
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Settings as Component } from "./settings";
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { useEffect } from "react"
|
||||||
|
import { Outlet, useLocation, useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
|
export const Settings = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.pathname === "/settings") {
|
||||||
|
navigate("/settings/profile")
|
||||||
|
}
|
||||||
|
}, [location.pathname, navigate])
|
||||||
|
|
||||||
|
return <Outlet />
|
||||||
|
}
|
||||||
+338
@@ -0,0 +1,338 @@
|
|||||||
|
import { Currency } from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Hint,
|
||||||
|
StatusBadge,
|
||||||
|
Table,
|
||||||
|
Tooltip,
|
||||||
|
clx,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
PaginationState,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
adminCurrenciesKeys,
|
||||||
|
adminStoreKeys,
|
||||||
|
useAdminCustomPost,
|
||||||
|
useAdminCustomQuery,
|
||||||
|
} from "medusa-react"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { OrderBy } from "../../../../../components/filtering/order-by"
|
||||||
|
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll"
|
||||||
|
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||||
|
import { StoreDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
type AddCurrenciesFormProps = {
|
||||||
|
store: StoreDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddCurrenciesSchema = zod.object({
|
||||||
|
currencies: zod.array(zod.string()).min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
|
export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof AddCurrenciesSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
currencies: [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(AddCurrenciesSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { setValue } = form
|
||||||
|
|
||||||
|
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = useMemo(
|
||||||
|
() => ({
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
}),
|
||||||
|
[pageIndex, pageSize]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ids = Object.keys(rowSelection)
|
||||||
|
setValue("currencies", ids, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
}, [rowSelection, setValue])
|
||||||
|
|
||||||
|
const params = useQueryParams(["order"])
|
||||||
|
const filter = {
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
offset: pageIndex * PAGE_SIZE,
|
||||||
|
...params,
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
const { data, count, isError, error } = useAdminCustomQuery(
|
||||||
|
"/admin/currencies",
|
||||||
|
adminCurrenciesKeys.list(filter),
|
||||||
|
filter
|
||||||
|
)
|
||||||
|
|
||||||
|
const preSelectedRows = store.supported_currency_codes.map((c) => c)
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: data?.currencies ?? [],
|
||||||
|
columns,
|
||||||
|
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.code,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
enableRowSelection: (row) => !preSelectedRows.includes(row.original.code),
|
||||||
|
manualPagination: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading: isMutating } = useAdminCustomPost(
|
||||||
|
`/admin/stores/${store.id}`,
|
||||||
|
adminStoreKeys.details()
|
||||||
|
)
|
||||||
|
|
||||||
|
const { handleScroll, isScrolled, tableContainerRef } = useHandleTableScroll()
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
const currencies = Array.from(
|
||||||
|
new Set([...data.currencies, ...preSelectedRows])
|
||||||
|
) as string[]
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
supported_currency_codes: currencies,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<RouteFocusModal.Header>
|
||||||
|
<div className="flex flex-1 items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{form.formState.errors.currencies && (
|
||||||
|
<Hint variant="error">
|
||||||
|
{form.formState.errors.currencies.message}
|
||||||
|
</Hint>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button size="small" type="submit" isLoading={isMutating}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Header>
|
||||||
|
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||||
|
<div></div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<OrderBy keys={["code"]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex-1 overflow-y-auto"
|
||||||
|
ref={tableContainerRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
<Table className="relative">
|
||||||
|
<Table.Header
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-base transition-fg sticky inset-x-0 top-0 z-10 border-t-0",
|
||||||
|
{
|
||||||
|
"shadow-elevation-card-hover": isScrolled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => {
|
||||||
|
return (
|
||||||
|
<Table.Row
|
||||||
|
key={headerGroup.id}
|
||||||
|
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<Table.HeaderCell key={header.id}>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</Table.HeaderCell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Row>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body className="border-b-0">
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<Table.Row
|
||||||
|
key={row.id}
|
||||||
|
className={clx(
|
||||||
|
"transition-fg last-of-type:border-b-0",
|
||||||
|
{
|
||||||
|
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||||
|
row.getIsSelected(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg-ui-bg-disabled hover:bg-ui-bg-disabled":
|
||||||
|
!row.getCanSelect(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<Table.Cell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
))}
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div className="w-full border-t">
|
||||||
|
<LocalizedTablePagination
|
||||||
|
canNextPage={table.getCanNextPage()}
|
||||||
|
canPreviousPage={table.getCanPreviousPage()}
|
||||||
|
nextPage={table.nextPage}
|
||||||
|
previousPage={table.previousPage}
|
||||||
|
count={count ?? 0}
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
pageCount={table.getPageCount()}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Currency>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const isPreSelected = !row.getCanSelect()
|
||||||
|
const isSelected = row.getIsSelected() || isPreSelected
|
||||||
|
|
||||||
|
const Component = (
|
||||||
|
<Checkbox
|
||||||
|
checked={isSelected}
|
||||||
|
disabled={isPreSelected}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isPreSelected) {
|
||||||
|
return (
|
||||||
|
<Tooltip content={t("store.currencyAlreadyAdded")} side="right">
|
||||||
|
{Component}
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Component
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: t("fields.name"),
|
||||||
|
cell: ({ getValue }) => getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("code", {
|
||||||
|
header: t("fields.code"),
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<Badge size="small">{getValue().toUpperCase()}</Badge>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("includes_tax", {
|
||||||
|
header: t("fields.taxInclusivePricing"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusBadge color={value ? "green" : "red"}>
|
||||||
|
{value ? t("general.enabled") : t("general.disabled")}
|
||||||
|
</StatusBadge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { StoreAddCurrencies as Component } from "./store-add-currencies"
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
import { AddCurrenciesForm } from "./components/add-currencies-form/add-currencies-form"
|
||||||
|
import { useV2Store } from "../../../lib/api-v2"
|
||||||
|
|
||||||
|
export const StoreAddCurrencies = () => {
|
||||||
|
const { store, isLoading, isError, error } = useV2Store({})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
{!isLoading && store && <AddCurrenciesForm store={store} />}
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./store-currency-section"
|
||||||
+315
@@ -0,0 +1,315 @@
|
|||||||
|
import { Trash } from "@medusajs/icons"
|
||||||
|
import { Currency } from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
CommandBar,
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
StatusBadge,
|
||||||
|
Table,
|
||||||
|
clx,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
adminStoreKeys,
|
||||||
|
useAdminCustomPost,
|
||||||
|
useAdminCustomQuery,
|
||||||
|
} from "medusa-react"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { ActionMenu } from "../../../../../../components/common/action-menu"
|
||||||
|
import { LocalizedTablePagination } from "../../../../../../components/localization/localized-table-pagination"
|
||||||
|
import { StoreDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
type StoreCurrencySectionProps = {
|
||||||
|
store: StoreDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
export const StoreCurrencySection = ({ store }: StoreCurrencySectionProps) => {
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const { data } = useAdminCustomQuery(
|
||||||
|
`/admin/currencies?code[]=${store.supported_currency_codes.join(",")}`,
|
||||||
|
adminStoreKeys.details()
|
||||||
|
)
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: data?.currencies ?? [],
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getRowId: (row) => row.code,
|
||||||
|
pageCount: Math.ceil(store.supported_currency_codes.length / PAGE_SIZE),
|
||||||
|
state: {
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
currencyCodes: store.supported_currency_codes,
|
||||||
|
storeId: store.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync } = useAdminCustomPost(
|
||||||
|
`/admin/stores/${store.id}`,
|
||||||
|
adminStoreKeys.details()
|
||||||
|
)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const handleDeleteCurrencies = async () => {
|
||||||
|
const ids = Object.keys(rowSelection)
|
||||||
|
|
||||||
|
const result = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("store.removeCurrencyWarning", {
|
||||||
|
count: ids.length,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.remove"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
supported_currency_codes: store.supported_currency_codes.filter(
|
||||||
|
(c) => !ids.includes(c)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setRowSelection({})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">{t("store.currencies")}</Heading>
|
||||||
|
<div>
|
||||||
|
<Link to="/settings/store/add-currencies">
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("general.add")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Table>
|
||||||
|
<Table.Header>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => {
|
||||||
|
return (
|
||||||
|
<Table.Row
|
||||||
|
key={headerGroup.id}
|
||||||
|
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<Table.HeaderCell key={header.id}>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</Table.HeaderCell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Row>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body className="border-b-0">
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<Table.Row
|
||||||
|
key={row.id}
|
||||||
|
className={clx("transition-fg", {
|
||||||
|
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||||
|
row.getIsSelected(),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<Table.Cell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</Table.Cell>
|
||||||
|
))}
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
<LocalizedTablePagination
|
||||||
|
canNextPage={table.getCanNextPage()}
|
||||||
|
canPreviousPage={table.getCanPreviousPage()}
|
||||||
|
nextPage={table.nextPage}
|
||||||
|
previousPage={table.previousPage}
|
||||||
|
count={store.supported_currency_codes.length}
|
||||||
|
pageIndex={table.getState().pagination.pageIndex}
|
||||||
|
pageCount={table.getPageCount()}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
/>
|
||||||
|
<CommandBar open={!!Object.keys(rowSelection).length}>
|
||||||
|
<CommandBar.Bar>
|
||||||
|
<CommandBar.Value>
|
||||||
|
{t("general.countSelected", {
|
||||||
|
count: Object.keys(rowSelection).length,
|
||||||
|
})}
|
||||||
|
</CommandBar.Value>
|
||||||
|
<CommandBar.Seperator />
|
||||||
|
<CommandBar.Command
|
||||||
|
action={handleDeleteCurrencies}
|
||||||
|
shortcut="r"
|
||||||
|
label={t("actions.remove")}
|
||||||
|
/>
|
||||||
|
</CommandBar.Bar>
|
||||||
|
</CommandBar>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CurrencyActions = ({
|
||||||
|
storeId,
|
||||||
|
currency,
|
||||||
|
currencyCodes,
|
||||||
|
}: {
|
||||||
|
storeId: string
|
||||||
|
currency: Currency
|
||||||
|
currencyCodes: string[]
|
||||||
|
}) => {
|
||||||
|
const { mutateAsync } = useAdminCustomPost(
|
||||||
|
`/admin/stores/${storeId}`,
|
||||||
|
adminStoreKeys.details()
|
||||||
|
)
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const handleRemove = async () => {
|
||||||
|
const result = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("store.removeCurrencyWarning", {
|
||||||
|
count: 1,
|
||||||
|
}),
|
||||||
|
verificationInstruction: t("general.typeToConfirm"),
|
||||||
|
verificationText: currency.name,
|
||||||
|
confirmText: t("actions.remove"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
supported_currency_codes: currencyCodes.filter(
|
||||||
|
(c) => c !== currency.code
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.remove"),
|
||||||
|
onClick: handleRemove,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Currency>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("code", {
|
||||||
|
header: t("fields.code"),
|
||||||
|
cell: ({ getValue }) => getValue().toUpperCase(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: t("fields.name"),
|
||||||
|
cell: ({ getValue }) => getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("includes_tax", {
|
||||||
|
header: "Tax Inclusive Prices",
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue()
|
||||||
|
const text = value ? t("general.enabled") : t("general.disabled")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusBadge color={value ? "green" : "red"}>{text}</StatusBadge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row, table }) => {
|
||||||
|
const { currencyCodes, storeId } = table.options.meta as {
|
||||||
|
currencyCodes: string[]
|
||||||
|
storeId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CurrencyActions
|
||||||
|
storeId={storeId}
|
||||||
|
currency={row.original}
|
||||||
|
currencyCodes={currencyCodes}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./store-general-section"
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
import { Store } from "@medusajs/medusa"
|
||||||
|
import { Badge, Button, Container, Copy, Heading, Text } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
type StoreGeneralSectionProps = {
|
||||||
|
store: Store
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<div>
|
||||||
|
<Heading>{t("store.domain")}</Heading>
|
||||||
|
<Text className="text-ui-fg-subtle" size="small">
|
||||||
|
{t("store.manageYourStoresDetails")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Link to={"/settings/store/edit"}>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.edit")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("fields.name")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{store.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("store.defaultCurrency")}
|
||||||
|
</Text>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Badge size="2xsmall">
|
||||||
|
{store.default_currency_code.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{store.default_currency.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("store.swapLinkTemplate")}
|
||||||
|
</Text>
|
||||||
|
{store.swap_link_template ? (
|
||||||
|
<div className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1">
|
||||||
|
<Text size="xsmall" leading="compact" className="truncate">
|
||||||
|
{store.swap_link_template}
|
||||||
|
</Text>
|
||||||
|
<Copy
|
||||||
|
content={store.swap_link_template}
|
||||||
|
variant="mini"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
-
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("store.paymentLinkTemplate")}
|
||||||
|
</Text>
|
||||||
|
{store.payment_link_template ? (
|
||||||
|
<div className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1">
|
||||||
|
<Text size="xsmall" leading="compact" className="truncate">
|
||||||
|
{store.payment_link_template}
|
||||||
|
</Text>
|
||||||
|
<Copy
|
||||||
|
content={store.payment_link_template}
|
||||||
|
variant="mini"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
-
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("store.inviteLinkTemplate")}
|
||||||
|
</Text>
|
||||||
|
{store.invite_link_template ? (
|
||||||
|
<div className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1">
|
||||||
|
<Text size="xsmall" leading="compact" className="truncate">
|
||||||
|
{store.invite_link_template}
|
||||||
|
</Text>
|
||||||
|
<Copy
|
||||||
|
content={store.invite_link_template}
|
||||||
|
variant="mini"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
-
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { storeLoader as loader } from "./loader"
|
||||||
|
export { StoreDetail as Component } from "./store-detail"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { AdminExtendedStoresRes } from "@medusajs/medusa"
|
||||||
|
import { Response } from "@medusajs/medusa-js"
|
||||||
|
import { adminStoreKeys } from "medusa-react"
|
||||||
|
import { redirect } from "react-router-dom"
|
||||||
|
|
||||||
|
import { FetchQueryOptions } from "@tanstack/react-query"
|
||||||
|
import { medusa, queryClient } from "../../../lib/medusa"
|
||||||
|
|
||||||
|
const storeDetailQuery = () => ({
|
||||||
|
queryKey: adminStoreKeys.details(),
|
||||||
|
queryFn: async () => medusa.client.request("GET", "/admin/stores"),
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchQuery = async (
|
||||||
|
query: FetchQueryOptions<Response<{ stores: any[] }>>
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const res = await queryClient.fetchQuery(query)
|
||||||
|
// TODO: Reconsider store retrieval
|
||||||
|
return res
|
||||||
|
} catch (error) {
|
||||||
|
const err = error ? JSON.parse(JSON.stringify(error)) : null
|
||||||
|
|
||||||
|
if ((err as Error & { status: number })?.status === 401) {
|
||||||
|
redirect("/login", 401)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const storeLoader = async () => {
|
||||||
|
const query = storeDetailQuery()
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<Response<AdminExtendedStoresRes>>(
|
||||||
|
query.queryKey
|
||||||
|
) ?? (await fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Outlet, useLoaderData } from "react-router-dom"
|
||||||
|
|
||||||
|
import { JsonViewSection } from "../../../components/common/json-view-section/json-view-section.tsx"
|
||||||
|
import { StoreCurrencySection } from "./components/store-currency-section/store-currencies-section.tsx/index.ts"
|
||||||
|
import { StoreGeneralSection } from "./components/store-general-section/index.ts"
|
||||||
|
import { storeLoader } from "./loader.ts"
|
||||||
|
import { useV2Store } from "../../../lib/api-v2/index.ts"
|
||||||
|
|
||||||
|
export const StoreDetail = () => {
|
||||||
|
const initialData = useLoaderData() as Awaited<ReturnType<typeof storeLoader>>
|
||||||
|
|
||||||
|
const { store, isLoading, isError, error } = useV2Store({
|
||||||
|
initialData: initialData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || !store) {
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{JSON.stringify(error, null, 2)}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<StoreGeneralSection store={store} />
|
||||||
|
<StoreCurrencySection store={store} />
|
||||||
|
<JsonViewSection data={store} />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import type { Store } from "@medusajs/medusa"
|
||||||
|
import { Button, Input } from "@medusajs/ui"
|
||||||
|
import { adminStoreKeys, useAdminCustomPost } from "medusa-react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
|
||||||
|
type EditStoreFormProps = {
|
||||||
|
store: Store
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditStoreSchema = zod.object({
|
||||||
|
name: zod.string().optional(),
|
||||||
|
swap_link_template: zod.union([zod.literal(""), zod.string().trim().url()]),
|
||||||
|
payment_link_template: zod.union([
|
||||||
|
zod.literal(""),
|
||||||
|
zod.string().trim().url(),
|
||||||
|
]),
|
||||||
|
invite_link_template: zod.union([zod.literal(""), zod.string().trim().url()]),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EditStoreForm = ({ store }: EditStoreFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditStoreSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
name: store.name,
|
||||||
|
swap_link_template: store.swap_link_template ?? "",
|
||||||
|
payment_link_template: store.payment_link_template ?? "",
|
||||||
|
invite_link_template: store.invite_link_template ?? "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditStoreSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminCustomPost(
|
||||||
|
`/admin/stores/${store.id}`,
|
||||||
|
adminStoreKeys.details()
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
mutateAsync(
|
||||||
|
{
|
||||||
|
name: values.name,
|
||||||
|
// invite_link_template: values.invite_link_template || undefined,
|
||||||
|
// swap_link_template: values.swap_link_template || undefined,
|
||||||
|
// payment_link_template: values.payment_link_template || undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||||
|
<RouteDrawer.Body>
|
||||||
|
<div className="flex flex-col gap-y-8">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.name")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input size="small" {...field} placeholder="ACME" />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="swap_link_template"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("store.swapLinkTemplate")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
{...field}
|
||||||
|
placeholder="https://www.store.com/swap={id}"
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="payment_link_template"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("store.paymentLinkTemplate")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
{...field}
|
||||||
|
placeholder="https://www.store.com/payment={id}"
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="invite_link_template"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("store.inviteLinkTemplate")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
{...field}
|
||||||
|
placeholder="https://www.admin.com/invite?token={invite_token}"
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button size="small" isLoading={isLoading} type="submit">
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { StoreEdit as Component } from "./store-edit"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { json } from "react-router-dom"
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { EditStoreForm } from "./components/edit-store-form/edit-store-form"
|
||||||
|
import { useV2Store } from "../../../lib/api-v2"
|
||||||
|
|
||||||
|
export const StoreEdit = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { store, isLoading, isError, error } = useV2Store({})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!store && !isLoading) {
|
||||||
|
throw json("An unknown error has occured", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading className="capitalize">{t("store.editStore")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{store && <EditStoreForm store={store} />}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly MEDUSA_ADMIN_BACKEND_URL: string
|
readonly MEDUSA_ADMIN_BACKEND_URL: string
|
||||||
readonly MEDUSA_V2: boolean
|
readonly VITE_MEDUSA_V2: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
AbstractAuthModuleProvider,
|
AbstractAuthModuleProvider,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
isString,
|
isString,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types"
|
|
||||||
|
|
||||||
import { AuthUserService } from "@services"
|
import { AuthUserService } from "@services"
|
||||||
import Scrypt from "scrypt-kdf"
|
import Scrypt from "scrypt-kdf"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./steps"
|
||||||
|
export * from "./workflows"
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { CreateStoreDTO, IStoreModuleService } from "@medusajs/types"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
import { createStoresWorkflow } from "../../store"
|
||||||
|
|
||||||
|
type CreateDefaultStoreStepInput = {
|
||||||
|
store: CreateStoreDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDefaultStoreStepId = "create-default-store"
|
||||||
|
export const createDefaultStoreStep = createStep(
|
||||||
|
createDefaultStoreStepId,
|
||||||
|
async (data: CreateDefaultStoreStepInput, { container }) => {
|
||||||
|
const storeService = container.resolve(ModuleRegistrationName.STORE)
|
||||||
|
|
||||||
|
let shouldDelete = false
|
||||||
|
let [store] = await storeService.list({}, { take: 1 })
|
||||||
|
|
||||||
|
if (!store) {
|
||||||
|
store = await createStoresWorkflow(container).run({
|
||||||
|
input: {
|
||||||
|
stores: [
|
||||||
|
{
|
||||||
|
// TODO: Revisit for a more sophisticated approach
|
||||||
|
...data.store,
|
||||||
|
supported_currency_codes: ["usd"],
|
||||||
|
default_currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
shouldDelete = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepResponse(store, { storeId: store.id, shouldDelete })
|
||||||
|
},
|
||||||
|
async (data, { container }) => {
|
||||||
|
if (!data || !data.shouldDelete) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = container.resolve<IStoreModuleService>(
|
||||||
|
ModuleRegistrationName.STORE
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.delete(data.storeId)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-default-store"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { createWorkflow } from "@medusajs/workflows-sdk"
|
||||||
|
import { createDefaultSalesChannelStep } from "../../sales-channel"
|
||||||
|
import { createDefaultStoreStep } from "../steps/create-default-store"
|
||||||
|
|
||||||
|
export const createDefaultsWorkflowID = "create-defaults"
|
||||||
|
export const createDefaultsWorkflow = createWorkflow(
|
||||||
|
createDefaultsWorkflowID,
|
||||||
|
(input) => {
|
||||||
|
const salesChannel = createDefaultSalesChannelStep({
|
||||||
|
data: {
|
||||||
|
name: "Default Sales Channel",
|
||||||
|
description: "Created by Medusa",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const store = createDefaultStoreStep({
|
||||||
|
store: {
|
||||||
|
default_sales_channel_id: salesChannel.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-defaults"
|
||||||
@@ -2,6 +2,7 @@ export * from "./api-key"
|
|||||||
export * from "./auth"
|
export * from "./auth"
|
||||||
export * from "./customer"
|
export * from "./customer"
|
||||||
export * from "./customer-group"
|
export * from "./customer-group"
|
||||||
|
export * from "./defaults"
|
||||||
export * from "./definition"
|
export * from "./definition"
|
||||||
export * from "./definitions"
|
export * from "./definitions"
|
||||||
export * from "./fulfillment"
|
export * from "./fulfillment"
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./steps"
|
export * from "./steps"
|
||||||
export * from "./workflows"
|
export * from "./workflows"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import {
|
||||||
|
CreateSalesChannelDTO,
|
||||||
|
ISalesChannelModuleService,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
interface StepInput {
|
||||||
|
data: CreateSalesChannelDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDefaultSalesChannelStepId = "create-default-sales-channel"
|
||||||
|
export const createDefaultSalesChannelStep = createStep(
|
||||||
|
createDefaultSalesChannelStepId,
|
||||||
|
async (input: StepInput, { container }) => {
|
||||||
|
const salesChannelService = container.resolve<ISalesChannelModuleService>(
|
||||||
|
ModuleRegistrationName.SALES_CHANNEL
|
||||||
|
)
|
||||||
|
|
||||||
|
let shouldDelete = false
|
||||||
|
let [salesChannel] = await salesChannelService.list({}, { take: 1 })
|
||||||
|
|
||||||
|
if (!salesChannel) {
|
||||||
|
salesChannel = await salesChannelService.create(input.data)
|
||||||
|
|
||||||
|
shouldDelete = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepResponse(salesChannel, { id: salesChannel.id, shouldDelete })
|
||||||
|
},
|
||||||
|
async (data, { container }) => {
|
||||||
|
if (!data?.id || !data.shouldDelete) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = container.resolve<ISalesChannelModuleService>(
|
||||||
|
ModuleRegistrationName.SALES_CHANNEL
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.delete(data.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./associate-products-with-channels"
|
export * from "./associate-products-with-channels"
|
||||||
|
export * from "./create-default-sales-channel"
|
||||||
export * from "./create-sales-channels"
|
export * from "./create-sales-channels"
|
||||||
export * from "./delete-sales-channels"
|
export * from "./delete-sales-channels"
|
||||||
export * from "./update-sales-channels"
|
export * from "./update-sales-channels"
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { ModuleJoinerConfig } from "@medusajs/types"
|
|||||||
import { MapToConfig } from "@medusajs/utils"
|
import { MapToConfig } from "@medusajs/utils"
|
||||||
import Currency from "./models/currency"
|
import Currency from "./models/currency"
|
||||||
|
|
||||||
export const LinkableKeys: Record<string, string> = {}
|
export const LinkableKeys: Record<string, string> = {
|
||||||
|
code: Currency.name,
|
||||||
|
}
|
||||||
|
|
||||||
const entityLinkableKeysMap: MapToConfig = {}
|
const entityLinkableKeysMap: MapToConfig = {}
|
||||||
Object.entries(LinkableKeys).forEach(([key, value]) => {
|
Object.entries(LinkableKeys).forEach(([key, value]) => {
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ export * from "./publishable-api-key-sales-channel"
|
|||||||
export * from "./region-payment-provider"
|
export * from "./region-payment-provider"
|
||||||
export * from "./sales-channel-location"
|
export * from "./sales-channel-location"
|
||||||
export * from "./shipping-option-price-set"
|
export * from "./shipping-option-price-set"
|
||||||
|
export * from "./store-default-currency"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
|
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||||
|
|
||||||
|
export const StoreDefaultCurrency: ModuleJoinerConfig = {
|
||||||
|
isLink: true,
|
||||||
|
isReadOnlyLink: true,
|
||||||
|
extends: [
|
||||||
|
{
|
||||||
|
serviceName: Modules.STORE,
|
||||||
|
relationship: {
|
||||||
|
serviceName: Modules.CURRENCY,
|
||||||
|
primaryKey: "code",
|
||||||
|
foreignKey: "default_currency_code",
|
||||||
|
alias: "default_currency",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
export const defaultAdminStoreRelations = []
|
|
||||||
export const allowedAdminStoreRelations = []
|
|
||||||
export const defaultAdminStoreFields = [
|
export const defaultAdminStoreFields = [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"supported_currency_codes",
|
"supported_currency_codes",
|
||||||
"default_currency_code",
|
"default_currency_code",
|
||||||
|
"default_currency.name",
|
||||||
|
"default_currency.symbol",
|
||||||
|
"default_currency.symbol_native",
|
||||||
"default_sales_channel_id",
|
"default_sales_channel_id",
|
||||||
"default_region_id",
|
"default_region_id",
|
||||||
"default_location_id",
|
"default_location_id",
|
||||||
@@ -14,9 +15,7 @@ export const defaultAdminStoreFields = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const retrieveTransformQueryConfig = {
|
export const retrieveTransformQueryConfig = {
|
||||||
defaultFields: defaultAdminStoreFields,
|
defaults: defaultAdminStoreFields,
|
||||||
defaultRelations: defaultAdminStoreRelations,
|
|
||||||
allowedRelations: allowedAdminStoreRelations,
|
|
||||||
isList: false,
|
isList: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
remoteQueryObjectFromString
|
remoteQueryObjectFromString,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
||||||
import { defaultAdminStoreFields } from "./query-config"
|
import { defaultAdminStoreFields } from "./query-config"
|
||||||
|
|||||||
@@ -6,6 +6,31 @@ import { track } from "medusa-telemetry"
|
|||||||
|
|
||||||
import loaders from "../loaders"
|
import loaders from "../loaders"
|
||||||
import Logger from "../loaders/logger"
|
import Logger from "../loaders/logger"
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import featureFlagLoader from "../loaders/feature-flags"
|
||||||
|
import configModuleLoader from "../loaders/config"
|
||||||
|
import { MedusaV2Flag } from "@medusajs/utils"
|
||||||
|
|
||||||
|
// TEMP: Only supporting emailpass
|
||||||
|
const createV2User = async ({ email, password }, { container }) => {
|
||||||
|
const authService = container.resolve(ModuleRegistrationName.AUTH)
|
||||||
|
const userService = container.resolve(ModuleRegistrationName.USER)
|
||||||
|
const user = await userService.create({ email })
|
||||||
|
const { authUser } = await authService.authenticate("emailpass", {
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
authScope: "admin",
|
||||||
|
})
|
||||||
|
|
||||||
|
await authService.update({
|
||||||
|
id: authUser.id,
|
||||||
|
app_metadata: {
|
||||||
|
user_id: user.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default async function ({
|
export default async function ({
|
||||||
directory,
|
directory,
|
||||||
@@ -32,10 +57,17 @@ export default async function ({
|
|||||||
Logger.info(`
|
Logger.info(`
|
||||||
Invite token: ${invite[0].token}
|
Invite token: ${invite[0].token}
|
||||||
Open the invite in Medusa Admin at: [your-admin-url]/invite?token=${invite[0].token}`)
|
Open the invite in Medusa Admin at: [your-admin-url]/invite?token=${invite[0].token}`)
|
||||||
|
} else {
|
||||||
|
const configModule = configModuleLoader(directory)
|
||||||
|
const featureFlagRouter = featureFlagLoader(configModule)
|
||||||
|
|
||||||
|
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
|
||||||
|
await createV2User({ email, password }, { container })
|
||||||
} else {
|
} else {
|
||||||
const userService = container.resolve("userService")
|
const userService = container.resolve("userService")
|
||||||
await userService.create({ id, email }, password)
|
await userService.create({ id, email }, password)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
requireCustomerAuthentication,
|
requireCustomerAuthentication,
|
||||||
} from "../../../api/middlewares"
|
} from "../../../api/middlewares"
|
||||||
import { ConfigModule } from "../../../types/global"
|
import { ConfigModule } from "../../../types/global"
|
||||||
|
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
||||||
import logger from "../../logger"
|
import logger from "../../logger"
|
||||||
import {
|
import {
|
||||||
AsyncRouteHandler,
|
AsyncRouteHandler,
|
||||||
@@ -19,12 +20,11 @@ import {
|
|||||||
MiddlewareRoute,
|
MiddlewareRoute,
|
||||||
MiddlewareVerb,
|
MiddlewareVerb,
|
||||||
MiddlewaresConfig,
|
MiddlewaresConfig,
|
||||||
|
ParserConfigArgs,
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
RouteDescriptor,
|
RouteDescriptor,
|
||||||
RouteVerb,
|
RouteVerb,
|
||||||
ParserConfigArgs,
|
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
|
||||||
|
|
||||||
const log = ({
|
const log = ({
|
||||||
activityId,
|
activityId,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import { createDefaultsWorkflow } from "@medusajs/core-flows"
|
||||||
import {
|
import {
|
||||||
InternalModuleDeclaration,
|
InternalModuleDeclaration,
|
||||||
ModulesDefinition,
|
ModulesDefinition
|
||||||
} from "@medusajs/modules-sdk"
|
} from "@medusajs/modules-sdk"
|
||||||
import { MODULE_RESOURCE_TYPE } from "@medusajs/types"
|
import { MODULE_RESOURCE_TYPE } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
isString,
|
|
||||||
MedusaV2Flag,
|
MedusaV2Flag,
|
||||||
|
isString,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { asValue } from "awilix"
|
import { asValue } from "awilix"
|
||||||
import { Express, NextFunction, Request, Response } from "express"
|
import { Express, NextFunction, Request, Response } from "express"
|
||||||
@@ -23,6 +24,7 @@ import databaseLoader, { dataSource } from "./database"
|
|||||||
import defaultsLoader from "./defaults"
|
import defaultsLoader from "./defaults"
|
||||||
import expressLoader from "./express"
|
import expressLoader from "./express"
|
||||||
import featureFlagsLoader from "./feature-flags"
|
import featureFlagsLoader from "./feature-flags"
|
||||||
|
import medusaProjectApisLoader from "./load-medusa-project-apis"
|
||||||
import Logger from "./logger"
|
import Logger from "./logger"
|
||||||
import loadMedusaApp, { mergeDefaultModules } from "./medusa-app"
|
import loadMedusaApp, { mergeDefaultModules } from "./medusa-app"
|
||||||
import modelsLoader from "./models"
|
import modelsLoader from "./models"
|
||||||
@@ -35,7 +37,6 @@ import searchIndexLoader from "./search-index"
|
|||||||
import servicesLoader from "./services"
|
import servicesLoader from "./services"
|
||||||
import strategiesLoader from "./strategies"
|
import strategiesLoader from "./strategies"
|
||||||
import subscribersLoader from "./subscribers"
|
import subscribersLoader from "./subscribers"
|
||||||
import medusaProjectApisLoader from "./load-medusa-project-apis"
|
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
directory: string
|
directory: string
|
||||||
@@ -130,7 +131,7 @@ async function loadMedusaV2({
|
|||||||
featureFlagRouter,
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
medusaProjectApisLoader({
|
await medusaProjectApisLoader({
|
||||||
rootDirectory,
|
rootDirectory,
|
||||||
container,
|
container,
|
||||||
app: expressApp,
|
app: expressApp,
|
||||||
@@ -138,6 +139,8 @@ async function loadMedusaV2({
|
|||||||
activityId: "medusa-project-apis",
|
activityId: "medusa-project-apis",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await createDefaultsWorkflow(container).run()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
container,
|
container,
|
||||||
app: expressApp,
|
app: expressApp,
|
||||||
|
|||||||
Reference in New Issue
Block a user