feat(admin, admin-ui, medusa-js, medusa-react, medusa): Support Admin Extensions (#4761)
Co-authored-by: Rares Stefan <948623+StephixOne@users.noreply.github.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
co-authored by
Rares Stefan
Oli Juhl
parent
26c78bbc03
commit
f1a05f4725
@@ -2,6 +2,8 @@ import { DndProvider } from "react-dnd"
|
||||
import { HTML5Backend } from "react-dnd-html5-backend"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { Route, Routes, useNavigate } from "react-router-dom"
|
||||
import RouteContainer from "../components/extensions/route-container"
|
||||
import RouteErrorElement from "../components/extensions/route-container/route-error-element"
|
||||
import PrivateRoute from "../components/private-route"
|
||||
import SEO from "../components/seo"
|
||||
import Layout from "../components/templates/layout"
|
||||
@@ -15,12 +17,14 @@ import Oauth from "../domain/oauth"
|
||||
import Orders from "../domain/orders"
|
||||
import DraftOrders from "../domain/orders/draft-orders"
|
||||
import Pricing from "../domain/pricing"
|
||||
import ProductCategories from "../domain/product-categories"
|
||||
import ProductsRoute from "../domain/products"
|
||||
import PublishableApiKeys from "../domain/publishable-api-keys"
|
||||
import SalesChannels from "../domain/sales-channels"
|
||||
import Settings from "../domain/settings"
|
||||
import { AnalyticsProvider } from "../providers/analytics-provider"
|
||||
import ProductCategories from "../domain/product-categories"
|
||||
import { useRoutes } from "../providers/route-provider"
|
||||
import { isRoute } from "../utils/extensions"
|
||||
|
||||
const IndexPage = () => {
|
||||
const navigate = useNavigate()
|
||||
@@ -35,6 +39,10 @@ const IndexPage = () => {
|
||||
}
|
||||
|
||||
const DashboardRoutes = () => {
|
||||
const { getTopLevelRoutes } = useRoutes()
|
||||
|
||||
const injectedRoutes = getTopLevelRoutes() || []
|
||||
|
||||
return (
|
||||
<AnalyticsProvider writeKey={WRITE_KEY}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
@@ -61,6 +69,20 @@ const DashboardRoutes = () => {
|
||||
element={<PublishableApiKeys />}
|
||||
/>
|
||||
<Route path="inventory/*" element={<Inventory />} />
|
||||
{injectedRoutes.map((route, index) => {
|
||||
return (
|
||||
<Route
|
||||
key={index}
|
||||
path={`/${route.path}/*`}
|
||||
errorElement={
|
||||
<RouteErrorElement
|
||||
origin={isRoute(route) ? route.origin : ""}
|
||||
/>
|
||||
}
|
||||
element={<RouteContainer route={route} />}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Routes>
|
||||
</Layout>
|
||||
</DndProvider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAdminAcceptInvite } from "medusa-react"
|
||||
import { useAdminAcceptInvite, useAdminLogin } from "medusa-react"
|
||||
import qs from "qs"
|
||||
import { useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
@@ -12,12 +12,18 @@ import PublicLayout from "../components/templates/login-layout"
|
||||
import useNotification from "../hooks/use-notification"
|
||||
import { getErrorMessage } from "../utils/error-messages"
|
||||
import FormValidator from "../utils/form-validator"
|
||||
import { analytics, useAdminCreateAnalyticsConfig } from "../services/analytics"
|
||||
import AnalyticsConfigForm, {
|
||||
AnalyticsConfigFormType,
|
||||
} from "../components/organisms/analytics-config-form"
|
||||
import { nestedForm } from "../utils/nested-form"
|
||||
|
||||
type FormValues = {
|
||||
password: string
|
||||
repeat_password: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
analytics: AnalyticsConfigFormType
|
||||
}
|
||||
|
||||
const InvitePage = () => {
|
||||
@@ -25,7 +31,14 @@ const InvitePage = () => {
|
||||
const parsed = qs.parse(location.search.substring(1))
|
||||
const [signUp, setSignUp] = useState(false)
|
||||
|
||||
let token: Object | null = null
|
||||
const first_run = !!parsed.first_run
|
||||
|
||||
let token: {
|
||||
iat: number
|
||||
invite_id: string
|
||||
role: string
|
||||
user_email: string
|
||||
} | null = null
|
||||
if (parsed?.token) {
|
||||
try {
|
||||
token = decodeToken(parsed.token as string)
|
||||
@@ -34,25 +47,41 @@ const InvitePage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setError,
|
||||
} = useForm<FormValues>({
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
password: "",
|
||||
repeat_password: "",
|
||||
analytics: {
|
||||
opt_out: false,
|
||||
anonymize: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate, isLoading } = useAdminAcceptInvite()
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setError,
|
||||
} = form
|
||||
|
||||
const { mutateAsync: acceptInvite, isLoading: acceptInviteIsLoading } =
|
||||
useAdminAcceptInvite()
|
||||
const {
|
||||
mutateAsync: createAnalyticsConfig,
|
||||
isLoading: createAnalyticsConfigIsLoading,
|
||||
} = useAdminCreateAnalyticsConfig()
|
||||
const { mutateAsync: doLogin, isLoading: loginIsLoading } = useAdminLogin()
|
||||
|
||||
const isLoading =
|
||||
acceptInviteIsLoading || createAnalyticsConfigIsLoading || loginIsLoading
|
||||
|
||||
const navigate = useNavigate()
|
||||
const notification = useNotification()
|
||||
|
||||
const handleAcceptInvite = handleSubmit((data: FormValues) => {
|
||||
const handleAcceptInvite = handleSubmit(async (data: FormValues) => {
|
||||
if (data.password !== data.repeat_password) {
|
||||
setError(
|
||||
"repeat_password",
|
||||
@@ -68,24 +97,35 @@ const InvitePage = () => {
|
||||
return
|
||||
}
|
||||
|
||||
mutate(
|
||||
{
|
||||
try {
|
||||
await acceptInvite({
|
||||
token: parsed.token as string,
|
||||
user: {
|
||||
first_name: data.first_name,
|
||||
last_name: data.last_name,
|
||||
password: data.password,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
navigate("/login")
|
||||
},
|
||||
onError: (err) => {
|
||||
notification("Error", getErrorMessage(err), "error")
|
||||
},
|
||||
})
|
||||
|
||||
await doLogin({ email: token!.user_email, password: data.password })
|
||||
|
||||
const shouldTrackEmail =
|
||||
!data.analytics.anonymize &&
|
||||
!data.analytics.opt_out &&
|
||||
token?.user_email
|
||||
|
||||
await createAnalyticsConfig(data.analytics)
|
||||
|
||||
if (shouldTrackEmail) {
|
||||
await analytics.track("userEmail", {
|
||||
email: token?.user_email,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
navigate("/a/orders")
|
||||
} catch (err) {
|
||||
notification("Error", getErrorMessage(err), "error")
|
||||
}
|
||||
})
|
||||
|
||||
if (!token) {
|
||||
@@ -113,30 +153,13 @@ const InvitePage = () => {
|
||||
<SEO title="Create Account" />
|
||||
{signUp ? (
|
||||
<form onSubmit={handleAcceptInvite}>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex w-[300px] flex-col items-center">
|
||||
<h1 className="inter-xlarge-semibold mb-large text-[20px]">
|
||||
Create your Medusa account
|
||||
</h1>
|
||||
<div className="gap-y-small flex flex-col">
|
||||
<div>
|
||||
<SigninInput
|
||||
placeholder="First name"
|
||||
{...register("first_name", {
|
||||
required: FormValidator.required("First name"),
|
||||
})}
|
||||
autoComplete="given-name"
|
||||
/>
|
||||
<InputError errors={errors} name="first_name" />
|
||||
</div>
|
||||
<div>
|
||||
<SigninInput
|
||||
placeholder="Last name"
|
||||
{...register("last_name", {
|
||||
required: FormValidator.required("Last name"),
|
||||
})}
|
||||
autoComplete="family-name"
|
||||
/>
|
||||
<InputError errors={errors} name="last_name" />
|
||||
<SigninInput readOnly placeholder={token.user_email} />
|
||||
</div>
|
||||
<div>
|
||||
<SigninInput
|
||||
@@ -160,10 +183,16 @@ const InvitePage = () => {
|
||||
<InputError errors={errors} name="repeat_password" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="gap-y-small my-8 flex w-[300px] flex-col">
|
||||
<AnalyticsConfigForm
|
||||
form={nestedForm(form, "analytics")}
|
||||
compact={true}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="mt-large w-[280px]"
|
||||
className="mt-large w-[300px]"
|
||||
loading={isLoading}
|
||||
>
|
||||
Create account
|
||||
@@ -176,17 +205,25 @@ const InvitePage = () => {
|
||||
) : (
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<h1 className="inter-xlarge-semibold text-[20px]">
|
||||
You have been invited to join the team
|
||||
{first_run
|
||||
? `Let's get you started!`
|
||||
: `You have been invited to join the team`}
|
||||
</h1>
|
||||
<p className="inter-base-regular text-grey-50 mt-xsmall">
|
||||
You can now join the team. Sign up below and get started
|
||||
<br />
|
||||
with your Medusa account right away.
|
||||
</p>
|
||||
{first_run ? (
|
||||
<p className="inter-base-regular text-grey-50 mt-xsmall">
|
||||
Create an admin account to access your <br /> Medusa dashboard.
|
||||
</p>
|
||||
) : (
|
||||
<p className="inter-base-regular text-grey-50 mt-xsmall">
|
||||
You can now join the team. Sign up below and get started
|
||||
<br />
|
||||
with your Medusa account right away.
|
||||
</p>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="mt-xlarge w-[280px]"
|
||||
className="mt-xlarge w-[300px]"
|
||||
onClick={() => setSignUp(true)}
|
||||
>
|
||||
Sign up
|
||||
|
||||
@@ -38,7 +38,6 @@ const LoginPage = () => {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<SEO title="Login" />
|
||||
|
||||
{resetPassword ? (
|
||||
<ResetTokenCard goBack={showLogin} />
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user