feat(dashboard,js-sdk,types): Update app layout, and add user sdk methods (#8182)
**What** - Updates app layout (sidebar and topbar) - Adds "System" option to theme toggle (we now default to system) - Adds sdk methods for user endpoints (RESOLVES CC-67)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+8
-7
@@ -1,18 +1,19 @@
|
|||||||
import { Tooltip } from "@medusajs/ui"
|
import { Tooltip } from "@medusajs/ui"
|
||||||
import { PropsWithChildren, ReactNode } from "react"
|
import { ComponentPropsWithoutRef, PropsWithChildren } from "react"
|
||||||
|
|
||||||
type ConditionalTooltipProps = PropsWithChildren<{
|
type ConditionalTooltipProps = PropsWithChildren<
|
||||||
content: ReactNode
|
ComponentPropsWithoutRef<typeof Tooltip> & {
|
||||||
showTooltip?: boolean
|
showTooltip?: boolean
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
|
|
||||||
export const ConditionalTooltip = ({
|
export const ConditionalTooltip = ({
|
||||||
children,
|
children,
|
||||||
content,
|
|
||||||
showTooltip = false,
|
showTooltip = false,
|
||||||
|
...props
|
||||||
}: ConditionalTooltipProps) => {
|
}: ConditionalTooltipProps) => {
|
||||||
if (showTooltip) {
|
if (showTooltip) {
|
||||||
return <Tooltip content={content}>{children}</Tooltip>
|
return <Tooltip {...props}>{children}</Tooltip>
|
||||||
}
|
}
|
||||||
|
|
||||||
return children
|
return children
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ const GridInput = forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className={clx(
|
className={clx(
|
||||||
"txt-compact-small text-ui-fg-base placeholder:text-ui-fg-muted disabled:text-ui-fg-disabled disabled:bg-ui-bg-base px-2 py-1.5 outline-none",
|
"txt-compact-small text-ui-fg-base placeholder:text-ui-fg-muted disabled:text-ui-fg-disabled disabled:bg-ui-bg-base bg-transparent px-2 py-1.5 outline-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
|
BuildingStorefront,
|
||||||
Buildings,
|
Buildings,
|
||||||
ChevronDownMini,
|
ChevronDownMini,
|
||||||
|
CogSixTooth,
|
||||||
CurrencyDollar,
|
CurrencyDollar,
|
||||||
|
EllipsisHorizontal,
|
||||||
|
MagnifyingGlass,
|
||||||
MinusMini,
|
MinusMini,
|
||||||
|
OpenRectArrowOut,
|
||||||
ReceiptPercent,
|
ReceiptPercent,
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
SquaresPlus,
|
SquaresPlus,
|
||||||
Tag,
|
Tag,
|
||||||
Users,
|
Users,
|
||||||
} from "@medusajs/icons"
|
} from "@medusajs/icons"
|
||||||
import { Avatar, Text } from "@medusajs/ui"
|
import { Avatar, DropdownMenu, Text, clx } from "@medusajs/ui"
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
@@ -20,7 +25,12 @@ import { Skeleton } from "../../common/skeleton"
|
|||||||
import { NavItem, NavItemProps } from "../../layout/nav-item"
|
import { NavItem, NavItemProps } from "../../layout/nav-item"
|
||||||
import { Shell } from "../../layout/shell"
|
import { Shell } from "../../layout/shell"
|
||||||
|
|
||||||
|
import { Link, useLocation, useNavigate } from "react-router-dom"
|
||||||
import routes from "virtual:medusa/routes/links"
|
import routes from "virtual:medusa/routes/links"
|
||||||
|
import { useLogout } from "../../../hooks/api"
|
||||||
|
import { queryClient } from "../../../lib/query-client"
|
||||||
|
import { useSearch } from "../../../providers/search-provider"
|
||||||
|
import { UserMenu } from "../user-menu"
|
||||||
|
|
||||||
export const MainLayout = () => {
|
export const MainLayout = () => {
|
||||||
return (
|
return (
|
||||||
@@ -40,41 +50,129 @@ const MainSidebar = () => {
|
|||||||
<Divider variant="dashed" />
|
<Divider variant="dashed" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CoreRouteSection />
|
<div className="flex flex-1 flex-col justify-between">
|
||||||
<ExtensionRouteSection />
|
<div className="flex flex-1 flex-col">
|
||||||
|
<CoreRouteSection />
|
||||||
|
<ExtensionRouteSection />
|
||||||
|
</div>
|
||||||
|
<UtilitySection />
|
||||||
|
</div>
|
||||||
|
<div className="bg-ui-bg-subtle sticky bottom-0">
|
||||||
|
<UserSection />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Logout = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { mutateAsync: logoutMutation } = useLogout()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logoutMutation(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
/**
|
||||||
|
* When the user logs out, we want to clear the query cache
|
||||||
|
*/
|
||||||
|
queryClient.clear()
|
||||||
|
navigate("/login")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Item onClick={handleLogout}>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<OpenRectArrowOut className="text-ui-fg-subtle" />
|
||||||
|
<span>{t("app.menus.actions.logout")}</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { store, isError, error } = useStore()
|
const { t } = useTranslation()
|
||||||
|
const { store, isPending, isError, error } = useStore()
|
||||||
|
|
||||||
const name = store?.name
|
const name = store?.name
|
||||||
const fallback = store?.name?.slice(0, 1).toUpperCase()
|
const fallback = store?.name?.slice(0, 1).toUpperCase()
|
||||||
|
|
||||||
|
const isLoaded = !isPending && !!store && !!name && !!fallback
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-3 py-2">
|
<div className="w-full p-3">
|
||||||
<div className="flex items-center p-1 md:pr-2">
|
<DropdownMenu>
|
||||||
<div className="flex items-center gap-x-3">
|
<DropdownMenu.Trigger
|
||||||
|
disabled={!isLoaded}
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-subtle transition-fg grid w-full grid-cols-[24px_1fr_15px] items-center gap-x-3 rounded-md p-0.5 pr-2 outline-none",
|
||||||
|
"hover:bg-ui-bg-subtle-hover",
|
||||||
|
"data-[state=open]:bg-ui-bg-subtle-hover",
|
||||||
|
"focus-visible:shadow-borders-focus"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{fallback ? (
|
{fallback ? (
|
||||||
<Avatar variant="squared" fallback={fallback} />
|
<Avatar variant="squared" size="xsmall" fallback={fallback} />
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="h-8 w-8 rounded-md" />
|
<Skeleton className="h-6 w-6 rounded-md" />
|
||||||
)}
|
)}
|
||||||
{name ? (
|
<div className="block overflow-hidden">
|
||||||
<Text size="small" weight="plus" leading="compact">
|
{name ? (
|
||||||
{store.name}
|
<Text
|
||||||
</Text>
|
size="small"
|
||||||
) : (
|
weight="plus"
|
||||||
<Skeleton className="h-[9px] w-[120px]" />
|
leading="compact"
|
||||||
)}
|
className="truncate"
|
||||||
</div>
|
>
|
||||||
</div>
|
{store.name}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="h-[9px] w-[120px]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<EllipsisHorizontal className="text-ui-fg-muted" />
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
{isLoaded && (
|
||||||
|
<DropdownMenu.Content className="w-[var(--radix-dropdown-menu-trigger-width)] min-w-0">
|
||||||
|
<div className="flex items-center gap-x-3 px-2 py-1">
|
||||||
|
<Avatar variant="squared" size="small" fallback={fallback} />
|
||||||
|
<div className="flex flex-col overflow-hidden">
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
weight="plus"
|
||||||
|
leading="compact"
|
||||||
|
className="truncate"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="xsmall"
|
||||||
|
leading="compact"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
>
|
||||||
|
{t("app.nav.main.store")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item className="gap-x-2" asChild>
|
||||||
|
<Link to="/settings/store">
|
||||||
|
<BuildingStorefront className="text-ui-fg-subtle" />
|
||||||
|
{t("app.nav.main.storeSettings")}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<Logout />
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -156,11 +254,40 @@ const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Searchbar = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { toggleSearch } = useSearch()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-3">
|
||||||
|
<button
|
||||||
|
onClick={toggleSearch}
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-subtle text-ui-fg-subtle flex w-full items-center gap-x-2.5 rounded-md px-2 py-1 outline-none",
|
||||||
|
"hover:bg-ui-bg-subtle-hover",
|
||||||
|
"focus-visible:shadow-borders-focus"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MagnifyingGlass />
|
||||||
|
<div className="flex-1 text-left">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("app.search.label")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
||||||
|
⌘K
|
||||||
|
</Text>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CoreRouteSection = () => {
|
const CoreRouteSection = () => {
|
||||||
const coreRoutes = useCoreRoutes()
|
const coreRoutes = useCoreRoutes()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex flex-col gap-y-1 py-3">
|
<nav className="flex flex-col gap-y-1 py-3">
|
||||||
|
<Searchbar />
|
||||||
{coreRoutes.map((route) => {
|
{coreRoutes.map((route) => {
|
||||||
return <NavItem key={route.to} {...route} />
|
return <NavItem key={route.to} {...route} />
|
||||||
})}
|
})}
|
||||||
@@ -192,7 +319,7 @@ const ExtensionRouteSection = () => {
|
|||||||
<Collapsible.Trigger asChild className="group/trigger">
|
<Collapsible.Trigger asChild className="group/trigger">
|
||||||
<button className="text-ui-fg-subtle flex w-full items-center justify-between px-2">
|
<button className="text-ui-fg-subtle flex w-full items-center justify-between px-2">
|
||||||
<Text size="xsmall" weight="plus" leading="compact">
|
<Text size="xsmall" weight="plus" leading="compact">
|
||||||
{t("nav.extensions")}
|
{t("app.nav.common.extensions")}
|
||||||
</Text>
|
</Text>
|
||||||
<div className="text-ui-fg-muted">
|
<div className="text-ui-fg-muted">
|
||||||
<ChevronDownMini className="group-data-[state=open]/trigger:hidden" />
|
<ChevronDownMini className="group-data-[state=open]/trigger:hidden" />
|
||||||
@@ -202,7 +329,7 @@ const ExtensionRouteSection = () => {
|
|||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
</div>
|
</div>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
<div className="flex flex-col gap-y-1 py-1 pb-4">
|
<nav className="flex flex-col gap-y-0.5 py-1 pb-4">
|
||||||
{extensionLinks.map((link) => {
|
{extensionLinks.map((link) => {
|
||||||
return (
|
return (
|
||||||
<NavItem
|
<NavItem
|
||||||
@@ -214,10 +341,37 @@ const ExtensionRouteSection = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</nav>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UtilitySection = () => {
|
||||||
|
const location = useLocation()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-0.5 py-3">
|
||||||
|
<NavItem
|
||||||
|
label={t("app.nav.settings.header")}
|
||||||
|
to="/settings"
|
||||||
|
from={location.pathname}
|
||||||
|
icon={<CogSixTooth />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserSection = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="px-3">
|
||||||
|
<Divider variant="dashed" />
|
||||||
|
</div>
|
||||||
|
<UserMenu />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { Text, clx } from "@medusajs/ui"
|
import { Kbd, Text, clx } from "@medusajs/ui"
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||||
import { ReactNode, useEffect, useState } from "react"
|
import {
|
||||||
import { Link, useLocation } from "react-router-dom"
|
PropsWithChildren,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { NavLink, useLocation } from "react-router-dom"
|
||||||
|
import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks"
|
||||||
|
import { ConditionalTooltip } from "../../common/conditional-tooltip"
|
||||||
|
|
||||||
type ItemType = "core" | "extension"
|
type ItemType = "core" | "extension" | "setting"
|
||||||
|
|
||||||
type NestedItemProps = {
|
type NestedItemProps = {
|
||||||
label: string
|
label: string
|
||||||
@@ -19,6 +28,60 @@ export type NavItemProps = {
|
|||||||
from?: string
|
from?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BASE_NAV_LINK_CLASSES =
|
||||||
|
"text-ui-fg-subtle transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md py-1 pl-0.5 pr-2 outline-none [&>svg]:text-ui-fg-subtle focus-visible:shadow-borders-focus"
|
||||||
|
const ACTIVE_NAV_LINK_CLASSES =
|
||||||
|
"bg-ui-bg-base shadow-elevation-card-rest text-ui-fg-base hover:bg-ui-bg-base"
|
||||||
|
const NESTED_NAV_LINK_CLASSES = "pl-[34px] pr-2 w-full text-ui-fg-muted"
|
||||||
|
const SETTING_NAV_LINK_CLASSES = "pl-2"
|
||||||
|
|
||||||
|
const getIsOpen = (
|
||||||
|
to: string,
|
||||||
|
items: NestedItemProps[] | undefined,
|
||||||
|
pathname: string
|
||||||
|
) => {
|
||||||
|
return [to, ...(items?.map((i) => i.to) ?? [])].some((p) =>
|
||||||
|
pathname.startsWith(p)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavItemTooltip = ({
|
||||||
|
to,
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<{ to: string }>) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const globalShortcuts = useGlobalShortcuts()
|
||||||
|
const shortcut = globalShortcuts.find((s) => s.to === to)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConditionalTooltip
|
||||||
|
showTooltip={!!shortcut}
|
||||||
|
maxWidth={9999} // Don't limit the width of the tooltip
|
||||||
|
content={
|
||||||
|
<div className="txt-compact-xsmall flex h-5 items-center justify-between gap-x-2 whitespace-nowrap">
|
||||||
|
<span>{shortcut?.label}</span>
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
{shortcut?.keys.Mac?.map((key, index) => (
|
||||||
|
<div className="flex items-center gap-x-1" key={index}>
|
||||||
|
<Kbd key={key}>{key}</Kbd>
|
||||||
|
{index < (shortcut.keys.Mac?.length || 0) - 1 && (
|
||||||
|
<span className="text-ui-fg-muted txt-compact-xsmall">
|
||||||
|
{t("app.keyboardShortcuts.then")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
side="right"
|
||||||
|
delayDuration={1500}
|
||||||
|
>
|
||||||
|
<div className="w-full">{children}</div>
|
||||||
|
</ConditionalTooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const NavItem = ({
|
export const NavItem = ({
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
@@ -27,111 +90,125 @@ export const NavItem = ({
|
|||||||
type = "core",
|
type = "core",
|
||||||
from,
|
from,
|
||||||
}: NavItemProps) => {
|
}: NavItemProps) => {
|
||||||
const location = useLocation()
|
const { pathname } = useLocation()
|
||||||
|
const [open, setOpen] = useState(getIsOpen(to, items, pathname))
|
||||||
const [open, setOpen] = useState(
|
|
||||||
[to, ...(items?.map((i) => i.to) ?? [])].some((p) =>
|
|
||||||
location.pathname.startsWith(p)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpen(
|
setOpen(getIsOpen(to, items, pathname))
|
||||||
[to, ...(items?.map((i) => i.to) ?? [])].some((p) =>
|
}, [pathname, to, items])
|
||||||
location.pathname.startsWith(p)
|
|
||||||
)
|
const navLinkClassNames = useCallback(
|
||||||
)
|
({
|
||||||
}, [location.pathname, to, items])
|
isActive,
|
||||||
|
isNested = false,
|
||||||
|
isSetting = false,
|
||||||
|
}: {
|
||||||
|
isActive: boolean
|
||||||
|
isNested?: boolean
|
||||||
|
isSetting?: boolean
|
||||||
|
}) =>
|
||||||
|
clx(BASE_NAV_LINK_CLASSES, {
|
||||||
|
[NESTED_NAV_LINK_CLASSES]: isNested,
|
||||||
|
[ACTIVE_NAV_LINK_CLASSES]: isActive,
|
||||||
|
[SETTING_NAV_LINK_CLASSES]: isSetting,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const isSetting = type === "setting"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<Link
|
<NavItemTooltip to={to}>
|
||||||
to={to}
|
<NavLink
|
||||||
state={
|
to={to}
|
||||||
from
|
state={
|
||||||
? {
|
from
|
||||||
from,
|
? {
|
||||||
}
|
from,
|
||||||
: undefined
|
}
|
||||||
}
|
: undefined
|
||||||
className={clx(
|
|
||||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md px-2 py-1 outline-none",
|
|
||||||
{
|
|
||||||
"bg-ui-bg-base hover:bg-ui-bg-base-hover shadow-elevation-card-rest":
|
|
||||||
location.pathname === to ||
|
|
||||||
location.pathname.startsWith(to + "/"), // TODO: utilise `NavLink` and `end` prop instead of this manual check
|
|
||||||
"max-md:hidden": items && items.length > 0,
|
|
||||||
}
|
}
|
||||||
)}
|
className={(props) =>
|
||||||
>
|
clx(navLinkClassNames({ ...props, isSetting }), {
|
||||||
<Icon icon={icon} type={type} />
|
"max-lg:hidden": !!items?.length,
|
||||||
<Text size="small" weight="plus" leading="compact">
|
})
|
||||||
{label}
|
}
|
||||||
</Text>
|
>
|
||||||
</Link>
|
{type !== "setting" && (
|
||||||
|
<div className="flex size-6 items-center justify-center">
|
||||||
|
<Icon icon={icon} type={type} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</NavLink>
|
||||||
|
</NavItemTooltip>
|
||||||
{items && items.length > 0 && (
|
{items && items.length > 0 && (
|
||||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||||
<Collapsible.Trigger
|
<Collapsible.Trigger
|
||||||
className={clx(
|
className={clx(
|
||||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex w-full items-center gap-x-2 rounded-md px-2 py-2.5 outline-none md:hidden md:py-1.5"
|
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex w-full items-center gap-x-2 rounded-md py-1 pl-0.5 pr-2 outline-none lg:hidden",
|
||||||
|
{ "pl-2": isSetting }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon icon={icon} type={type} />
|
<div className="flex size-6 items-center justify-center">
|
||||||
|
<Icon icon={icon} type={type} />
|
||||||
|
</div>
|
||||||
<Text size="small" weight="plus" leading="compact">
|
<Text size="small" weight="plus" leading="compact">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
<Collapsible.Content className="flex flex-col pt-1">
|
<Collapsible.Content>
|
||||||
<div className="flex w-full items-center gap-x-1 pl-2 md:hidden">
|
<div className="flex flex-col gap-y-0.5 pb-2 pt-0.5">
|
||||||
<div
|
<ul className="flex flex-col gap-y-0.5">
|
||||||
role="presentation"
|
<li className="flex w-full items-center gap-x-1 lg:hidden">
|
||||||
className="flex h-full w-5 items-center justify-center"
|
<NavItemTooltip to={to}>
|
||||||
>
|
<NavLink
|
||||||
<div className="bg-ui-border-strong h-full w-px" />
|
to={to}
|
||||||
</div>
|
className={(props) =>
|
||||||
<Link
|
clx(
|
||||||
to={to}
|
navLinkClassNames({
|
||||||
className={clx(
|
...props,
|
||||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover mb-2 mt-1 flex flex-1 items-center gap-x-2 rounded-md px-2 py-1 outline-none",
|
isNested: true,
|
||||||
{
|
isSetting,
|
||||||
"bg-ui-bg-base hover:bg-ui-bg-base text-ui-fg-base shadow-elevation-card-rest":
|
})
|
||||||
location.pathname.startsWith(to),
|
)
|
||||||
}
|
}
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<ul>
|
|
||||||
{items.map((item) => {
|
|
||||||
return (
|
|
||||||
<li key={item.to} className="flex h-[32px] items-center pl-1">
|
|
||||||
<div
|
|
||||||
role="presentation"
|
|
||||||
className="flex h-full w-5 items-center justify-center"
|
|
||||||
>
|
|
||||||
<div className="bg-ui-border-strong h-full w-px" />
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
to={item.to}
|
|
||||||
className={clx(
|
|
||||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex flex-1 items-center gap-x-2 rounded-md px-2 py-1 outline-none first-of-type:mt-1 last-of-type:mb-2",
|
|
||||||
{
|
|
||||||
"bg-ui-bg-base text-ui-fg-base hover:bg-ui-bg-base shadow-elevation-card-rest":
|
|
||||||
location.pathname.startsWith(item.to),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Text size="small" weight="plus" leading="compact">
|
<Text size="small" weight="plus" leading="compact">
|
||||||
{item.label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</Link>
|
</NavLink>
|
||||||
</li>
|
</NavItemTooltip>
|
||||||
)
|
</li>
|
||||||
})}
|
{items.map((item) => {
|
||||||
</ul>
|
return (
|
||||||
|
<li key={item.to} className="flex h-7 items-center">
|
||||||
|
<NavItemTooltip to={item.to}>
|
||||||
|
<NavLink
|
||||||
|
to={item.to}
|
||||||
|
className={(props) =>
|
||||||
|
clx(
|
||||||
|
navLinkClassNames({
|
||||||
|
...props,
|
||||||
|
isNested: true,
|
||||||
|
isSetting,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</NavLink>
|
||||||
|
</NavItemTooltip>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+1
-1
@@ -128,7 +128,7 @@ const Sidebar = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
"flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[400px]",
|
"flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[440px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
+128
-102
@@ -1,5 +1,5 @@
|
|||||||
import { ArrowUturnLeft, MinusMini } from "@medusajs/icons"
|
import { ArrowUturnLeft, MinusMini } from "@medusajs/icons"
|
||||||
import { IconButton, Text } from "@medusajs/ui"
|
import { IconButton, Text, clx } from "@medusajs/ui"
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||||
import { Fragment, useEffect, useMemo, useState } from "react"
|
import { Fragment, useEffect, useMemo, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@@ -11,6 +11,7 @@ import { NavItem, NavItemProps } from "../nav-item"
|
|||||||
import { Shell } from "../shell"
|
import { Shell } from "../shell"
|
||||||
|
|
||||||
import routes from "virtual:medusa/routes/links"
|
import routes from "virtual:medusa/routes/links"
|
||||||
|
import { UserMenu } from "../user-menu"
|
||||||
|
|
||||||
export const SettingsLayout = () => {
|
export const SettingsLayout = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,10 +26,6 @@ const useSettingRoutes = (): NavItemProps[] => {
|
|||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
|
||||||
label: t("profile.domain"),
|
|
||||||
to: "/settings/profile",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: t("store.domain"),
|
label: t("store.domain"),
|
||||||
to: "/settings/store",
|
to: "/settings/store",
|
||||||
@@ -84,6 +81,20 @@ const useDeveloperRoutes = (): NavItemProps[] => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useMyAccountRoutes = (): NavItemProps[] => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: t("profile.domain"),
|
||||||
|
to: "/settings/profile",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const useExtensionRoutes = (): NavItemProps[] => {
|
const useExtensionRoutes = (): NavItemProps[] => {
|
||||||
const links = routes.links
|
const links = routes.links
|
||||||
|
|
||||||
@@ -115,12 +126,64 @@ const SettingsSidebar = () => {
|
|||||||
const routes = useSettingRoutes()
|
const routes = useSettingRoutes()
|
||||||
const developerRoutes = useDeveloperRoutes()
|
const developerRoutes = useDeveloperRoutes()
|
||||||
const extensionRoutes = useExtensionRoutes()
|
const extensionRoutes = useExtensionRoutes()
|
||||||
|
const myAccountRoutes = useMyAccountRoutes()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const location = useLocation()
|
return (
|
||||||
|
<aside className="flex flex-1 flex-col justify-between overflow-y-auto">
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<div className="bg-ui-bg-subtle sticky top-0 z-[1]">
|
||||||
|
<Header />
|
||||||
|
<div className="flex items-center justify-center px-3">
|
||||||
|
<Divider variant="dashed" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||||
|
<CollapsibleSection
|
||||||
|
label={t("app.nav.settings.general")}
|
||||||
|
items={routes}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-center px-3">
|
||||||
|
<Divider variant="dashed" />
|
||||||
|
</div>
|
||||||
|
<CollapsibleSection
|
||||||
|
label={t("app.nav.settings.developer")}
|
||||||
|
items={developerRoutes}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-center px-3">
|
||||||
|
<Divider variant="dashed" />
|
||||||
|
</div>
|
||||||
|
<CollapsibleSection
|
||||||
|
label={t("app.nav.settings.myAccount")}
|
||||||
|
items={myAccountRoutes}
|
||||||
|
/>
|
||||||
|
{extensionRoutes.length > 0 && (
|
||||||
|
<Fragment>
|
||||||
|
<div className="flex items-center justify-center px-3">
|
||||||
|
<Divider variant="dashed" />
|
||||||
|
</div>
|
||||||
|
<CollapsibleSection
|
||||||
|
label={t("app.nav.common.extensions")}
|
||||||
|
items={extensionRoutes}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="bg-ui-bg-subtle sticky bottom-0">
|
||||||
|
<UserSection />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
const [from, setFrom] = useState("/orders")
|
const [from, setFrom] = useState("/orders")
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.state?.from) {
|
if (location.state?.from) {
|
||||||
setFrom(getSafeFromValue(location.state.from))
|
setFrom(getSafeFromValue(location.state.from))
|
||||||
@@ -128,107 +191,70 @@ const SettingsSidebar = () => {
|
|||||||
}, [location])
|
}, [location])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex flex-1 flex-col justify-between overflow-y-auto">
|
<div className="bg-ui-bg-subtle p-3">
|
||||||
<div className="p-3">
|
<Link
|
||||||
<div className="flex items-center gap-x-3 px-2 py-1.5">
|
to={from}
|
||||||
<IconButton size="2xsmall" variant="transparent" asChild>
|
replace
|
||||||
<Link
|
className={clx(
|
||||||
to={from}
|
"bg-ui-bg-subtle transition-fg flex items-center rounded-md outline-none",
|
||||||
replace
|
"hover:bg-ui-bg-subtle-hover",
|
||||||
className="flex items-center justify-center"
|
"focus-visible:shadow-borders-focus"
|
||||||
>
|
)}
|
||||||
<ArrowUturnLeft />
|
>
|
||||||
</Link>
|
<div className="flex items-center gap-x-2.5 px-2 py-1">
|
||||||
</IconButton>
|
<div className="flex items-center justify-center">
|
||||||
|
<ArrowUturnLeft className="text-ui-fg-subtle" />
|
||||||
|
</div>
|
||||||
<Text leading="compact" weight="plus" size="small">
|
<Text leading="compact" weight="plus" size="small">
|
||||||
{t("nav.settings")}
|
{t("app.nav.settings.header")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CollapsibleSection = ({
|
||||||
|
label,
|
||||||
|
items,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
items: NavItemProps[]
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Collapsible.Root defaultOpen className="py-3">
|
||||||
|
<div className="px-3">
|
||||||
|
<div className="text-ui-fg-muted flex h-7 items-center justify-between px-2">
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<IconButton size="2xsmall" variant="transparent">
|
||||||
|
<MinusMini className="text-ui-fg-muted" />
|
||||||
|
</IconButton>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center px-3">
|
<Collapsible.Content>
|
||||||
|
<div className="pt-0.5">
|
||||||
|
<nav className="flex flex-col gap-y-0.5">
|
||||||
|
{items.map((setting) => (
|
||||||
|
<NavItem key={setting.to} type="setting" {...setting} />
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserSection = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="px-3">
|
||||||
<Divider variant="dashed" />
|
<Divider variant="dashed" />
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
|
||||||
<Collapsible.Root defaultOpen className="py-3">
|
|
||||||
<div className="px-3">
|
|
||||||
<div className="text-ui-fg-muted flex h-7 items-center justify-between px-2">
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{t("nav.general")}
|
|
||||||
</Text>
|
|
||||||
<Collapsible.Trigger asChild>
|
|
||||||
<IconButton size="2xsmall" variant="transparent">
|
|
||||||
<MinusMini className="text-ui-fg-muted" />
|
|
||||||
</IconButton>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Collapsible.Content>
|
|
||||||
<div className="pt-0.5">
|
|
||||||
<nav className="flex flex-col gap-y-1">
|
|
||||||
{routes.map((setting) => (
|
|
||||||
<NavItem key={setting.to} {...setting} />
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible.Root>
|
|
||||||
<div className="flex items-center justify-center px-3">
|
|
||||||
<Divider variant="dashed" />
|
|
||||||
</div>
|
|
||||||
<Collapsible.Root defaultOpen className="py-3">
|
|
||||||
<div className="px-3">
|
|
||||||
<div className="text-ui-fg-muted flex h-7 items-center justify-between px-2">
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{t("nav.developer")}
|
|
||||||
</Text>
|
|
||||||
<Collapsible.Trigger asChild>
|
|
||||||
<IconButton size="2xsmall" variant="transparent">
|
|
||||||
<MinusMini className="text-ui-fg-muted" />
|
|
||||||
</IconButton>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Collapsible.Content>
|
|
||||||
<div className="pt-0.5">
|
|
||||||
<nav className="flex flex-col gap-y-1">
|
|
||||||
{developerRoutes.map((setting) => (
|
|
||||||
<NavItem key={setting.to} {...setting} />
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible.Root>
|
|
||||||
{extensionRoutes.length > 0 && (
|
|
||||||
<Fragment>
|
|
||||||
<div className="flex items-center justify-center px-3">
|
|
||||||
<Divider variant="dashed" />
|
|
||||||
</div>
|
|
||||||
<Collapsible.Root defaultOpen className="py-3">
|
|
||||||
<div className="px-3">
|
|
||||||
<div className="text-ui-fg-muted flex h-7 items-center justify-between px-2">
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{t("nav.extensions")}
|
|
||||||
</Text>
|
|
||||||
<Collapsible.Trigger asChild>
|
|
||||||
<IconButton size="2xsmall" variant="transparent">
|
|
||||||
<MinusMini className="text-ui-fg-muted" />
|
|
||||||
</IconButton>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Collapsible.Content>
|
|
||||||
<div className="pt-0.5">
|
|
||||||
<nav className="flex flex-col gap-y-1">
|
|
||||||
{extensionRoutes.map((setting) => (
|
|
||||||
<NavItem key={setting.to} {...setting} />
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible.Root>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
<UserMenu />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,19 @@
|
|||||||
import * as Dialog from "@radix-ui/react-dialog"
|
import * as Dialog from "@radix-ui/react-dialog"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ArrowRightOnRectangle,
|
|
||||||
BellAlert,
|
BellAlert,
|
||||||
BookOpen,
|
|
||||||
Calendar,
|
|
||||||
CircleHalfSolid,
|
|
||||||
CogSixTooth,
|
|
||||||
Keyboard,
|
|
||||||
MagnifyingGlass,
|
|
||||||
SidebarLeft,
|
SidebarLeft,
|
||||||
TriangleRightMini,
|
TriangleRightMini,
|
||||||
User as UserIcon,
|
XMark,
|
||||||
} from "@medusajs/icons"
|
} from "@medusajs/icons"
|
||||||
import {
|
import { IconButton, clx } from "@medusajs/ui"
|
||||||
Avatar,
|
import { PropsWithChildren } from "react"
|
||||||
Button,
|
import { Link, Outlet, UIMatch, useMatches } from "react-router-dom"
|
||||||
DropdownMenu,
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
Kbd,
|
|
||||||
Text,
|
|
||||||
clx,
|
|
||||||
} from "@medusajs/ui"
|
|
||||||
import { PropsWithChildren, useState } from "react"
|
|
||||||
import {
|
|
||||||
Link,
|
|
||||||
Outlet,
|
|
||||||
UIMatch,
|
|
||||||
useLocation,
|
|
||||||
useMatches,
|
|
||||||
useNavigate,
|
|
||||||
} from "react-router-dom"
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useLogout } from "../../../hooks/api/auth"
|
|
||||||
import { useMe } from "../../../hooks/api/users"
|
|
||||||
import { queryClient } from "../../../lib/query-client"
|
|
||||||
import { KeybindProvider } from "../../../providers/keybind-provider"
|
import { KeybindProvider } from "../../../providers/keybind-provider"
|
||||||
import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks"
|
import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks"
|
||||||
import { useSearch } from "../../../providers/search-provider"
|
|
||||||
import { useSidebar } from "../../../providers/sidebar-provider"
|
import { useSidebar } from "../../../providers/sidebar-provider"
|
||||||
import { useTheme } from "../../../providers/theme-provider"
|
|
||||||
import { Skeleton } from "../../common/skeleton"
|
|
||||||
|
|
||||||
export const Shell = ({ children }: PropsWithChildren) => {
|
export const Shell = ({ children }: PropsWithChildren) => {
|
||||||
const globalShortcuts = useGlobalShortcuts()
|
const globalShortcuts = useGlobalShortcuts()
|
||||||
@@ -136,239 +107,6 @@ const Breadcrumbs = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserBadge = () => {
|
|
||||||
const { user, isLoading, isError, error } = useMe()
|
|
||||||
|
|
||||||
const name = [user?.first_name, user?.last_name].filter(Boolean).join(" ")
|
|
||||||
const displayName = name || user?.email
|
|
||||||
|
|
||||||
const fallback = displayName ? displayName[0].toUpperCase() : null
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<button className="shadow-borders-base flex max-w-[192px] select-none items-center gap-x-2 overflow-hidden text-ellipsis whitespace-nowrap rounded-full py-1 pl-1 pr-2.5">
|
|
||||||
<Skeleton className="h-5 w-5 rounded-full" />
|
|
||||||
<Skeleton className="h-[9px] w-[70px]" />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu.Trigger asChild>
|
|
||||||
<button
|
|
||||||
disabled={!user}
|
|
||||||
className={clx(
|
|
||||||
"shadow-borders-base flex max-w-[192px] select-none items-center gap-x-2 overflow-hidden text-ellipsis whitespace-nowrap rounded-full py-1 pl-1 pr-2.5 outline-none"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{fallback ? (
|
|
||||||
<Avatar size="xsmall" fallback={fallback} />
|
|
||||||
) : (
|
|
||||||
<Skeleton className="h-5 w-5 rounded-full" />
|
|
||||||
)}
|
|
||||||
{displayName ? (
|
|
||||||
<Text
|
|
||||||
size="xsmall"
|
|
||||||
weight="plus"
|
|
||||||
leading="compact"
|
|
||||||
className="truncate"
|
|
||||||
>
|
|
||||||
{displayName}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Skeleton className="h-[9px] w-[70px]" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThemeToggle = () => {
|
|
||||||
const { theme, setTheme } = useTheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu.SubMenu>
|
|
||||||
<DropdownMenu.SubMenuTrigger className="rounded-md">
|
|
||||||
<CircleHalfSolid className="text-ui-fg-subtle mr-2" />
|
|
||||||
Theme
|
|
||||||
</DropdownMenu.SubMenuTrigger>
|
|
||||||
<DropdownMenu.SubMenuContent>
|
|
||||||
<DropdownMenu.RadioGroup value={theme}>
|
|
||||||
<DropdownMenu.RadioItem
|
|
||||||
value="light"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setTheme("light")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Light
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
<DropdownMenu.RadioItem
|
|
||||||
value="dark"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setTheme("dark")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Dark
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
</DropdownMenu.RadioGroup>
|
|
||||||
</DropdownMenu.SubMenuContent>
|
|
||||||
</DropdownMenu.SubMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Logout = () => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const { mutateAsync: logoutMutation } = useLogout()
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await logoutMutation(undefined, {
|
|
||||||
onSuccess: () => {
|
|
||||||
/**
|
|
||||||
* When the user logs out, we want to clear the query cache
|
|
||||||
*/
|
|
||||||
queryClient.clear()
|
|
||||||
navigate("/login")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu.Item onClick={handleLogout}>
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<ArrowRightOnRectangle className="text-ui-fg-subtle" />
|
|
||||||
<span>Logout</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Profile = () => {
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to="/settings/profile" state={{ from: location.pathname }}>
|
|
||||||
<DropdownMenu.Item>
|
|
||||||
<UserIcon className="text-ui-fg-subtle mr-2" />
|
|
||||||
Profile
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const GlobalKeybindsModal = (props: {
|
|
||||||
open: boolean
|
|
||||||
onOpenChange: (open: boolean) => void
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const globalShortcuts = useGlobalShortcuts()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog.Root {...props}>
|
|
||||||
<Dialog.Portal>
|
|
||||||
<Dialog.Overlay className="bg-ui-bg-overlay fixed inset-0" />
|
|
||||||
<Dialog.Content className="bg-ui-bg-subtle shadow-elevation-modal fixed left-[50%] top-[50%] flex h-full max-h-[612px] w-full max-w-[560px] translate-x-[-50%] translate-y-[-50%] flex-col divide-y overflow-hidden rounded-lg">
|
|
||||||
<div className="px-6 py-4">
|
|
||||||
<Dialog.Title asChild>
|
|
||||||
<Heading>Keyboard Shortcuts</Heading>
|
|
||||||
</Dialog.Title>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col divide-y overflow-y-auto">
|
|
||||||
{globalShortcuts.map((shortcut, index) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="text-ui-fg-subtle flex items-center justify-between px-6 py-3"
|
|
||||||
>
|
|
||||||
<Text size="small">{shortcut.label}</Text>
|
|
||||||
<div className="flex items-center gap-x-1.5">
|
|
||||||
{shortcut.keys.Mac?.map((key, index) => {
|
|
||||||
return <Kbd key={index}>{key}</Kbd>
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end border-b px-6 py-4">
|
|
||||||
<Dialog.Close asChild>
|
|
||||||
<Button size="small">{t("actions.close")}</Button>
|
|
||||||
</Dialog.Close>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const LoggedInUser = () => {
|
|
||||||
const [openMenu, setOpenMenu] = useState(false)
|
|
||||||
const [openModal, setOpenModal] = useState(false)
|
|
||||||
|
|
||||||
const toggleModal = () => {
|
|
||||||
setOpenMenu(false)
|
|
||||||
setOpenModal(!openModal)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DropdownMenu open={openMenu} onOpenChange={setOpenMenu}>
|
|
||||||
<UserBadge />
|
|
||||||
<DropdownMenu.Content align="center">
|
|
||||||
<Profile />
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item asChild>
|
|
||||||
<Link to="https://docs.medusajs.com/v2" target="_blank">
|
|
||||||
<BookOpen className="text-ui-fg-subtle mr-2" />
|
|
||||||
Documentation
|
|
||||||
</Link>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item asChild>
|
|
||||||
<Link to="https://medusajs.com/changelog/" target="_blank">
|
|
||||||
<Calendar className="text-ui-fg-subtle mr-2" />
|
|
||||||
Changelog
|
|
||||||
</Link>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item onClick={toggleModal}>
|
|
||||||
<Keyboard className="text-ui-fg-subtle mr-2" />
|
|
||||||
Keyboard shortcuts
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<ThemeToggle />
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<Logout />
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu>
|
|
||||||
<GlobalKeybindsModal open={openModal} onOpenChange={setOpenModal} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsLink = () => {
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
to="/settings"
|
|
||||||
className="flex items-center justify-center"
|
|
||||||
state={{ from: location.pathname }}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
variant="transparent"
|
|
||||||
className="text-ui-fg-muted transition-fg hover:text-ui-fg-subtle"
|
|
||||||
>
|
|
||||||
<CogSixTooth />
|
|
||||||
</IconButton>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ToggleNotifications = () => {
|
const ToggleNotifications = () => {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -381,26 +119,6 @@ const ToggleNotifications = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Searchbar = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { toggleSearch } = useSearch()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={toggleSearch}
|
|
||||||
className="shadow-borders-base bg-ui-bg-subtle hover:bg-ui-bg-subtle-hover transition-fg focus-visible:shadow-borders-focus text-ui-fg-muted flex w-full max-w-[280px] select-none items-center gap-x-2 rounded-full py-1.5 pl-2 pr-1.5 outline-none"
|
|
||||||
>
|
|
||||||
<MagnifyingGlass />
|
|
||||||
<div className="flex-1 text-left">
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{t("app.search.placeholder")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Kbd className="rounded-full">⌘K</Kbd>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ToggleSidebar = () => {
|
const ToggleSidebar = () => {
|
||||||
const { toggle } = useSidebar()
|
const { toggle } = useSidebar()
|
||||||
|
|
||||||
@@ -410,6 +128,7 @@ const ToggleSidebar = () => {
|
|||||||
className="hidden lg:flex"
|
className="hidden lg:flex"
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
onClick={() => toggle("desktop")}
|
onClick={() => toggle("desktop")}
|
||||||
|
size="small"
|
||||||
>
|
>
|
||||||
<SidebarLeft className="text-ui-fg-muted" />
|
<SidebarLeft className="text-ui-fg-muted" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -417,6 +136,7 @@ const ToggleSidebar = () => {
|
|||||||
className="hidden max-lg:flex"
|
className="hidden max-lg:flex"
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
onClick={() => toggle("mobile")}
|
onClick={() => toggle("mobile")}
|
||||||
|
size="small"
|
||||||
>
|
>
|
||||||
<SidebarLeft className="text-ui-fg-muted" />
|
<SidebarLeft className="text-ui-fg-muted" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -426,20 +146,13 @@ const ToggleSidebar = () => {
|
|||||||
|
|
||||||
const Topbar = () => {
|
const Topbar = () => {
|
||||||
return (
|
return (
|
||||||
<div className="grid w-full grid-cols-3 border-b p-3">
|
<div className="grid w-full grid-cols-2 border-b p-3">
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5">
|
||||||
<ToggleSidebar />
|
<ToggleSidebar />
|
||||||
<Breadcrumbs />
|
<Breadcrumbs />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<Searchbar />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end gap-x-3">
|
<div className="flex items-center justify-end gap-x-3">
|
||||||
<div className="text-ui-fg-muted flex items-center gap-x-1">
|
<ToggleNotifications />
|
||||||
<ToggleNotifications />
|
|
||||||
<SettingsLink />
|
|
||||||
</div>
|
|
||||||
<LoggedInUser />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -460,13 +173,41 @@ const DesktopSidebarContainer = ({ children }: PropsWithChildren) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MobileSidebarContainer = ({ children }: PropsWithChildren) => {
|
const MobileSidebarContainer = ({ children }: PropsWithChildren) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { mobile, toggle } = useSidebar()
|
const { mobile, toggle } = useSidebar()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={mobile} onOpenChange={() => toggle("mobile")}>
|
<Dialog.Root open={mobile} onOpenChange={() => toggle("mobile")}>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="bg-ui-bg-overlay fixed inset-0" />
|
<Dialog.Overlay
|
||||||
<Dialog.Content className="bg-ui-bg-subtle fixed inset-y-0 left-0 h-screen w-full max-w-[240px] border-r">
|
className={clx(
|
||||||
|
"bg-ui-bg-overlay fixed inset-0",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Dialog.Content
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-subtle shadow-elevation-modal fixed inset-y-2 left-2 flex w-full max-w-[304px] flex-col overflow-hidden rounded-lg border-r",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-left-1/2 data-[state=open]:slide-in-from-left-1/2 duration-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="p-3">
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
>
|
||||||
|
<XMark />
|
||||||
|
</IconButton>
|
||||||
|
</Dialog.Close>
|
||||||
|
<Dialog.Title className="sr-only">
|
||||||
|
{t("app.nav.accessibility.title")}
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description className="sr-only">
|
||||||
|
{t("app.nav.accessibility.description")}
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./user-menu"
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
import {
|
||||||
|
BookOpen,
|
||||||
|
CircleHalfSolid,
|
||||||
|
EllipsisHorizontal,
|
||||||
|
Keyboard,
|
||||||
|
OpenRectArrowOut,
|
||||||
|
TimelineVertical,
|
||||||
|
User as UserIcon,
|
||||||
|
XMark,
|
||||||
|
} from "@medusajs/icons"
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
DropdownMenu,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Kbd,
|
||||||
|
Text,
|
||||||
|
clx,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import * as Dialog from "@radix-ui/react-dialog"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { Skeleton } from "../../common/skeleton"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Link, useLocation, useNavigate } from "react-router-dom"
|
||||||
|
import { useLogout, useMe } from "../../../hooks/api"
|
||||||
|
import { queryClient } from "../../../lib/query-client"
|
||||||
|
import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks"
|
||||||
|
import { useTheme } from "../../../providers/theme-provider"
|
||||||
|
|
||||||
|
export const UserMenu = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const [openMenu, setOpenMenu] = useState(false)
|
||||||
|
const [openModal, setOpenModal] = useState(false)
|
||||||
|
|
||||||
|
const toggleModal = () => {
|
||||||
|
setOpenMenu(false)
|
||||||
|
setOpenModal(!openModal)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<DropdownMenu open={openMenu} onOpenChange={setOpenMenu}>
|
||||||
|
<UserBadge />
|
||||||
|
<DropdownMenu.Content className="min-w-[var(--radix-dropdown-menu-trigger-width)] max-w-[var(--radix-dropdown-menu-trigger-width)]">
|
||||||
|
<UserItem />
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item asChild>
|
||||||
|
<Link to="/settings/profile" state={{ from: location.pathname }}>
|
||||||
|
<UserIcon className="text-ui-fg-subtle mr-2" />
|
||||||
|
{t("app.menus.user.profileSettings")}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item asChild>
|
||||||
|
<Link to="https://docs.medusajs.com/v2" target="_blank">
|
||||||
|
<BookOpen className="text-ui-fg-subtle mr-2" />
|
||||||
|
{t("app.menus.user.documentation")}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item asChild>
|
||||||
|
<Link to="https://medusajs.com/changelog/" target="_blank">
|
||||||
|
<TimelineVertical className="text-ui-fg-subtle mr-2" />
|
||||||
|
{t("app.menus.user.changelog")}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item onClick={toggleModal}>
|
||||||
|
<Keyboard className="text-ui-fg-subtle mr-2" />
|
||||||
|
{t("app.menus.user.shortcuts")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<ThemeToggle />
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<Logout />
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
<GlobalKeybindsModal open={openModal} onOpenChange={setOpenModal} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserBadge = () => {
|
||||||
|
const { user, isPending, isError, error } = useMe()
|
||||||
|
|
||||||
|
const name = [user?.first_name, user?.last_name].filter(Boolean).join(" ")
|
||||||
|
const displayName = name || user?.email
|
||||||
|
|
||||||
|
const fallback = displayName ? displayName[0].toUpperCase() : null
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<button className="shadow-borders-base flex max-w-[192px] select-none items-center gap-x-2 overflow-hidden text-ellipsis whitespace-nowrap rounded-full py-1 pl-1 pr-2.5">
|
||||||
|
<Skeleton className="h-5 w-5 rounded-full" />
|
||||||
|
<Skeleton className="h-[9px] w-[70px]" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3">
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
disabled={!user}
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-subtle grid w-full cursor-pointer grid-cols-[24px_1fr_15px] items-center gap-2 rounded-md py-1 pl-0.5 pr-2 outline-none",
|
||||||
|
"hover:bg-ui-bg-subtle-hover",
|
||||||
|
"data-[state=open]:bg-ui-bg-subtle-hover",
|
||||||
|
"focus-visible:shadow-borders-focus"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex size-6 items-center justify-center">
|
||||||
|
{fallback ? (
|
||||||
|
<Avatar size="xsmall" fallback={fallback} />
|
||||||
|
) : (
|
||||||
|
<Skeleton className="h-6 w-6 rounded-full" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
{displayName ? (
|
||||||
|
<Text
|
||||||
|
size="xsmall"
|
||||||
|
weight="plus"
|
||||||
|
leading="compact"
|
||||||
|
className="truncate"
|
||||||
|
>
|
||||||
|
{displayName}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="h-[9px] w-[70px]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<EllipsisHorizontal className="text-ui-fg-muted" />
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeToggle = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.SubMenu>
|
||||||
|
<DropdownMenu.SubMenuTrigger className="rounded-md">
|
||||||
|
<CircleHalfSolid className="text-ui-fg-subtle mr-2" />
|
||||||
|
{t("app.menus.user.theme.label")}
|
||||||
|
</DropdownMenu.SubMenuTrigger>
|
||||||
|
<DropdownMenu.SubMenuContent>
|
||||||
|
<DropdownMenu.RadioGroup value={theme}>
|
||||||
|
<DropdownMenu.RadioItem
|
||||||
|
value="system"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setTheme("system")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("app.menus.user.theme.system")}
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem
|
||||||
|
value="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setTheme("light")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("app.menus.user.theme.light")}
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem
|
||||||
|
value="dark"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setTheme("dark")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("app.menus.user.theme.dark")}
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
</DropdownMenu.SubMenuContent>
|
||||||
|
</DropdownMenu.SubMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Logout = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { mutateAsync: logoutMutation } = useLogout()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logoutMutation(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
/**
|
||||||
|
* When the user logs out, we want to clear the query cache
|
||||||
|
*/
|
||||||
|
queryClient.clear()
|
||||||
|
navigate("/login")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Item onClick={handleLogout}>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<OpenRectArrowOut className="text-ui-fg-subtle" />
|
||||||
|
<span>{t("app.menus.actions.logout")}</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const GlobalKeybindsModal = (props: {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const globalShortcuts = useGlobalShortcuts()
|
||||||
|
|
||||||
|
const [searchValue, onSearchValueChange] = useState("")
|
||||||
|
|
||||||
|
const searchResults = searchValue
|
||||||
|
? globalShortcuts.filter((shortcut) => {
|
||||||
|
return shortcut.label.toLowerCase().includes(searchValue?.toLowerCase())
|
||||||
|
})
|
||||||
|
: globalShortcuts
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root {...props}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className="bg-ui-bg-overlay fixed inset-0" />
|
||||||
|
<Dialog.Content className="bg-ui-bg-subtle shadow-elevation-modal fixed left-[50%] top-[50%] flex h-full max-h-[612px] w-full max-w-[560px] translate-x-[-50%] translate-y-[-50%] flex-col divide-y overflow-hidden rounded-lg">
|
||||||
|
<div className="flex flex-col gap-y-3 px-6 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Dialog.Title asChild>
|
||||||
|
<Heading>{t("app.menus.user.shortcuts")}</Heading>
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description className="sr-only"></Dialog.Description>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Kbd>esc</Kbd>
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<IconButton variant="transparent" size="small">
|
||||||
|
<XMark />
|
||||||
|
</IconButton>
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
type="search"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(e) => onSearchValueChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col divide-y overflow-y-auto">
|
||||||
|
{searchResults.map((shortcut, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="text-ui-fg-subtle flex items-center justify-between px-6 py-3"
|
||||||
|
>
|
||||||
|
<Text size="small">{shortcut.label}</Text>
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
{shortcut.keys.Mac?.map((key, index) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-x-1" key={index}>
|
||||||
|
<Kbd>{key}</Kbd>
|
||||||
|
{index < (shortcut.keys.Mac?.length || 0) - 1 && (
|
||||||
|
<span className="txt-compact-xsmall text-ui-fg-subtle">
|
||||||
|
{t("app.keyboardShortcuts.then")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserItem = () => {
|
||||||
|
const { user, isPending, isError, error } = useMe()
|
||||||
|
|
||||||
|
const loaded = !isPending && !!user
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return <div></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = [user.first_name, user.last_name].filter(Boolean).join(" ")
|
||||||
|
const email = user.email
|
||||||
|
const fallback = name ? name[0].toUpperCase() : email[0].toUpperCase()
|
||||||
|
const avatar = user.avatar_url
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-x-3 overflow-hidden px-2 py-1">
|
||||||
|
<Avatar
|
||||||
|
size="small"
|
||||||
|
variant="rounded"
|
||||||
|
src={avatar || undefined}
|
||||||
|
fallback={fallback}
|
||||||
|
/>
|
||||||
|
<div className="block w-full min-w-0 max-w-[187px] overflow-hidden whitespace-nowrap">
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
weight="plus"
|
||||||
|
leading="compact"
|
||||||
|
className="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{name || email}
|
||||||
|
</Text>
|
||||||
|
{!!name && (
|
||||||
|
<Text
|
||||||
|
size="xsmall"
|
||||||
|
leading="compact"
|
||||||
|
className="text-ui-fg-subtle overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{email}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "react"
|
} from "react"
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useLocation } from "react-router-dom"
|
import { useLocation, useNavigate } from "react-router-dom"
|
||||||
import { Shortcut, ShortcutType } from "../../providers/keybind-provider"
|
import { Shortcut, ShortcutType } from "../../providers/keybind-provider"
|
||||||
import { useGlobalShortcuts } from "../../providers/keybind-provider/hooks"
|
import { useGlobalShortcuts } from "../../providers/keybind-provider/hooks"
|
||||||
import { useSearch } from "../../providers/search-provider"
|
import { useSearch } from "../../providers/search-provider"
|
||||||
@@ -20,6 +20,7 @@ export const Search = () => {
|
|||||||
const globalCommands = useGlobalShortcuts()
|
const globalCommands = useGlobalShortcuts()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
@@ -40,9 +41,18 @@ export const Search = () => {
|
|||||||
}))
|
}))
|
||||||
}, [globalCommands])
|
}, [globalCommands])
|
||||||
|
|
||||||
const handleSelect = (callback: () => void) => {
|
const handleSelect = (shortcut: Shortcut) => {
|
||||||
callback()
|
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
|
|
||||||
|
if (shortcut.to) {
|
||||||
|
navigate(shortcut.to)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shortcut.callback) {
|
||||||
|
shortcut.callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,13 +70,25 @@ export const Search = () => {
|
|||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={item.label}
|
key={item.label}
|
||||||
onSelect={() => handleSelect(item.callback)}
|
onSelect={() => handleSelect(item)}
|
||||||
className="flex items-center justify-between"
|
className="flex items-center justify-between"
|
||||||
>
|
>
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5">
|
||||||
{item.keys.Mac?.map((key, index) => {
|
{item.keys.Mac?.map((key, index) => {
|
||||||
return <Kbd key={index}>{key}</Kbd>
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-x-1"
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<Kbd>{key}</Kbd>
|
||||||
|
{index < (item.keys.Mac?.length || 0) - 1 && (
|
||||||
|
<span className="txt-compact-xsmall text-ui-fg-subtle">
|
||||||
|
{t("app.keyboardShortcuts.then")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
@@ -104,8 +126,8 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
|||||||
<Dialog.Root {...props}>
|
<Dialog.Root {...props}>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="bg-ui-bg-overlay fixed inset-0" />
|
<Dialog.Overlay className="bg-ui-bg-overlay fixed inset-0" />
|
||||||
<Dialog.Content className="bg-ui-bg-base shadow-elevation-modal fixed left-[50%] top-[50%] w-full max-w-2xl translate-x-[-50%] translate-y-[-50%] overflow-hidden rounded-xl p-0">
|
<Dialog.Content className="bg-ui-bg-base shadow-elevation-modal fixed left-[50%] top-[50%] flex max-h-[calc(100%-16px)] w-[calc(100%-16px)] min-w-0 max-w-2xl translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-xl p-0">
|
||||||
<CommandPalette className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input]]:h-[52px]">
|
<CommandPalette className="[&_[cmdk-group-heading]]:text-muted-foreground flex h-full flex-col overflow-hidden [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input]]:h-[52px]">
|
||||||
{children}
|
{children}
|
||||||
</CommandPalette>
|
</CommandPalette>
|
||||||
<div className="bg-ui-bg-field text-ui-fg-subtle flex items-center justify-end border-t px-4 py-3">
|
<div className="bg-ui-bg-field text-ui-fg-subtle flex items-center justify-end border-t px-4 py-3">
|
||||||
@@ -167,7 +189,7 @@ const CommandList = forwardRef<
|
|||||||
<Command.List
|
<Command.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clx(
|
className={clx(
|
||||||
"max-h-[300px] overflow-y-auto overflow-x-hidden px-2 pb-4",
|
"max-h-[300px] flex-1 overflow-y-auto overflow-x-hidden px-2 pb-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
+8
-2
@@ -14,7 +14,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react"
|
} from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link, useNavigate } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { NoResults } from "../../../common/empty-table-content"
|
import { NoResults } from "../../../common/empty-table-content"
|
||||||
|
|
||||||
type BulkCommand = {
|
type BulkCommand = {
|
||||||
@@ -83,7 +83,6 @@ export const DataTableRoot = <TData,>({
|
|||||||
layout = "fit",
|
layout = "fit",
|
||||||
}: DataTableRootProps<TData>) => {
|
}: DataTableRootProps<TData>) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
|
||||||
const [showStickyBorder, setShowStickyBorder] = useState(false)
|
const [showStickyBorder, setShowStickyBorder] = useState(false)
|
||||||
|
|
||||||
const scrollableRef = useRef<HTMLDivElement>(null)
|
const scrollableRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -204,6 +203,13 @@ export const DataTableRoot = <TData,>({
|
|||||||
return (
|
return (
|
||||||
<Table.Row
|
<Table.Row
|
||||||
key={row.id}
|
key={row.id}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
console.log("e.key", e.key, e.target)
|
||||||
|
|
||||||
|
if (e.key === "x") {
|
||||||
|
row.toggleSelected()
|
||||||
|
}
|
||||||
|
}}
|
||||||
data-selected={row.getIsSelected()}
|
data-selected={row.getIsSelected()}
|
||||||
className={clx(
|
className={clx(
|
||||||
"transition-fg group/row group relative [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
"transition-fg group/row group relative [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
QueryKey,
|
QueryKey,
|
||||||
UseMutationOptions,
|
UseMutationOptions,
|
||||||
@@ -5,11 +7,9 @@ import {
|
|||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
} from "@tanstack/react-query"
|
} from "@tanstack/react-query"
|
||||||
import { client } from "../../lib/client"
|
import { sdk } from "../../lib/client"
|
||||||
import { queryClient } from "../../lib/query-client"
|
import { queryClient } from "../../lib/query-client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
import { UpdateUserReq } from "../../types/api-payloads"
|
|
||||||
import { UserDeleteRes, UserListRes, UserRes } from "../../types/api-responses"
|
|
||||||
|
|
||||||
const USERS_QUERY_KEY = "users" as const
|
const USERS_QUERY_KEY = "users" as const
|
||||||
const usersQueryKeys = {
|
const usersQueryKeys = {
|
||||||
@@ -18,10 +18,16 @@ const usersQueryKeys = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useMe = (
|
export const useMe = (
|
||||||
options?: UseQueryOptions<UserRes, Error, UserRes, QueryKey>
|
query?: HttpTypes.AdminUserParams,
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
HttpTypes.AdminUserResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminUserResponse,
|
||||||
|
QueryKey
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => client.users.me(),
|
queryFn: () => sdk.admin.user.me(query),
|
||||||
queryKey: usersQueryKeys.me(),
|
queryKey: usersQueryKeys.me(),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
@@ -34,14 +40,19 @@ export const useMe = (
|
|||||||
|
|
||||||
export const useUser = (
|
export const useUser = (
|
||||||
id: string,
|
id: string,
|
||||||
query?: Record<string, any>,
|
query?: HttpTypes.AdminUserParams,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<UserRes, Error, UserRes, QueryKey>,
|
UseQueryOptions<
|
||||||
|
HttpTypes.AdminUserResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminUserResponse,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => client.users.retrieve(id, query),
|
queryFn: () => sdk.admin.user.retrieve(id, query),
|
||||||
queryKey: usersQueryKeys.detail(id),
|
queryKey: usersQueryKeys.detail(id),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
@@ -50,14 +61,19 @@ export const useUser = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useUsers = (
|
export const useUsers = (
|
||||||
query?: Record<string, any>,
|
query?: HttpTypes.AdminUserListParams,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<UserListRes, Error, UserListRes, QueryKey>,
|
UseQueryOptions<
|
||||||
|
HttpTypes.AdminUserListResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminUserListResponse,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => client.users.list(query),
|
queryFn: () => sdk.admin.user.list(query),
|
||||||
queryKey: usersQueryKeys.list(query),
|
queryKey: usersQueryKeys.list(query),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
@@ -65,12 +81,38 @@ export const useUsers = (
|
|||||||
return { ...data, ...rest }
|
return { ...data, ...rest }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpdateUser = (
|
export const useCreateUser = (
|
||||||
id: string,
|
query?: HttpTypes.AdminUserParams,
|
||||||
options?: UseMutationOptions<UserRes, Error, UpdateUserReq>
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminUserResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminCreateUser,
|
||||||
|
QueryKey
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload) => client.users.update(id, payload),
|
mutationFn: (payload) => sdk.admin.user.create(payload, query),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: usersQueryKeys.lists() })
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateUser = (
|
||||||
|
id: string,
|
||||||
|
query?: HttpTypes.AdminUserParams,
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminUserResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminUpdateUser,
|
||||||
|
QueryKey
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => sdk.admin.user.update(id, payload, query),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({ queryKey: usersQueryKeys.detail(id) })
|
queryClient.invalidateQueries({ queryKey: usersQueryKeys.detail(id) })
|
||||||
queryClient.invalidateQueries({ queryKey: usersQueryKeys.lists() })
|
queryClient.invalidateQueries({ queryKey: usersQueryKeys.lists() })
|
||||||
@@ -86,10 +128,14 @@ export const useUpdateUser = (
|
|||||||
|
|
||||||
export const useDeleteUser = (
|
export const useDeleteUser = (
|
||||||
id: string,
|
id: string,
|
||||||
options?: UseMutationOptions<UserDeleteRes, Error, void>
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminUserDeleteResponse,
|
||||||
|
FetchError,
|
||||||
|
void
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => client.users.delete(id),
|
mutationFn: () => sdk.admin.user.delete(id),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({ queryKey: usersQueryKeys.detail(id) })
|
queryClient.invalidateQueries({ queryKey: usersQueryKeys.detail(id) })
|
||||||
queryClient.invalidateQueries({ queryKey: usersQueryKeys.lists() })
|
queryClient.invalidateQueries({ queryKey: usersQueryKeys.lists() })
|
||||||
|
|||||||
@@ -90,12 +90,6 @@
|
|||||||
"mustBeInt": "The value must be a whole number.",
|
"mustBeInt": "The value must be a whole number.",
|
||||||
"mustBePositive": "The value must be a positive number."
|
"mustBePositive": "The value must be a positive number."
|
||||||
},
|
},
|
||||||
"nav": {
|
|
||||||
"general": "General",
|
|
||||||
"developer": "Developer",
|
|
||||||
"extensions": "Extensions",
|
|
||||||
"settings": "Settings"
|
|
||||||
},
|
|
||||||
"actions": {
|
"actions": {
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saveAsDraft": "Save as draft",
|
"saveAsDraft": "Save as draft",
|
||||||
@@ -130,6 +124,7 @@
|
|||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"search": {
|
"search": {
|
||||||
|
"label": "Search",
|
||||||
"allAreas": "All areas",
|
"allAreas": "All areas",
|
||||||
"navigation": "Navigation",
|
"navigation": "Navigation",
|
||||||
"openResult": "Open result",
|
"openResult": "Open result",
|
||||||
@@ -139,27 +134,74 @@
|
|||||||
"pageShortcut": "Jump to",
|
"pageShortcut": "Jump to",
|
||||||
"settingShortcut": "Settings",
|
"settingShortcut": "Settings",
|
||||||
"commandShortcut": "Commands",
|
"commandShortcut": "Commands",
|
||||||
"goToOrders": "Go to orders",
|
"then": "then",
|
||||||
"goToProducts": "Go to products",
|
"navigation": {
|
||||||
"goToCollections": "Go to collections",
|
"goToOrders": "Orders",
|
||||||
"goToCategories": "Go to categories",
|
"goToProducts": "Products",
|
||||||
"goToCustomers": "Go to customers",
|
"goToCollections": "Collections",
|
||||||
"goToCustomerGroups": "Go to customer groups",
|
"goToCategories": "Categories",
|
||||||
"goToInventory": "Go to inventory",
|
"goToCustomers": "Customers",
|
||||||
"goToReservations": "Go to reservations",
|
"goToCustomerGroups": "Customer Groups",
|
||||||
"goToPriceLists": "Go to price lists",
|
"goToInventory": "Inventory",
|
||||||
"goToPromotions": "Go to promotions",
|
"goToReservations": "Reservations",
|
||||||
"goToCampaigns": "Go to campaigns",
|
"goToPriceLists": "Price Lists",
|
||||||
"goToStore": "Go to store",
|
"goToPromotions": "Promotions",
|
||||||
"goToUsers": "Go to users",
|
"goToCampaigns": "Campaigns"
|
||||||
"goToRegions": "Go to regions",
|
},
|
||||||
"goToTaxRegions": "Go to tax region",
|
"settings": {
|
||||||
"goToSalesChannels": "Go to sales channels",
|
"goToSettings": "Settings",
|
||||||
"goToProductTypes": "Go to product types",
|
"goToStore": "Store",
|
||||||
"goToLocations": "Go to locations",
|
"goToUsers": "Users",
|
||||||
"goToPublishableApiKeys": "Go to publishable API keys",
|
"goToRegions": "Regions",
|
||||||
"goToSecretApiKeys": "Go to secret API keys",
|
"goToTaxRegions": "Tax Regions",
|
||||||
"goToWorkflows": "Go to workflows"
|
"goToSalesChannels": "Sales Channels",
|
||||||
|
"goToProductTypes": "Product Types",
|
||||||
|
"goToLocations": "Locations",
|
||||||
|
"goToPublishableApiKeys": "Publishable API Keys",
|
||||||
|
"goToSecretApiKeys": "Secret API Keys",
|
||||||
|
"goToWorkflows": "Workflows",
|
||||||
|
"goToProfile": "Profile"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menus": {
|
||||||
|
"user": {
|
||||||
|
"documentation": "Documentation",
|
||||||
|
"changelog": "Changelog",
|
||||||
|
"shortcuts": "Shortcuts",
|
||||||
|
"profileSettings": "Profile settings",
|
||||||
|
"theme": {
|
||||||
|
"label": "Theme",
|
||||||
|
"dark": "Dark",
|
||||||
|
"light": "Light",
|
||||||
|
"system": "System"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"store": {
|
||||||
|
"label": "Store",
|
||||||
|
"storeSettings": "Store settings"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"logout": "Log out"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"accessibility": {
|
||||||
|
"title": "Navigation",
|
||||||
|
"description": "Navigation menu for the dashboard."
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"extensions": "Extensions"
|
||||||
|
},
|
||||||
|
"main": {
|
||||||
|
"store": "Store",
|
||||||
|
"storeSettings": "Store settings"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"header": "Settings",
|
||||||
|
"general": "General",
|
||||||
|
"developer": "Developer",
|
||||||
|
"myAccount": "My Account"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const useShortcuts = ({
|
|||||||
debounce: number
|
debounce: number
|
||||||
}) => {
|
}) => {
|
||||||
const [keys, setKeys] = useState<string[]>([])
|
const [keys, setKeys] = useState<string[]>([])
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const removeKeys = useCallback(
|
const removeKeys = useCallback(
|
||||||
@@ -42,6 +43,15 @@ export const useShortcuts = ({
|
|||||||
if (shortcut && shortcut.callback) {
|
if (shortcut && shortcut.callback) {
|
||||||
shortcut.callback()
|
shortcut.callback()
|
||||||
setKeys([])
|
setKeys([])
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shortcut && shortcut.to) {
|
||||||
|
navigate(shortcut.to)
|
||||||
|
setKeys([])
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}, debounce / 2),
|
}, debounce / 2),
|
||||||
[]
|
[]
|
||||||
@@ -105,170 +115,186 @@ export const useGlobalShortcuts = () => {
|
|||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "O"],
|
Mac: ["G", "O"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToOrders"),
|
label: t("app.keyboardShortcuts.navigation.goToOrders"),
|
||||||
type: "pageShortcut",
|
type: "pageShortcut",
|
||||||
callback: () => navigate("/orders"),
|
to: "/orders",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "P"],
|
Mac: ["G", "P"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToProducts"),
|
label: t("app.keyboardShortcuts.navigation.goToProducts"),
|
||||||
type: "pageShortcut",
|
type: "pageShortcut",
|
||||||
callback: () => navigate("/products"),
|
to: "/products",
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: {
|
|
||||||
Mac: ["G", "P", "C"],
|
|
||||||
},
|
|
||||||
label: t("app.keyboardShortcuts.goToCollections"),
|
|
||||||
type: "pageShortcut",
|
|
||||||
callback: () => navigate("/collections"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: {
|
|
||||||
Mac: ["G", "P", "A"],
|
|
||||||
},
|
|
||||||
label: t("app.keyboardShortcuts.goToCategories"),
|
|
||||||
type: "pageShortcut",
|
|
||||||
callback: () => navigate("/categories"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "C"],
|
Mac: ["G", "C"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToCustomers"),
|
label: t("app.keyboardShortcuts.navigation.goToCollections"),
|
||||||
type: "pageShortcut",
|
type: "pageShortcut",
|
||||||
callback: () => navigate("/customers"),
|
to: "/collections",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "C", "G"],
|
Mac: ["G", "A"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToCustomerGroups"),
|
label: t("app.keyboardShortcuts.navigation.goToCategories"),
|
||||||
type: "pageShortcut",
|
type: "pageShortcut",
|
||||||
callback: () => navigate("/customer-groups"),
|
to: "/categories",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "U"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.navigation.goToCustomers"),
|
||||||
|
type: "pageShortcut",
|
||||||
|
to: "/customers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "G"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.navigation.goToCustomerGroups"),
|
||||||
|
type: "pageShortcut",
|
||||||
|
to: "/customer-groups",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "I"],
|
Mac: ["G", "I"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToInventory"),
|
label: t("app.keyboardShortcuts.navigation.goToInventory"),
|
||||||
type: "pageShortcut",
|
type: "pageShortcut",
|
||||||
callback: () => navigate("/inventory"),
|
to: "/inventory",
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: {
|
|
||||||
Mac: ["G", "I", "R"],
|
|
||||||
},
|
|
||||||
label: t("app.keyboardShortcuts.goToReservations"),
|
|
||||||
type: "pageShortcut",
|
|
||||||
callback: () => navigate("/reservations"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: {
|
|
||||||
Mac: ["G", "L"],
|
|
||||||
},
|
|
||||||
label: t("app.keyboardShortcuts.goToPriceLists"),
|
|
||||||
type: "pageShortcut",
|
|
||||||
callback: () => navigate("/price-lists"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "R"],
|
Mac: ["G", "R"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToPromotions"),
|
label: t("app.keyboardShortcuts.navigation.goToReservations"),
|
||||||
type: "pageShortcut",
|
type: "pageShortcut",
|
||||||
callback: () => navigate("/promotions"),
|
to: "/reservations",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "R", "C"],
|
Mac: ["G", "L"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToCampaigns"),
|
label: t("app.keyboardShortcuts.navigation.goToPriceLists"),
|
||||||
type: "pageShortcut",
|
type: "pageShortcut",
|
||||||
callback: () => navigate("/campaigns"),
|
to: "/price-lists",
|
||||||
},
|
|
||||||
//
|
|
||||||
{
|
|
||||||
keys: {
|
|
||||||
Mac: ["G", "S", "S"],
|
|
||||||
},
|
|
||||||
label: t("app.keyboardShortcuts.goToStore"),
|
|
||||||
type: "settingShortcut",
|
|
||||||
callback: () => navigate("/settings/store"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "S", "U"],
|
Mac: ["G", "M"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToUsers"),
|
label: t("app.keyboardShortcuts.navigation.goToPromotions"),
|
||||||
type: "settingShortcut",
|
type: "pageShortcut",
|
||||||
callback: () => navigate("/settings/users"),
|
to: "/promotions",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "S", "R"],
|
Mac: ["G", "K"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToRegions"),
|
label: t("app.keyboardShortcuts.navigation.goToCampaigns"),
|
||||||
|
type: "pageShortcut",
|
||||||
|
to: "/campaigns",
|
||||||
|
},
|
||||||
|
// Settings
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", ","],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.settings.goToSettings"),
|
||||||
type: "settingShortcut",
|
type: "settingShortcut",
|
||||||
callback: () => navigate("/settings/regions"),
|
to: "/settings",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "S", "T"],
|
Mac: ["G", ",", "S"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToTaxRegions"),
|
label: t("app.keyboardShortcuts.settings.goToStore"),
|
||||||
type: "settingShortcut",
|
type: "settingShortcut",
|
||||||
callback: () => navigate("/settings/tax-regions"),
|
to: "/settings/store",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "S", "A"],
|
Mac: ["G", ",", "U"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToSalesChannels"),
|
label: t("app.keyboardShortcuts.settings.goToUsers"),
|
||||||
type: "settingShortcut",
|
type: "settingShortcut",
|
||||||
callback: () => navigate("/settings/sales-channels"),
|
to: "/settings/users",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "S", "P"],
|
Mac: ["G", ",", "R"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToProductTypes"),
|
label: t("app.keyboardShortcuts.settings.goToRegions"),
|
||||||
type: "settingShortcut",
|
type: "settingShortcut",
|
||||||
callback: () => navigate("/settings/product-types"),
|
to: "/settings/regions",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "S", "L"],
|
Mac: ["G", ",", "T"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToLocations"),
|
label: t("app.keyboardShortcuts.settings.goToTaxRegions"),
|
||||||
type: "settingShortcut",
|
type: "settingShortcut",
|
||||||
callback: () => navigate("/settings/locations"),
|
to: "/settings/tax-regions",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "S", "J"],
|
Mac: ["G", ",", "A"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToPublishableApiKeys"),
|
label: t("app.keyboardShortcuts.settings.goToSalesChannels"),
|
||||||
type: "settingShortcut",
|
type: "settingShortcut",
|
||||||
callback: () => navigate("/settings/publishable-api-keys"),
|
to: "/settings/sales-channels",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "S", "K"],
|
Mac: ["G", ",", "P"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToSecretApiKeys"),
|
label: t("app.keyboardShortcuts.settings.goToProductTypes"),
|
||||||
type: "settingShortcut",
|
type: "settingShortcut",
|
||||||
callback: () => navigate("/settings/secret-api-keys"),
|
to: "/settings/product-types",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
Mac: ["G", "S", "W"],
|
Mac: ["G", ",", "L"],
|
||||||
},
|
},
|
||||||
label: t("app.keyboardShortcuts.goToWorkflows"),
|
label: t("app.keyboardShortcuts.settings.goToLocations"),
|
||||||
type: "settingShortcut",
|
type: "settingShortcut",
|
||||||
callback: () => navigate("/settings/workflows"),
|
to: "/settings/locations",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", ",", "J"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.settings.goToPublishableApiKeys"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
to: "/settings/publishable-api-keys",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", ",", "K"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.settings.goToSecretApiKeys"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
to: "/settings/secret-api-keys",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", ",", "W"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.settings.goToWorkflows"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
to: "/settings/workflows",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", ",", "M"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.settings.goToProfile"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
to: "/settings/profile",
|
||||||
},
|
},
|
||||||
// Commands
|
// Commands
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ export type Shortcut = {
|
|||||||
keys: Keys
|
keys: Keys
|
||||||
type: ShortcutType
|
type: ShortcutType
|
||||||
label: string
|
label: string
|
||||||
callback: () => void
|
|
||||||
_defaultKeys?: Keys
|
_defaultKeys?: Keys
|
||||||
}
|
} & (
|
||||||
|
| {
|
||||||
|
callback: () => void
|
||||||
|
to?: never
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
to: string
|
||||||
|
callback?: never
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -624,6 +624,9 @@ export const RouteMap: RouteObject[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Settings",
|
||||||
|
},
|
||||||
element: <SettingsLayout />,
|
element: <SettingsLayout />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
import { PropsWithChildren, useEffect, useState } from "react"
|
import { PropsWithChildren, useEffect, useState } from "react"
|
||||||
import { Search } from "../../components/search"
|
import { Search } from "../../components/search"
|
||||||
|
import { useSidebar } from "../sidebar-provider"
|
||||||
import { SearchContext } from "./search-context"
|
import { SearchContext } from "./search-context"
|
||||||
|
|
||||||
export const SearchProvider = ({ children }: PropsWithChildren) => {
|
export const SearchProvider = ({ children }: PropsWithChildren) => {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const { mobile, toggle } = useSidebar()
|
||||||
|
|
||||||
const toggleSearch = () => {
|
const toggleSearch = () => {
|
||||||
setOpen(!open)
|
const update = !open
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the mobile sidebar is open, then make sure
|
||||||
|
* to close it when opening the search
|
||||||
|
*/
|
||||||
|
if (update && mobile) {
|
||||||
|
toggle("mobile")
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export type { Theme } from "./theme-context"
|
export type { ThemeOption as Theme } from "./theme-context"
|
||||||
export * from "./theme-provider"
|
export * from "./theme-provider"
|
||||||
export * from "./use-theme"
|
export * from "./use-theme"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { createContext } from "react"
|
import { createContext } from "react"
|
||||||
|
|
||||||
export type Theme = "light" | "dark"
|
export type ThemeOption = "light" | "dark" | "system"
|
||||||
|
export type ThemeValue = "light" | "dark"
|
||||||
|
|
||||||
type ThemeContextValue = {
|
type ThemeContextValue = {
|
||||||
theme: Theme
|
theme: ThemeOption
|
||||||
setTheme: (theme: Theme) => void
|
setTheme: (theme: ThemeOption) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThemeContext = createContext<ThemeContextValue | null>(null)
|
export const ThemeContext = createContext<ThemeContextValue | null>(null)
|
||||||
|
|||||||
@@ -1,16 +1,44 @@
|
|||||||
import { PropsWithChildren, useEffect, useState } from "react"
|
import { PropsWithChildren, useEffect, useState } from "react"
|
||||||
import { Theme, ThemeContext } from "./theme-context"
|
import { ThemeContext, ThemeOption, ThemeValue } from "./theme-context"
|
||||||
|
|
||||||
const THEME_KEY = "medusa_admin_theme"
|
const THEME_KEY = "medusa_admin_theme"
|
||||||
|
|
||||||
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
function getDefaultValue(): ThemeOption {
|
||||||
const [state, setState] = useState<Theme>(
|
const persisted = localStorage?.getItem(THEME_KEY) as ThemeOption
|
||||||
(localStorage?.getItem(THEME_KEY) as Theme) || "light"
|
|
||||||
)
|
|
||||||
|
|
||||||
const setTheme = (theme: Theme) => {
|
if (persisted) {
|
||||||
|
return persisted
|
||||||
|
}
|
||||||
|
|
||||||
|
return "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThemeValue(selected: ThemeOption): ThemeValue {
|
||||||
|
if (selected === "system") {
|
||||||
|
if (window !== undefined) {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to light theme if we can't detect the system preference
|
||||||
|
return "light"
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const [state, setState] = useState<ThemeOption>(getDefaultValue())
|
||||||
|
const [value, setValue] = useState<ThemeValue>(getThemeValue(state))
|
||||||
|
|
||||||
|
const setTheme = (theme: ThemeOption) => {
|
||||||
localStorage.setItem(THEME_KEY, theme)
|
localStorage.setItem(THEME_KEY, theme)
|
||||||
|
|
||||||
|
const themeValue = getThemeValue(theme)
|
||||||
|
|
||||||
setState(theme)
|
setState(theme)
|
||||||
|
setValue(themeValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,8 +62,8 @@ export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
|||||||
)
|
)
|
||||||
document.head.appendChild(css)
|
document.head.appendChild(css)
|
||||||
|
|
||||||
html.classList.remove(state === "light" ? "dark" : "light")
|
html.classList.remove(value === "light" ? "dark" : "light")
|
||||||
html.classList.add(state)
|
html.classList.add(value)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-enable transitions after the theme has been set,
|
* Re-enable transitions after the theme has been set,
|
||||||
@@ -44,7 +72,7 @@ export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
|||||||
window.getComputedStyle(css).opacity
|
window.getComputedStyle(css).opacity
|
||||||
document.head.removeChild(css)
|
document.head.removeChild(css)
|
||||||
}
|
}
|
||||||
}, [state])
|
}, [value])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ theme: state, setTheme }}>
|
<ThemeContext.Provider value={{ theme: state, setTheme }}>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const Settings = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.pathname === "/settings") {
|
if (location.pathname === "/settings") {
|
||||||
navigate("/settings/profile")
|
navigate("/settings/store", { replace: true })
|
||||||
}
|
}
|
||||||
}, [location.pathname, navigate])
|
}, [location.pathname, navigate])
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { Store } from "./store"
|
|||||||
import { TaxRate } from "./tax-rate"
|
import { TaxRate } from "./tax-rate"
|
||||||
import { TaxRegion } from "./tax-region"
|
import { TaxRegion } from "./tax-region"
|
||||||
import { Upload } from "./upload"
|
import { Upload } from "./upload"
|
||||||
|
import { User } from "./user"
|
||||||
|
|
||||||
export class Admin {
|
export class Admin {
|
||||||
public invite: Invite
|
public invite: Invite
|
||||||
@@ -48,6 +49,7 @@ export class Admin {
|
|||||||
public taxRegion: TaxRegion
|
public taxRegion: TaxRegion
|
||||||
public store: Store
|
public store: Store
|
||||||
public productTag: ProductTag
|
public productTag: ProductTag
|
||||||
|
public user: User
|
||||||
public return: Return
|
public return: Return
|
||||||
|
|
||||||
constructor(client: Client) {
|
constructor(client: Client) {
|
||||||
@@ -74,6 +76,7 @@ export class Admin {
|
|||||||
this.taxRegion = new TaxRegion(client)
|
this.taxRegion = new TaxRegion(client)
|
||||||
this.store = new Store(client)
|
this.store = new Store(client)
|
||||||
this.productTag = new ProductTag(client)
|
this.productTag = new ProductTag(client)
|
||||||
|
this.user = new User(client)
|
||||||
this.return = new Return(client)
|
this.return = new Return(client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Client } from "../client"
|
||||||
|
import { ClientHeaders } from "../types"
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
private client: Client
|
||||||
|
constructor(client: Client) {
|
||||||
|
this.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
body: HttpTypes.AdminCreateUser,
|
||||||
|
query?: HttpTypes.AdminUserParams,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return this.client.fetch<HttpTypes.AdminUserResponse>(`/admin/users`, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
query,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: string,
|
||||||
|
body: HttpTypes.AdminUpdateUser,
|
||||||
|
query?: HttpTypes.AdminUserParams,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return this.client.fetch<HttpTypes.AdminUserResponse>(
|
||||||
|
`/admin/users/${id}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
query,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(
|
||||||
|
queryParams?: HttpTypes.AdminUserListParams,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return this.client.fetch<HttpTypes.AdminUserListResponse>(`/admin/users`, {
|
||||||
|
headers,
|
||||||
|
query: queryParams,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieve(
|
||||||
|
id: string,
|
||||||
|
query?: HttpTypes.AdminUserParams,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return this.client.fetch<HttpTypes.AdminUserResponse>(
|
||||||
|
`/admin/users/${id}`,
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string, headers?: ClientHeaders) {
|
||||||
|
return this.client.fetch<HttpTypes.AdminUserDeleteResponse>(
|
||||||
|
`/admin/users/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async me(query?: HttpTypes.AdminUserParams, headers?: ClientHeaders) {
|
||||||
|
return this.client.fetch<HttpTypes.AdminUserResponse>(`/admin/users/me`, {
|
||||||
|
query,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { BaseUserResponse } from "./common"
|
|
||||||
|
|
||||||
export type AdminUserResponse = BaseUserResponse
|
|
||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
export type BaseUserResponse = {
|
export interface AdminUser {
|
||||||
id: string
|
id: string
|
||||||
email: string
|
email: string
|
||||||
first_name: string | null
|
first_name: string | null
|
||||||
last_name: string | null
|
last_name: string | null
|
||||||
avatar_url: string | null
|
avatar_url: string | null
|
||||||
metadata: Record<string, unknown> | null
|
metadata: Record<string, unknown> | null
|
||||||
created_at: Date
|
created_at: string
|
||||||
updated_at: Date
|
updated_at: string
|
||||||
deleted_at: Date | null
|
deleted_at: string | null
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./entities"
|
||||||
|
export * from "./payloads"
|
||||||
|
export * from "./queries"
|
||||||
|
export * from "./responses"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export interface AdminCreateUser {
|
||||||
|
email: string
|
||||||
|
first_name?: string | null
|
||||||
|
last_name?: string | null
|
||||||
|
avatar_url?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUpdateUser {
|
||||||
|
first_name?: string | null
|
||||||
|
last_name?: string | null
|
||||||
|
avatar_url?: string | null
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { OperatorMap } from "../../../dal"
|
||||||
|
import { FindParams, SelectParams } from "../../common"
|
||||||
|
|
||||||
|
export interface AdminUserListParams extends FindParams {
|
||||||
|
q?: string
|
||||||
|
id?: string | string[]
|
||||||
|
email?: string | null
|
||||||
|
first_name?: string | null
|
||||||
|
last_name?: string | null
|
||||||
|
created_at?: OperatorMap<string>
|
||||||
|
updated_at?: OperatorMap<string>
|
||||||
|
deleted_at?: OperatorMap<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUserParams extends SelectParams {}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { DeleteResponse, PaginatedResponse } from "../../common"
|
||||||
|
import { AdminUser } from "./entities"
|
||||||
|
|
||||||
|
export interface AdminUserResponse {
|
||||||
|
user: AdminUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUserListResponse
|
||||||
|
extends PaginatedResponse<{ users: AdminUser[] }> {}
|
||||||
|
|
||||||
|
export interface AdminUserDeleteResponse extends DeleteResponse<"user"> {}
|
||||||
@@ -50,13 +50,13 @@ const SubMenuTrigger = React.forwardRef<
|
|||||||
"focus-visible:bg-ui-bg-component-hover focus:bg-ui-bg-component-hover",
|
"focus-visible:bg-ui-bg-component-hover focus:bg-ui-bg-component-hover",
|
||||||
"active:bg-ui-bg-component-pressed",
|
"active:bg-ui-bg-component-pressed",
|
||||||
"data-[disabled]:text-ui-fg-disabled data-[disabled]:pointer-events-none",
|
"data-[disabled]:text-ui-fg-disabled data-[disabled]:pointer-events-none",
|
||||||
"data-[state=open]:bg-ui-bg-base-hover",
|
"data-[state=open]:!bg-ui-bg-component-hover",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightMini className="ml-auto" />
|
<ChevronRightMini className="ml-auto text-ui-fg-muted" />
|
||||||
</Primitives.SubTrigger>
|
</Primitives.SubTrigger>
|
||||||
))
|
))
|
||||||
SubMenuTrigger.displayName = "DropdownMenu.SubMenuTrigger"
|
SubMenuTrigger.displayName = "DropdownMenu.SubMenuTrigger"
|
||||||
|
|||||||
Reference in New Issue
Block a user