From 8155e2cfad435c724b080ccd38bd744fe7ea7bc2 Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:28:28 +0100 Subject: [PATCH] feat: Init. v2 implementation in admin (#6715) --- .../link-modules/store-currency.spec.ts | 54 +++ .../__tests__/store/admin/store.spec.ts | 4 +- .../src/components/layout/shell/shell.tsx | 17 +- .../dashboard/src/lib/api-v2/auth.ts | 43 +++ .../dashboard/src/lib/api-v2/index.ts | 2 + .../dashboard/src/lib/api-v2/store.ts | 14 + .../router-provider/router-provider.tsx | 2 +- .../src/providers/router-provider/v2.tsx | 90 ++++- .../dashboard/src/v2-routes/home/home.tsx | 13 + .../dashboard/src/v2-routes/home/index.ts | 1 + .../dashboard/src/v2-routes/login/login.tsx | 142 +++++++- .../profile-general-section/index.ts | 1 + .../profile-general-section.tsx | 63 ++++ .../v2-routes/profile/profile-detail/index.ts | 1 + .../profile/profile-detail/profile-detail.tsx | 26 ++ .../edit-profile-form/edit-profile-form.tsx | 207 +++++++++++ .../v2-routes/profile/profile-edit/index.ts | 1 + .../profile/profile-edit/profile-edit.tsx | 26 ++ .../dashboard/src/v2-routes/settings/index.ts | 1 + .../src/v2-routes/settings/settings.tsx | 15 + .../add-currencies-form.tsx | 338 ++++++++++++++++++ .../store/store-add-currencies/index.ts | 1 + .../store-add-currencies.tsx | 17 + .../store-currencies-section.tsx/index.ts | 1 + .../store-currency-section.tsx | 315 ++++++++++++++++ .../components/store-general-section/index.ts | 1 + .../store-general-section.tsx | 114 ++++++ .../src/v2-routes/store/store-detail/index.ts | 2 + .../v2-routes/store/store-detail/loader.ts | 38 ++ .../store/store-detail/store-detail.tsx | 36 ++ .../edit-store-form/edit-store-form.tsx | 149 ++++++++ .../src/v2-routes/store/store-edit/index.ts | 1 + .../v2-routes/store/store-edit/store-edit.tsx | 28 ++ .../admin-next/dashboard/src/vite-env.d.ts | 2 +- packages/auth/src/providers/email-password.ts | 2 +- packages/core-flows/src/defaults/index.ts | 2 + .../defaults/steps/create-default-store.ts | 49 +++ .../core-flows/src/defaults/steps/index.ts | 1 + .../src/defaults/workflows/create-defaults.ts | 23 ++ .../src/defaults/workflows/index.ts | 1 + packages/core-flows/src/index.ts | 1 + .../core-flows/src/sales-channel/index.ts | 1 + .../steps/create-default-sales-channel.ts | 42 +++ .../src/sales-channel/steps/index.ts | 1 + packages/currency/src/joiner-config.ts | 4 +- .../link-modules/src/definitions/index.ts | 1 + .../src/definitions/store-default-currency.ts | 18 + .../src/api-v2/admin/stores/query-config.ts | 9 +- .../medusa/src/api-v2/admin/stores/route.ts | 2 +- packages/medusa/src/commands/user.js | 36 +- .../src/loaders/helpers/routing/index.ts | 4 +- packages/medusa/src/loaders/index.ts | 11 +- 52 files changed, 1951 insertions(+), 23 deletions(-) create mode 100644 integration-tests/modules/__tests__/link-modules/store-currency.spec.ts create mode 100644 packages/admin-next/dashboard/src/lib/api-v2/auth.ts create mode 100644 packages/admin-next/dashboard/src/lib/api-v2/index.ts create mode 100644 packages/admin-next/dashboard/src/lib/api-v2/store.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/home/home.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/home/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/profile-detail.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/profile-edit.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/settings/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/settings/settings.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/store-add-currencies.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/store-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-detail/loader.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-detail/store-detail.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-edit/components/edit-store-form/edit-store-form.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/store/store-edit/store-edit.tsx create mode 100644 packages/core-flows/src/defaults/index.ts create mode 100644 packages/core-flows/src/defaults/steps/create-default-store.ts create mode 100644 packages/core-flows/src/defaults/steps/index.ts create mode 100644 packages/core-flows/src/defaults/workflows/create-defaults.ts create mode 100644 packages/core-flows/src/defaults/workflows/index.ts create mode 100644 packages/core-flows/src/sales-channel/steps/create-default-sales-channel.ts create mode 100644 packages/link-modules/src/definitions/store-default-currency.ts diff --git a/integration-tests/modules/__tests__/link-modules/store-currency.spec.ts b/integration-tests/modules/__tests__/link-modules/store-currency.spec.ts new file mode 100644 index 0000000000..984485be62 --- /dev/null +++ b/integration-tests/modules/__tests__/link-modules/store-currency.spec.ts @@ -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" }), + }), + ]) + ) + }) + }) + }, +}) diff --git a/integration-tests/modules/__tests__/store/admin/store.spec.ts b/integration-tests/modules/__tests__/store/admin/store.spec.ts index 149c57144b..3de41cf9a2 100644 --- a/integration-tests/modules/__tests__/store/admin/store.spec.ts +++ b/integration-tests/modules/__tests__/store/admin/store.spec.ts @@ -1,7 +1,7 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { IStoreModuleService } from "@medusajs/types" -import { createAdminUser } from "../../../../helpers/create-admin-user" import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { createAdminUser } from "../../../../helpers/create-admin-user" jest.setTimeout(50000) @@ -57,7 +57,7 @@ medusaIntegrationTestRunner({ ) 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) }) }) diff --git a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx index 6c671b75af..fe80ebca1d 100644 --- a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx +++ b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx @@ -28,6 +28,9 @@ import { queryClient } from "../../../lib/medusa" import { useSearch } from "../../../providers/search-provider" import { useSidebar } from "../../../providers/sidebar-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) => { return ( @@ -116,7 +119,19 @@ const Breadcrumbs = () => { } 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 displayName = name || user?.email diff --git a/packages/admin-next/dashboard/src/lib/api-v2/auth.ts b/packages/admin-next/dashboard/src/lib/api-v2/auth.ts new file mode 100644 index 0000000000..c16e0657ba --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/api-v2/auth.ts @@ -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}`, + } + ) + }, + } + ) +} diff --git a/packages/admin-next/dashboard/src/lib/api-v2/index.ts b/packages/admin-next/dashboard/src/lib/api-v2/index.ts new file mode 100644 index 0000000000..4b23d2e698 --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/api-v2/index.ts @@ -0,0 +1,2 @@ +export * from "./auth" +export * from "./store" diff --git a/packages/admin-next/dashboard/src/lib/api-v2/store.ts b/packages/admin-next/dashboard/src/lib/api-v2/store.ts new file mode 100644 index 0000000000..e4537386da --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/api-v2/store.ts @@ -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 } +} diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index 8c42e124a1..ed6899ce29 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -6,7 +6,7 @@ import { import { v1Routes } from "./v1" 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) diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx index 588e3fa6c3..15d573b178 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx @@ -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 ( +
+ +
+ ) + } + + if (!user) { + return + } + + return ( + + + + + + ) +} /** * Experimental V2 routes. @@ -10,8 +44,62 @@ export const v2Routes: RouteObject[] = [ path: "/login", lazy: () => import("../../v2-routes/login"), }, + { + path: "/", + lazy: () => import("../../v2-routes/home"), + }, { path: "*", lazy: () => import("../../routes/no-match"), }, + { + element: , + errorElement: , + children: [ + { + path: "/settings", + element: , + 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"), + }, + ], + }, + ], + }, + ], + }, ] diff --git a/packages/admin-next/dashboard/src/v2-routes/home/home.tsx b/packages/admin-next/dashboard/src/v2-routes/home/home.tsx new file mode 100644 index 0000000000..c29fd68fc6 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/home/home.tsx @@ -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
+} diff --git a/packages/admin-next/dashboard/src/v2-routes/home/index.ts b/packages/admin-next/dashboard/src/v2-routes/home/index.ts new file mode 100644 index 0000000000..ea1e81bb8a --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/home/index.ts @@ -0,0 +1 @@ +export { Home as Component } from "./home"; diff --git a/packages/admin-next/dashboard/src/v2-routes/login/login.tsx b/packages/admin-next/dashboard/src/v2-routes/login/login.tsx index 8f14c1f8c7..bbee7dc464 100644 --- a/packages/admin-next/dashboard/src/v2-routes/login/login.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/login/login.tsx @@ -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 = () => { - return
Medusa V2 Login
+ const { t } = useTranslation() + const location = useLocation() + const navigate = useNavigate() + + const from = location.state?.from?.pathname || "/settings" + + const form = useForm>({ + 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 ( +
+
+ +
+ {t("login.title")} + + {t("login.hint")} + +
+
+ +
+ { + return ( + + {t("fields.email")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.password")} + + + + + + ) + }} + /> +
+ +
+ +
+ + , + ]} + /> + +
+
+ ) } diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/index.ts new file mode 100644 index 0000000000..948c77df96 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/index.ts @@ -0,0 +1 @@ +export * from "./profile-general-section" diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx new file mode 100644 index 0000000000..79e5057699 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx @@ -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> +} + +export const ProfileGeneralSection = ({ user }: ProfileGeneralSectionProps) => { + const { i18n, t } = useTranslation() + return ( + +
+
+ {t("profile.domain")} + + {t("profile.manageYourProfileDetails")} + +
+ + + +
+
+ + {t("fields.name")} + + + {user.first_name} {user.last_name} + +
+
+ + {t("fields.email")} + + + {user.email} + +
+
+ + {t("profile.language")} + + + {languages.find((lang) => lang.code === i18n.language) + ?.display_name || "-"} + +
+
+ + {t("profile.usageInsights")} + + + {t("general.disabled")} + +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/index.ts b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/index.ts new file mode 100644 index 0000000000..9d981a433d --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/index.ts @@ -0,0 +1 @@ +export { ProfileDetail as Component } from "./profile-detail" diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/profile-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/profile-detail.tsx new file mode 100644 index 0000000000..2880bcff6e --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-detail/profile-detail.tsx @@ -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
Loading...
+ } + + if (isError || !user) { + if (error) { + throw error + } + + throw json("An unknown error has occured", 500) + } + + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx new file mode 100644 index 0000000000..8ff62b4ee2 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx @@ -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> + 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>({ + 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 ( + +
+ +
+
+ ( + + {t("fields.firstName")} + + + + + + )} + /> + ( + + {t("fields.lastName")} + + + + + + )} + /> +
+ ( + +
+ {t("profile.language")} + {t("profile.languageHint")} +
+
+ + + + +
+
+ )} + /> + ( + +
+ {t("profile.usageInsights")} + + + +
+ + + , + ]} + /> + + + +
+ )} + /> +
+
+ +
+ + + + +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/index.ts b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/index.ts new file mode 100644 index 0000000000..99be2393cb --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/index.ts @@ -0,0 +1 @@ +export { ProfileEdit as Component } from "./profile-edit" diff --git a/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/profile-edit.tsx b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/profile-edit.tsx new file mode 100644 index 0000000000..ac7f42225a --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/profile/profile-edit/profile-edit.tsx @@ -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 ( + + + {t("profile.editProfile")} + + {!isLoading && user && ( + + )} + + ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/settings/index.ts b/packages/admin-next/dashboard/src/v2-routes/settings/index.ts new file mode 100644 index 0000000000..af259dd1e4 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/settings/index.ts @@ -0,0 +1 @@ +export { Settings as Component } from "./settings"; diff --git a/packages/admin-next/dashboard/src/v2-routes/settings/settings.tsx b/packages/admin-next/dashboard/src/v2-routes/settings/settings.tsx new file mode 100644 index 0000000000..058b328f7c --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/settings/settings.tsx @@ -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 +} diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx new file mode 100644 index 0000000000..13d52a0dc2 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx @@ -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>({ + defaultValues: { + currencies: [], + }, + resolver: zodResolver(AddCurrenciesSchema), + }) + + const { setValue } = form + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + 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 ( + +
+ +
+
+ {form.formState.errors.currencies && ( + + {form.formState.errors.currencies.message} + + )} +
+
+ + + + +
+
+
+ +
+
+
+ +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+
+
+ +
+
+
+
+ ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + const isPreSelected = !row.getCanSelect() + const isSelected = row.getIsSelected() || isPreSelected + + const Component = ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + + if (isPreSelected) { + return ( + + {Component} + + ) + } + + return Component + }, + }), + columnHelper.accessor("name", { + header: t("fields.name"), + cell: ({ getValue }) => getValue(), + }), + columnHelper.accessor("code", { + header: t("fields.code"), + cell: ({ getValue }) => ( + {getValue().toUpperCase()} + ), + }), + columnHelper.accessor("includes_tax", { + header: t("fields.taxInclusivePricing"), + cell: ({ getValue }) => { + const value = getValue() + + return ( + + {value ? t("general.enabled") : t("general.disabled")} + + ) + }, + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/index.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/index.ts new file mode 100644 index 0000000000..8a88a62bd8 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/index.ts @@ -0,0 +1 @@ +export { StoreAddCurrencies as Component } from "./store-add-currencies" diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/store-add-currencies.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/store-add-currencies.tsx new file mode 100644 index 0000000000..e9c5c1644b --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-add-currencies/store-add-currencies.tsx @@ -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 ( + + {!isLoading && store && } + + ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/index.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/index.ts new file mode 100644 index 0000000000..9a84261a47 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/index.ts @@ -0,0 +1 @@ +export * from "./store-currency-section" diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx new file mode 100644 index 0000000000..9d63c87c18 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx @@ -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({}) + 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 ( + +
+ {t("store.currencies")} +
+ + + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
+ + + + + {t("general.countSelected", { + count: Object.keys(rowSelection).length, + })} + + + + + +
+ ) +} + +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 ( + , + label: t("actions.remove"), + onClick: handleRemove, + }, + ], + }, + ]} + /> + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + 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 ( + {text} + ) + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row, table }) => { + const { currencyCodes, storeId } = table.options.meta as { + currencyCodes: string[] + storeId: string + } + + return ( + + ) + }, + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/index.ts new file mode 100644 index 0000000000..0ea68a992d --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/index.ts @@ -0,0 +1 @@ +export * from "./store-general-section" diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/store-general-section.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/store-general-section.tsx new file mode 100644 index 0000000000..ccc4f022de --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/components/store-general-section/store-general-section.tsx @@ -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 ( + +
+
+ {t("store.domain")} + + {t("store.manageYourStoresDetails")} + +
+ + + +
+
+ + {t("fields.name")} + + + {store.name} + +
+
+ + {t("store.defaultCurrency")} + +
+ + {store.default_currency_code.toUpperCase()} + + + {store.default_currency.name} + +
+
+
+ + {t("store.swapLinkTemplate")} + + {store.swap_link_template ? ( +
+ + {store.swap_link_template} + + +
+ ) : ( + + - + + )} +
+
+ + {t("store.paymentLinkTemplate")} + + {store.payment_link_template ? ( +
+ + {store.payment_link_template} + + +
+ ) : ( + + - + + )} +
+
+ + {t("store.inviteLinkTemplate")} + + {store.invite_link_template ? ( +
+ + {store.invite_link_template} + + +
+ ) : ( + + - + + )} +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/index.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/index.ts new file mode 100644 index 0000000000..bc9d614088 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/index.ts @@ -0,0 +1,2 @@ +export { storeLoader as loader } from "./loader" +export { StoreDetail as Component } from "./store-detail" diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/loader.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/loader.ts new file mode 100644 index 0000000000..4060a8000c --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/loader.ts @@ -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> +) => { + 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>( + query.queryKey + ) ?? (await fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-detail/store-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/store-detail.tsx new file mode 100644 index 0000000000..1b9e0a2b65 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-detail/store-detail.tsx @@ -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> + + const { store, isLoading, isError, error } = useV2Store({ + initialData: initialData, + }) + + if (isLoading) { + return
Loading...
+ } + + if (isError || !store) { + if (error) { + throw error + } + + return
{JSON.stringify(error, null, 2)}
+ } + + return ( +
+ + + + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-edit/components/edit-store-form/edit-store-form.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/components/edit-store-form/edit-store-form.tsx new file mode 100644 index 0000000000..e9f8257149 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/components/edit-store-form/edit-store-form.tsx @@ -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>({ + 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 ( + +
+ +
+ ( + + {t("fields.name")} + + + + + + )} + /> + ( + + {t("store.swapLinkTemplate")} + + + + + + )} + /> + ( + + {t("store.paymentLinkTemplate")} + + + + + + )} + /> + ( + + {t("store.inviteLinkTemplate")} + + + + + + )} + /> +
+
+ +
+ + + + +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-edit/index.ts b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/index.ts new file mode 100644 index 0000000000..16ec90284f --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/index.ts @@ -0,0 +1 @@ +export { StoreEdit as Component } from "./store-edit" diff --git a/packages/admin-next/dashboard/src/v2-routes/store/store-edit/store-edit.tsx b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/store-edit.tsx new file mode 100644 index 0000000000..fe4d8daa74 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/store/store-edit/store-edit.tsx @@ -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 ( + + + {t("store.editStore")} + + {store && } + + ) +} diff --git a/packages/admin-next/dashboard/src/vite-env.d.ts b/packages/admin-next/dashboard/src/vite-env.d.ts index 0351443b82..04d24530dd 100644 --- a/packages/admin-next/dashboard/src/vite-env.d.ts +++ b/packages/admin-next/dashboard/src/vite-env.d.ts @@ -2,7 +2,7 @@ interface ImportMetaEnv { readonly MEDUSA_ADMIN_BACKEND_URL: string - readonly MEDUSA_V2: boolean + readonly VITE_MEDUSA_V2: boolean } interface ImportMeta { diff --git a/packages/auth/src/providers/email-password.ts b/packages/auth/src/providers/email-password.ts index fc239438a7..39a9c95a7b 100644 --- a/packages/auth/src/providers/email-password.ts +++ b/packages/auth/src/providers/email-password.ts @@ -1,9 +1,9 @@ +import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types" import { AbstractAuthModuleProvider, MedusaError, isString, } from "@medusajs/utils" -import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types" import { AuthUserService } from "@services" import Scrypt from "scrypt-kdf" diff --git a/packages/core-flows/src/defaults/index.ts b/packages/core-flows/src/defaults/index.ts new file mode 100644 index 0000000000..68de82c9f9 --- /dev/null +++ b/packages/core-flows/src/defaults/index.ts @@ -0,0 +1,2 @@ +export * from "./steps" +export * from "./workflows" diff --git a/packages/core-flows/src/defaults/steps/create-default-store.ts b/packages/core-flows/src/defaults/steps/create-default-store.ts new file mode 100644 index 0000000000..5c8c95d1b7 --- /dev/null +++ b/packages/core-flows/src/defaults/steps/create-default-store.ts @@ -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( + ModuleRegistrationName.STORE + ) + + await service.delete(data.storeId) + } +) diff --git a/packages/core-flows/src/defaults/steps/index.ts b/packages/core-flows/src/defaults/steps/index.ts new file mode 100644 index 0000000000..aea300000d --- /dev/null +++ b/packages/core-flows/src/defaults/steps/index.ts @@ -0,0 +1 @@ +export * from "./create-default-store" diff --git a/packages/core-flows/src/defaults/workflows/create-defaults.ts b/packages/core-flows/src/defaults/workflows/create-defaults.ts new file mode 100644 index 0000000000..847a79996d --- /dev/null +++ b/packages/core-flows/src/defaults/workflows/create-defaults.ts @@ -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 + } +) diff --git a/packages/core-flows/src/defaults/workflows/index.ts b/packages/core-flows/src/defaults/workflows/index.ts new file mode 100644 index 0000000000..9f965e552e --- /dev/null +++ b/packages/core-flows/src/defaults/workflows/index.ts @@ -0,0 +1 @@ +export * from "./create-defaults" diff --git a/packages/core-flows/src/index.ts b/packages/core-flows/src/index.ts index 12d1a64135..00f215b61b 100644 --- a/packages/core-flows/src/index.ts +++ b/packages/core-flows/src/index.ts @@ -2,6 +2,7 @@ export * from "./api-key" export * from "./auth" export * from "./customer" export * from "./customer-group" +export * from "./defaults" export * from "./definition" export * from "./definitions" export * from "./fulfillment" diff --git a/packages/core-flows/src/sales-channel/index.ts b/packages/core-flows/src/sales-channel/index.ts index 68de82c9f9..e58562ad24 100644 --- a/packages/core-flows/src/sales-channel/index.ts +++ b/packages/core-flows/src/sales-channel/index.ts @@ -1,2 +1,3 @@ export * from "./steps" export * from "./workflows" + diff --git a/packages/core-flows/src/sales-channel/steps/create-default-sales-channel.ts b/packages/core-flows/src/sales-channel/steps/create-default-sales-channel.ts new file mode 100644 index 0000000000..d28b210fad --- /dev/null +++ b/packages/core-flows/src/sales-channel/steps/create-default-sales-channel.ts @@ -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( + 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( + ModuleRegistrationName.SALES_CHANNEL + ) + + await service.delete(data.id) + } +) diff --git a/packages/core-flows/src/sales-channel/steps/index.ts b/packages/core-flows/src/sales-channel/steps/index.ts index cd51b9588a..662d94bce6 100644 --- a/packages/core-flows/src/sales-channel/steps/index.ts +++ b/packages/core-flows/src/sales-channel/steps/index.ts @@ -1,4 +1,5 @@ export * from "./associate-products-with-channels" +export * from "./create-default-sales-channel" export * from "./create-sales-channels" export * from "./delete-sales-channels" export * from "./update-sales-channels" diff --git a/packages/currency/src/joiner-config.ts b/packages/currency/src/joiner-config.ts index dc807cb8ae..fe1da1c054 100644 --- a/packages/currency/src/joiner-config.ts +++ b/packages/currency/src/joiner-config.ts @@ -3,7 +3,9 @@ import { ModuleJoinerConfig } from "@medusajs/types" import { MapToConfig } from "@medusajs/utils" import Currency from "./models/currency" -export const LinkableKeys: Record = {} +export const LinkableKeys: Record = { + code: Currency.name, +} const entityLinkableKeysMap: MapToConfig = {} Object.entries(LinkableKeys).forEach(([key, value]) => { diff --git a/packages/link-modules/src/definitions/index.ts b/packages/link-modules/src/definitions/index.ts index 39d7f87935..f7246f0717 100644 --- a/packages/link-modules/src/definitions/index.ts +++ b/packages/link-modules/src/definitions/index.ts @@ -14,4 +14,5 @@ export * from "./publishable-api-key-sales-channel" export * from "./region-payment-provider" export * from "./sales-channel-location" export * from "./shipping-option-price-set" +export * from "./store-default-currency" diff --git a/packages/link-modules/src/definitions/store-default-currency.ts b/packages/link-modules/src/definitions/store-default-currency.ts new file mode 100644 index 0000000000..b2d3828256 --- /dev/null +++ b/packages/link-modules/src/definitions/store-default-currency.ts @@ -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", + }, + }, + ], +} diff --git a/packages/medusa/src/api-v2/admin/stores/query-config.ts b/packages/medusa/src/api-v2/admin/stores/query-config.ts index dc879e64c5..07e49acdbf 100644 --- a/packages/medusa/src/api-v2/admin/stores/query-config.ts +++ b/packages/medusa/src/api-v2/admin/stores/query-config.ts @@ -1,10 +1,11 @@ -export const defaultAdminStoreRelations = [] -export const allowedAdminStoreRelations = [] export const defaultAdminStoreFields = [ "id", "name", "supported_currency_codes", "default_currency_code", + "default_currency.name", + "default_currency.symbol", + "default_currency.symbol_native", "default_sales_channel_id", "default_region_id", "default_location_id", @@ -14,9 +15,7 @@ export const defaultAdminStoreFields = [ ] export const retrieveTransformQueryConfig = { - defaultFields: defaultAdminStoreFields, - defaultRelations: defaultAdminStoreRelations, - allowedRelations: allowedAdminStoreRelations, + defaults: defaultAdminStoreFields, isList: false, } diff --git a/packages/medusa/src/api-v2/admin/stores/route.ts b/packages/medusa/src/api-v2/admin/stores/route.ts index 5f6750481b..ca70ca7347 100644 --- a/packages/medusa/src/api-v2/admin/stores/route.ts +++ b/packages/medusa/src/api-v2/admin/stores/route.ts @@ -1,6 +1,6 @@ import { ContainerRegistrationKeys, - remoteQueryObjectFromString + remoteQueryObjectFromString, } from "@medusajs/utils" import { MedusaRequest, MedusaResponse } from "../../../types/routing" import { defaultAdminStoreFields } from "./query-config" diff --git a/packages/medusa/src/commands/user.js b/packages/medusa/src/commands/user.js index 9870cfd684..a5039da207 100644 --- a/packages/medusa/src/commands/user.js +++ b/packages/medusa/src/commands/user.js @@ -6,6 +6,31 @@ import { track } from "medusa-telemetry" import loaders from "../loaders" 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 ({ directory, @@ -33,8 +58,15 @@ export default async function ({ Invite token: ${invite[0].token} Open the invite in Medusa Admin at: [your-admin-url]/invite?token=${invite[0].token}`) } else { - const userService = container.resolve("userService") - await userService.create({ id, email }, password) + const configModule = configModuleLoader(directory) + const featureFlagRouter = featureFlagLoader(configModule) + + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + await createV2User({ email, password }, { container }) + } else { + const userService = container.resolve("userService") + await userService.create({ id, email }, password) + } } } catch (err) { console.error(err) diff --git a/packages/medusa/src/loaders/helpers/routing/index.ts b/packages/medusa/src/loaders/helpers/routing/index.ts index dee0a94c18..bf95351030 100644 --- a/packages/medusa/src/loaders/helpers/routing/index.ts +++ b/packages/medusa/src/loaders/helpers/routing/index.ts @@ -11,6 +11,7 @@ import { requireCustomerAuthentication, } from "../../../api/middlewares" import { ConfigModule } from "../../../types/global" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" import logger from "../../logger" import { AsyncRouteHandler, @@ -19,12 +20,11 @@ import { MiddlewareRoute, MiddlewareVerb, MiddlewaresConfig, + ParserConfigArgs, RouteConfig, RouteDescriptor, RouteVerb, - ParserConfigArgs, } from "./types" -import { MedusaRequest, MedusaResponse } from "../../../types/routing" const log = ({ activityId, diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index 3741264d37..fa2b9d798f 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -1,12 +1,13 @@ +import { createDefaultsWorkflow } from "@medusajs/core-flows" import { InternalModuleDeclaration, - ModulesDefinition, + ModulesDefinition } from "@medusajs/modules-sdk" import { MODULE_RESOURCE_TYPE } from "@medusajs/types" import { ContainerRegistrationKeys, - isString, MedusaV2Flag, + isString, } from "@medusajs/utils" import { asValue } from "awilix" import { Express, NextFunction, Request, Response } from "express" @@ -23,6 +24,7 @@ import databaseLoader, { dataSource } from "./database" import defaultsLoader from "./defaults" import expressLoader from "./express" import featureFlagsLoader from "./feature-flags" +import medusaProjectApisLoader from "./load-medusa-project-apis" import Logger from "./logger" import loadMedusaApp, { mergeDefaultModules } from "./medusa-app" import modelsLoader from "./models" @@ -35,7 +37,6 @@ import searchIndexLoader from "./search-index" import servicesLoader from "./services" import strategiesLoader from "./strategies" import subscribersLoader from "./subscribers" -import medusaProjectApisLoader from "./load-medusa-project-apis" type Options = { directory: string @@ -130,7 +131,7 @@ async function loadMedusaV2({ featureFlagRouter, }) - medusaProjectApisLoader({ + await medusaProjectApisLoader({ rootDirectory, container, app: expressApp, @@ -138,6 +139,8 @@ async function loadMedusaV2({ activityId: "medusa-project-apis", }) + await createDefaultsWorkflow(container).run() + return { container, app: expressApp,