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:
committed by
GitHub
parent
26c78bbc03
commit
f1a05f4725
@@ -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">
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useExtensionBaseProps } from "../../../hooks/use-extension-base-props"
|
||||
|
||||
export const useSettingContainerProps = () => {
|
||||
const baseProps = useExtensionBaseProps()
|
||||
|
||||
return baseProps
|
||||
}
|
||||
@@ -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
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user