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:
Kasper Fabricius Kristensen
2023-08-17 14:14:45 +02:00
committed by GitHub
parent 26c78bbc03
commit f1a05f4725
189 changed files with 14570 additions and 12773 deletions

View File

@@ -3,6 +3,8 @@ import { useEffect } from "react"
import { Controller, useWatch } from "react-hook-form"
import { NestedForm } from "../../../utils/nested-form"
import Switch from "../../atoms/switch"
import InfoIcon from "../../fundamentals/icons/info-icon"
import Tooltip from "../../atoms/tooltip"
export type AnalyticsConfigFormType = {
anonymize: boolean
@@ -11,9 +13,10 @@ export type AnalyticsConfigFormType = {
type Props = {
form: NestedForm<AnalyticsConfigFormType>
compact?: boolean
}
const AnalyticsConfigForm = ({ form }: Props) => {
const AnalyticsConfigForm = ({ form, compact }: Props) => {
const { control, setValue, path } = form
const watchOptOut = useWatch({
@@ -31,17 +34,33 @@ const AnalyticsConfigForm = ({ form }: Props) => {
return (
<div className="gap-y-xlarge flex flex-col">
<div
className={clsx("flex items-start transition-opacity", {
className={clsx("flex items-center gap-3 transition-opacity", {
"opacity-50": watchOptOut,
})}
>
<div className="gap-y-2xsmall flex flex-1 flex-col">
<h2 className="inter-base-semibold">Anonymize my usage data</h2>
<p className="inter-base-regular text-grey-50">
You can choose to anonymize your usage data. If this option is
selected, we will not collect your personal information, such as
your name and email address.
</p>
<div className="flex items-center">
<h2 className="inter-base-semibold mr-2">
Anonymize my usage data{" "}
</h2>
{compact && (
<Tooltip
content="You can choose to anonymize your usage data. If this option is
selected, we will not collect your personal information, such as
your name and email address."
side="top"
>
<InfoIcon size="18px" color={"#889096"} />
</Tooltip>
)}
</div>
{!compact && (
<p className="inter-base-regular text-grey-50">
You can choose to anonymize your usage data. If this option is
selected, we will not collect your personal information, such as
your name and email address.
</p>
)}
</div>
<Controller
name={path("anonymize")}
@@ -57,14 +76,26 @@ const AnalyticsConfigForm = ({ form }: Props) => {
}}
/>
</div>
<div className="flex items-start">
<div className="flex items-center gap-3">
<div className="gap-y-2xsmall flex flex-1 flex-col">
<h2 className="inter-base-semibold">
Opt out of sharing my usage data
</h2>
<p className="inter-base-regular text-grey-50">
You can always opt out of sharing your usage data at any time.
</p>
<div className="flex items-center">
<h2 className="inter-base-semibold mr-2">
Opt out of sharing my usage data
</h2>
{compact && (
<Tooltip
content="You can always opt out of sharing your usage data at any time."
side="top"
>
<InfoIcon size="18px" color={"#889096"} />
</Tooltip>
)}
</div>
{!compact && (
<p className="inter-base-regular text-grey-50">
You can always opt out of sharing your usage data at any time.
</p>
)}
</div>
<Controller
name={path("opt_out")}

View File

@@ -1,7 +1,9 @@
import { useAdminLogin } from "medusa-react"
import { useForm } from "react-hook-form"
import { useNavigate } from "react-router-dom"
import { useWidgets } from "../../../providers/widget-provider"
import InputError from "../../atoms/input-error"
import WidgetContainer from "../../extensions/widget-container"
import Button from "../../fundamentals/button"
import SigninInput from "../../molecules/input-signin"
@@ -24,6 +26,8 @@ const LoginCard = ({ toResetPassword }: LoginCardProps) => {
const navigate = useNavigate()
const { mutate, isLoading } = useAdminLogin()
const { getWidgets } = useWidgets()
const onSubmit = (values: FormValues) => {
mutate(values, {
onSuccess: () => {
@@ -44,44 +48,66 @@ const LoginCard = ({ toResetPassword }: LoginCardProps) => {
})
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col items-center">
<h1 className="inter-xlarge-semibold text-grey-90 mb-large text-[20px]">
Log in to Medusa
</h1>
<div>
<SigninInput
placeholder="Email"
{...register("email", { required: true })}
autoComplete="email"
className="mb-small"
<div className="gap-y-large flex flex-col">
{getWidgets("login.before").map((w, i) => {
return (
<WidgetContainer
key={i}
widget={w}
injectionZone="login.before"
entity={undefined}
/>
<SigninInput
placeholder="Password"
type={"password"}
{...register("password", { required: true })}
autoComplete="current-password"
className="mb-xsmall"
/>
<InputError errors={errors} name="password" />
)
})}
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col items-center">
<h1 className="inter-xlarge-semibold text-grey-90 mb-large text-[20px]">
Log in to Medusa
</h1>
<div>
<SigninInput
placeholder="Email"
{...register("email", { required: true })}
autoComplete="email"
className="mb-small"
/>
<SigninInput
placeholder="Password"
type={"password"}
{...register("password", { required: true })}
autoComplete="current-password"
className="mb-xsmall"
/>
<InputError errors={errors} name="password" />
</div>
<Button
className="rounded-rounded inter-base-regular mt-4 w-[280px]"
variant="secondary"
size="medium"
type="submit"
loading={isLoading}
>
Continue
</Button>
<span
className="inter-small-regular text-grey-50 mt-8 cursor-pointer"
onClick={toResetPassword}
>
Forgot your password?
</span>
</div>
<Button
className="rounded-rounded inter-base-regular mt-4 w-[280px]"
variant="secondary"
size="medium"
type="submit"
loading={isLoading}
>
Continue
</Button>
<span
className="inter-small-regular text-grey-50 mt-8 cursor-pointer"
onClick={toResetPassword}
>
Forgot your password?
</span>
</div>
</form>
</form>
{getWidgets("login.after").map((w, i) => {
return (
<WidgetContainer
key={i}
widget={w}
injectionZone="login.after"
entity={undefined}
/>
)
})}
</div>
)
}

View File

@@ -2,14 +2,16 @@ import { useAdminStore } from "medusa-react"
import React, { useState } from "react"
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
import { useRoutes } from "../../../providers/route-provider"
import BuildingsIcon from "../../fundamentals/icons/buildings-icon"
import CartIcon from "../../fundamentals/icons/cart-icon"
import CashIcon from "../../fundamentals/icons/cash-icon"
import GearIcon from "../../fundamentals/icons/gear-icon"
import GiftIcon from "../../fundamentals/icons/gift-icon"
import SaleIcon from "../../fundamentals/icons/sale-icon"
import TagIcon from "../../fundamentals/icons/tag-icon"
import SquaresPlus from "../../fundamentals/icons/squares-plus"
import SwatchIcon from "../../fundamentals/icons/swatch-icon"
import TagIcon from "../../fundamentals/icons/tag-icon"
import UsersIcon from "../../fundamentals/icons/users-icon"
import SidebarMenuItem from "../../molecules/sidebar-menu-item"
import UserMenu from "../../molecules/user-menu"
@@ -22,6 +24,8 @@ const Sidebar: React.FC = () => {
const { isFeatureEnabled } = useFeatureFlag()
const { store } = useAdminStore()
const { getLinks } = useRoutes()
const triggerHandler = () => {
const id = triggerHandler.id++
return {
@@ -104,6 +108,21 @@ const Sidebar: React.FC = () => {
triggerHandler={triggerHandler}
text={"Pricing"}
/>
{getLinks().map(({ path, label, icon }, index) => {
const cleanLink = path.replace("/a/", "")
const Icon = icon ? icon : SquaresPlus
return (
<SidebarMenuItem
key={index}
pageLink={`/a${cleanLink}`}
icon={icon ? <Icon /> : <SquaresPlus size={ICON_SIZE} />}
triggerHandler={triggerHandler}
text={label}
/>
)
})}
<SidebarMenuItem
pageLink={"/a/settings"}
icon={<GearIcon size={ICON_SIZE} />}