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

@@ -26,7 +26,7 @@ const SettingsCard: React.FC<SettingsCardProps> = ({
return (
<Link to={to ?? ""} className="flex flex-1 items-center">
<button
className="bg-grey-0 rounded-rounded p-large border-grey-20 group flex h-full flex-1 items-center border"
className="bg-grey-0 rounded-rounded p-base border-grey-20 group flex h-full flex-1 items-center border"
disabled={disabled}
onClick={() => {
if (externalLink) {
@@ -34,8 +34,10 @@ const SettingsCard: React.FC<SettingsCardProps> = ({
}
}}
>
<div className="h-2xlarge w-2xlarge bg-grey-10 rounded-circle text-grey-60 group-disabled:bg-grey-10 group-disabled:text-grey-40 flex items-center justify-center">
{icon}
<div className="h-2xlarge w-2xlarge bg-grey-0 rounded-rounded border-grey-20 text-grey-60 group-disabled:bg-grey-10 group-disabled:text-grey-40 flex items-center justify-center border">
<div className="bg-grey-10 h-xlarge w-xlarge flex items-center justify-center overflow-hidden rounded-md">
{icon}
</div>
</div>
<div className="mx-large flex-1 text-left">
<h3 className="inter-large-semibold text-grey-90 group-disabled:text-grey-40 m-0">

View File

@@ -0,0 +1,80 @@
import React from "react"
import { Route, Routes } from "react-router-dom"
import { useRoutes } from "../../../providers/route-provider"
import { Route as AdminRoute, RouteSegment } from "../../../types/extensions"
import { isRoute } from "../../../utils/extensions"
import RouteErrorElement from "./route-error-element"
import { useRouteContainerProps } from "./use-route-container-props"
type RouteContainerProps = {
route: AdminRoute | RouteSegment
previousPath?: string
}
const RouteContainer = ({ route, previousPath = "" }: RouteContainerProps) => {
const routeContainerProps = useRouteContainerProps()
const isFullRoute = isRoute(route)
const { path } = route
const { getNestedRoutes } = useRoutes()
const fullPath = `${previousPath}${path}`
const nestedRoutes = getNestedRoutes(fullPath)
const hasNestedRoutes = nestedRoutes.length > 0
/**
* If the route is only a segment, we need to render the nested routes that
* are children of the segment. If the segment has no nested routes, we
* return null.
*/
if (!isFullRoute) {
if (hasNestedRoutes) {
return (
<Routes>
{nestedRoutes.map((r, i) => (
<Route
path={r.path}
key={i}
element={<RouteContainer route={r} previousPath={fullPath} />}
/>
))}
</Routes>
)
}
return null
}
const { Page, origin } = route
const PageWithProps = React.createElement(Page, routeContainerProps)
if (!hasNestedRoutes) {
return PageWithProps
}
return (
<>
<Routes>
<Route
path={"/"}
element={PageWithProps}
errorElement={<RouteErrorElement origin={origin} />}
/>
{nestedRoutes.map((r, i) => (
<Route
path={r.path}
key={i}
element={<RouteContainer route={r} previousPath={fullPath} />}
/>
))}
</Routes>
</>
)
}
export default RouteContainer

View File

@@ -0,0 +1,75 @@
import { useEffect } from "react"
import { useRouteError } from "react-router-dom"
import Button from "../../fundamentals/button"
import RefreshIcon from "../../fundamentals/icons/refresh-icon"
import WarningCircleIcon from "../../fundamentals/icons/warning-circle"
type PageErrorElementProps = {
origin: string
}
const isProd = process.env.NODE_ENV === "production"
const RouteErrorElement = ({ origin }: PageErrorElementProps) => {
const error = useRouteError()
useEffect(() => {
if (!isProd && error) {
console.group(
`%cAn error occurred in a page from ${origin}:`,
"color: red; font-weight: bold;"
)
console.error(error)
console.groupEnd()
}
}, [error, origin])
const reload = () => {
window.location.reload()
}
return (
<div className="flex h-full w-full items-center justify-center">
<div className="rounded-rounded p-base bg-rose-10 border-rose-40 gap-x-small flex justify-start border">
<div>
<WarningCircleIcon
size={20}
fillType="solid"
className="text-rose-40"
/>
</div>
<div className="text-rose-40 inter-small-regular w-full pr-[20px]">
<h1 className="inter-base-semibold mb-2xsmall">Uncaught error</h1>
<p className="mb-small">
{isProd
? "An error unknown error occurred, and the page could not be loaded."
: `A Page from <strong>${origin}</strong> crashed. See the console for more info.`}
</p>
<p className="mb-large">
<strong>What should I do?</strong>
<br />
If you are the developer of this page, you should fix the error and
reload the page. If you are not the developer, you should contact
the maintainer and report the error.
</p>
<div className="gap-x-base flex items-center">
<Button
variant="nuclear"
size="small"
type="button"
onClick={reload}
className="w-full"
>
<div className="flex items-center">
<RefreshIcon size="20" />
<span className="ml-xsmall">Reload</span>
</div>
</Button>
</div>
</div>
</div>
</div>
)
}
export default RouteErrorElement

View File

@@ -0,0 +1,8 @@
import { useExtensionBaseProps } from "../../../hooks/use-extension-base-props"
import { RouteProps } from "../../../types/extensions"
export const useRouteContainerProps = (): RouteProps => {
const baseProps = useExtensionBaseProps()
return baseProps
}

View File

@@ -0,0 +1,14 @@
import React, { ComponentType } from "react"
import { useSettingContainerProps } from "./use-setting-container-props"
type SettingContainerProps = {
Page: ComponentType<any>
}
const SettingContainer = ({ Page }: SettingContainerProps) => {
const props = useSettingContainerProps()
return React.createElement(Page, props)
}
export default SettingContainer

View File

@@ -0,0 +1,77 @@
import { useEffect } from "react"
import { useRouteError } from "react-router-dom"
import Button from "../../fundamentals/button"
import RefreshIcon from "../../fundamentals/icons/refresh-icon"
import WarningCircleIcon from "../../fundamentals/icons/warning-circle"
type SettingsPageErrorElementProps = {
origin: string
}
const isProd = process.env.NODE_ENV === "production"
const SettingsPageErrorElement = ({
origin,
}: SettingsPageErrorElementProps) => {
const error = useRouteError()
useEffect(() => {
if (!isProd && error) {
console.group(
`%cAn error occurred in a settings page from ${origin}:`,
"color: red; font-weight: bold;"
)
console.error(error)
console.groupEnd()
}
}, [error, origin])
const reload = () => {
window.location.reload()
}
return (
<div className="flex h-full w-full items-center justify-center">
<div className="rounded-rounded p-base bg-rose-10 border-rose-40 gap-x-small flex justify-start border">
<div>
<WarningCircleIcon
size={20}
fillType="solid"
className="text-rose-40"
/>
</div>
<div className="text-rose-40 inter-small-regular w-full pr-[20px]">
<h1 className="inter-base-semibold mb-2xsmall">Uncaught error</h1>
<p className="mb-small">
{isProd
? "An error unknown error occurred, and the page could not be loaded."
: `A Page from <strong>${origin}</strong> crashed. See the console for more info.`}
</p>
<p className="mb-large">
<strong>What should I do?</strong>
<br />
If you are the developer of this setting page, you should fix the
error and reload the page. If you are not the developer, you should
contact the maintainer and report the error.
</p>
<div className="gap-x-base flex items-center">
<Button
variant="nuclear"
size="small"
type="button"
onClick={reload}
className="w-full"
>
<div className="flex items-center">
<RefreshIcon size="20" />
<span className="ml-xsmall">Reload</span>
</div>
</Button>
</div>
</div>
</div>
</div>
)
}
export default SettingsPageErrorElement

View File

@@ -0,0 +1,7 @@
import { useExtensionBaseProps } from "../../../hooks/use-extension-base-props"
export const useSettingContainerProps = () => {
const baseProps = useExtensionBaseProps()
return baseProps
}

View File

@@ -0,0 +1,32 @@
import React from "react"
import { InjectionZone, Widget } from "../../../types/extensions"
import { EntityMap } from "./types"
import { useWidgetContainerProps } from "./use-widget-container-props"
import WidgetErrorBoundary from "./widget-error-boundary"
type WidgetContainerProps<T extends keyof EntityMap> = {
injectionZone: T
widget: Widget
entity: EntityMap[T]
}
const WidgetContainer = <T extends InjectionZone>({
injectionZone,
widget,
entity,
}: WidgetContainerProps<T>) => {
const { Widget, origin } = widget
const props = useWidgetContainerProps({
injectionZone,
entity,
})
return (
<WidgetErrorBoundary origin={origin}>
{React.createElement(Widget, props)}
</WidgetErrorBoundary>
)
}
export default WidgetContainer

View File

@@ -0,0 +1,77 @@
import {
Customer,
CustomerGroup,
Discount,
DraftOrder,
GiftCard,
Order,
PriceList,
Product,
ProductCollection,
} from "@medusajs/medusa"
export type EntityMap = {
// Details
"product.details.after": Product
"product.details.before": Product
"product_collection.details.after": ProductCollection
"product_collection.details.before": ProductCollection
"order.details.after": Order
"order.details.before": Order
"draft_order.details.after": DraftOrder
"draft_order.details.before": DraftOrder
"customer.details.after": Customer
"customer.details.before": Customer
"customer_group.details.after": CustomerGroup
"customer_group.details.before": CustomerGroup
"discount.details.after": Discount
"discount.details.before": Discount
"price_list.details.after": PriceList
"price_list.details.before": PriceList
"gift_card.details.after": Product
"gift_card.details.before": Product
"custom_gift_card.after": GiftCard
"custom_gift_card.before": GiftCard
// List
"product.list.after"?: never | null | undefined
"product.list.before"?: never | null | undefined
"product_collection.list.after"?: never | null | undefined
"product_collection.list.before"?: never | null | undefined
"order.list.after"?: never | null | undefined
"order.list.before"?: never | null | undefined
"draft_order.list.after"?: never | null | undefined
"draft_order.list.before"?: never | null | undefined
"customer.list.after"?: never | null | undefined
"customer.list.before"?: never | null | undefined
"customer_group.list.after"?: never | null | undefined
"customer_group.list.before"?: never | null | undefined
"discount.list.after"?: never | null | undefined
"discount.list.before"?: never | null | undefined
"price_list.list.after"?: never | null | undefined
"price_list.list.before"?: never | null | undefined
"gift_card.list.after"?: never | null | undefined
"gift_card.list.before"?: never | null | undefined
// Login
"login.before"?: never | null | undefined
"login.after"?: never | null | undefined
}
export const PropKeyMap = {
"product.details.after": "product",
"product.details.before": "product",
"product_collection.details.after": "productCollection",
"product_collection.details.before": "productCollection",
"order.details.after": "order",
"order.details.before": "order",
"draft_order.details.after": "draftOrder",
"draft_order.details.before": "draftOrder",
"customer.details.after": "customer",
"customer.details.before": "customer",
"customer_group.details.after": "customerGroup",
"customer_group.details.before": "customerGroup",
"discount.details.after": "discount",
"discount.details.before": "discount",
"price_list.details.after": "priceList",
"price_list.details.before": "priceList",
custom_gift_card: "giftCard",
}

View File

@@ -0,0 +1,31 @@
import { useExtensionBaseProps } from "../../../hooks/use-extension-base-props"
import { WidgetProps } from "../../../types/extensions"
import { EntityMap, PropKeyMap } from "./types"
type UseWidgetContainerProps<T extends keyof EntityMap> = {
injectionZone: T
entity?: EntityMap[T]
}
export const useWidgetContainerProps = <T extends keyof EntityMap>({
injectionZone,
entity,
}: UseWidgetContainerProps<T>) => {
const baseProps = useExtensionBaseProps() satisfies WidgetProps
/**
* Not all InjectionZones have an entity, so we need to check for it first, and then
* add it to the props if it exists.
*/
if (entity) {
const propKey = injectionZone as keyof typeof PropKeyMap
const entityKey = PropKeyMap[propKey]
return {
...baseProps,
[entityKey]: entity,
}
}
return baseProps
}

View File

@@ -0,0 +1,136 @@
import React, { ErrorInfo } from "react"
import Button from "../../fundamentals/button"
import RefreshIcon from "../../fundamentals/icons/refresh-icon"
import WarningCircleIcon from "../../fundamentals/icons/warning-circle"
import XCircleIcon from "../../fundamentals/icons/x-circle-icon"
type Props = {
children: React.ReactNode
origin: string
}
type State = {
hasError: boolean
hidden?: boolean
}
class WidgetErrorBoundary extends React.Component<Props, State> {
public state: State = {
hasError: false,
}
public static getDerivedStateFromError(_: Error): State {
return { hasError: true, hidden: false }
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
if (process.env.NODE_ENV !== "production") {
console.group(
`%cAn error occurred in a widget from ${this.props.origin}:`,
"color: red; font-weight: bold, background-color: #fff;"
)
console.error(error)
console.error(
"%cComponent Stack:",
"color: red",
errorInfo.componentStack
)
console.groupEnd()
}
}
public handleResetError() {
this.setState({ hasError: false })
}
public hideError() {
this.setState({ hidden: true })
}
public renderFallback() {
if (process.env.NODE_ENV !== "production" && !this.state.hidden) {
return (
<FallbackWidget
origin={this.props.origin}
reset={this.handleResetError.bind(this)}
hide={this.hideError.bind(this)}
/>
)
}
// Don't render anything in production
return null
}
render() {
if (this.state.hasError) {
return this.renderFallback()
}
return this.props.children
}
}
const FallbackWidget = ({
origin,
reset,
hide,
}: {
origin: string
reset: () => void
hide: () => void
}) => {
return (
<div className="rounded-rounded p-base bg-rose-10 border-rose-40 gap-x-small flex justify-start border">
<div>
<WarningCircleIcon
size={20}
fillType="solid"
className="text-rose-40"
/>
</div>
<div className="text-rose-40 inter-small-regular w-full pr-[20px]">
<h1 className="inter-base-semibold mb-2xsmall">Uncaught error</h1>
<p className="mb-small">
A widget from <strong>{origin}</strong> crashed. See the console for
more info.
</p>
<p className="mb-large">
<strong>What should I do?</strong>
<br />
If you are the developer of this widget, you should fix the error and
reload the page. If you are not the developer, you should contact the
maintainer and report the error.
</p>
<div className="gap-x-base flex items-center">
<Button
variant="nuclear"
size="small"
type="button"
onClick={hide}
className="w-full"
>
<div className="flex items-center">
<XCircleIcon size="20" />
<span className="ml-xsmall">Hide</span>
</div>
</Button>
<Button
variant="nuclear"
size="small"
type="button"
onClick={reset}
className="w-full"
>
<div className="flex items-center">
<RefreshIcon size="20" />
<span className="ml-xsmall">Reload</span>
</div>
</Button>
</div>
</div>
</div>
)
}
export default WidgetErrorBoundary

View File

@@ -0,0 +1,29 @@
import React from "react"
import IconProps from "../types/icon-type"
const ArrowUTurnLeft: React.FC<IconProps> = ({
size = "20",
color = "currentColor",
...attributes
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
{...attributes}
>
<path
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M7.667 12.333 3.001 7.667m0 0 4.666-4.666M3.001 7.667h9.332a4.666 4.666 0 1 1 0 9.332H10"
/>
</svg>
)
}
export default ArrowUTurnLeft

View File

@@ -10,27 +10,13 @@ const InfoIcon: React.FC<IconProps> = ({
<svg
width={size}
height={size}
viewBox="0 0 16 16"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...attributes}
>
<path
d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 10.6667V8"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 5.33331H8.0075"
d="M8.375 8.375L8.40917 8.35833C8.51602 8.30495 8.63594 8.2833 8.75472 8.29596C8.8735 8.30862 8.98616 8.35505 9.07937 8.42976C9.17258 8.50446 9.24242 8.60432 9.28064 8.71749C9.31885 8.83066 9.32384 8.95242 9.295 9.06833L8.705 11.4317C8.67595 11.5476 8.68078 11.6695 8.71891 11.7828C8.75704 11.8961 8.82687 11.9961 8.92011 12.071C9.01336 12.1458 9.12611 12.1923 9.245 12.205C9.36388 12.2177 9.4839 12.196 9.59083 12.1425L9.625 12.125M16.5 9C16.5 9.98491 16.306 10.9602 15.9291 11.8701C15.5522 12.7801 14.9997 13.6069 14.3033 14.3033C13.6069 14.9997 12.7801 15.5522 11.8701 15.9291C10.9602 16.306 9.98491 16.5 9 16.5C8.01509 16.5 7.03982 16.306 6.12987 15.9291C5.21993 15.5522 4.39314 14.9997 3.6967 14.3033C3.00026 13.6069 2.44781 12.7801 2.0709 11.8701C1.69399 10.9602 1.5 9.98491 1.5 9C1.5 7.01088 2.29018 5.10322 3.6967 3.6967C5.10322 2.29018 7.01088 1.5 9 1.5C10.9891 1.5 12.8968 2.29018 14.3033 3.6967C15.7098 5.10322 16.5 7.01088 16.5 9ZM9 5.875H9.00667V5.88167H9V5.875Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"

View File

@@ -0,0 +1,29 @@
import React from "react"
import IconProps from "../types/icon-type"
const SquaresPlus: React.FC<IconProps> = ({
size = "20",
color = "currentColor",
...attributes
}) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...attributes}
>
<path
d="M11.25 14.0625H14.0625M14.0625 14.0625H16.875M14.0625 14.0625V11.25M14.0625 14.0625V16.875M5 8.75H6.875C7.37228 8.75 7.84919 8.55246 8.20082 8.20082C8.55246 7.84919 8.75 7.37228 8.75 6.875V5C8.75 4.50272 8.55246 4.02581 8.20082 3.67417C7.84919 3.32254 7.37228 3.125 6.875 3.125H5C4.50272 3.125 4.02581 3.32254 3.67417 3.67417C3.32254 4.02581 3.125 4.50272 3.125 5V6.875C3.125 7.37228 3.32254 7.84919 3.67417 8.20082C4.02581 8.55246 4.50272 8.75 5 8.75V8.75ZM5 16.875H6.875C7.37228 16.875 7.84919 16.6775 8.20082 16.3258C8.55246 15.9742 8.75 15.4973 8.75 15V13.125C8.75 12.6277 8.55246 12.1508 8.20082 11.7992C7.84919 11.4475 7.37228 11.25 6.875 11.25H5C4.50272 11.25 4.02581 11.4475 3.67417 11.7992C3.32254 12.1508 3.125 12.6277 3.125 13.125V15C3.125 15.4973 3.32254 15.9742 3.67417 16.3258C4.02581 16.6775 4.50272 16.875 5 16.875ZM13.125 8.75H15C15.4973 8.75 15.9742 8.55246 16.3258 8.20082C16.6775 7.84919 16.875 7.37228 16.875 6.875V5C16.875 4.50272 16.6775 4.02581 16.3258 3.67417C15.9742 3.32254 15.4973 3.125 15 3.125H13.125C12.6277 3.125 12.1508 3.32254 11.7992 3.67417C11.4475 4.02581 11.25 4.50272 11.25 5V6.875C11.25 7.37228 11.4475 7.84919 11.7992 8.20082C12.1508 8.55246 12.6277 8.75 13.125 8.75V8.75Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default SquaresPlus

View File

@@ -51,7 +51,7 @@ const SigninInput = React.forwardRef(
return (
<div
className={clsx(
"rounded-rounded h-[40px] w-[280px] overflow-hidden border",
"rounded-rounded h-[40px] w-[300px] overflow-hidden border",
"bg-grey-5 inter-base-regular placeholder:text-grey-40",
"focus-within:shadow-input focus-within:border-violet-60",
"flex items-center",
@@ -83,6 +83,7 @@ const SigninInput = React.forwardRef(
type="button"
onClick={() => setShowPassword(!showPassword)}
className="text-grey-40 focus:text-violet-60 px-4 focus:outline-none"
tabIndex={-1}
>
{showPassword ? <EyeIcon size={16} /> : <EyeOffIcon size={16} />}
</button>

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} />}

View File

@@ -109,7 +109,7 @@ const UserTable: React.FC<UserTableProps> = ({
}
return `${window.location.origin}${
__BASE__ ? `${__BASE__}/` : "/"
process.env.ADMIN_PATH ? `${process.env.ADMIN_PATH}/` : "/"
}invite?token={invite_token}`
}, [store])