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:
Kasper Fabricius Kristensen
2024-07-19 13:18:48 +02:00
committed by GitHub
parent 07205e4249
commit 75c5d5ad9e
31 changed files with 1346 additions and 2400 deletions

View File

@@ -1,18 +1,19 @@
import { Tooltip } from "@medusajs/ui"
import { PropsWithChildren, ReactNode } from "react"
import { ComponentPropsWithoutRef, PropsWithChildren } from "react"
type ConditionalTooltipProps = PropsWithChildren<{
content: ReactNode
showTooltip?: boolean
}>
type ConditionalTooltipProps = PropsWithChildren<
ComponentPropsWithoutRef<typeof Tooltip> & {
showTooltip?: boolean
}
>
export const ConditionalTooltip = ({
children,
content,
showTooltip = false,
...props
}: ConditionalTooltipProps) => {
if (showTooltip) {
return <Tooltip content={content}>{children}</Tooltip>
return <Tooltip {...props}>{children}</Tooltip>
}
return children

View File

@@ -292,7 +292,7 @@ const GridInput = forwardRef<
{...props}
autoComplete="off"
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
)}
/>

View File

@@ -1,15 +1,20 @@
import {
BuildingStorefront,
Buildings,
ChevronDownMini,
CogSixTooth,
CurrencyDollar,
EllipsisHorizontal,
MagnifyingGlass,
MinusMini,
OpenRectArrowOut,
ReceiptPercent,
ShoppingCart,
SquaresPlus,
Tag,
Users,
} 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 { useTranslation } from "react-i18next"
@@ -20,7 +25,12 @@ import { Skeleton } from "../../common/skeleton"
import { NavItem, NavItemProps } from "../../layout/nav-item"
import { Shell } from "../../layout/shell"
import { Link, useLocation, useNavigate } from "react-router-dom"
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 = () => {
return (
@@ -40,41 +50,129 @@ const MainSidebar = () => {
<Divider variant="dashed" />
</div>
</div>
<CoreRouteSection />
<ExtensionRouteSection />
<div className="flex flex-1 flex-col justify-between">
<div className="flex flex-1 flex-col">
<CoreRouteSection />
<ExtensionRouteSection />
</div>
<UtilitySection />
</div>
<div className="bg-ui-bg-subtle sticky bottom-0">
<UserSection />
</div>
</div>
</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 { store, isError, error } = useStore()
const { t } = useTranslation()
const { store, isPending, isError, error } = useStore()
const name = store?.name
const fallback = store?.name?.slice(0, 1).toUpperCase()
const isLoaded = !isPending && !!store && !!name && !!fallback
if (isError) {
throw error
}
return (
<div className="w-full px-3 py-2">
<div className="flex items-center p-1 md:pr-2">
<div className="flex items-center gap-x-3">
<div className="w-full p-3">
<DropdownMenu>
<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 ? (
<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 ? (
<Text size="small" weight="plus" leading="compact">
{store.name}
</Text>
) : (
<Skeleton className="h-[9px] w-[120px]" />
)}
</div>
</div>
<div className="block overflow-hidden">
{name ? (
<Text
size="small"
weight="plus"
leading="compact"
className="truncate"
>
{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>
)
}
@@ -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 coreRoutes = useCoreRoutes()
return (
<nav className="flex flex-col gap-y-1 py-3">
<Searchbar />
{coreRoutes.map((route) => {
return <NavItem key={route.to} {...route} />
})}
@@ -192,7 +319,7 @@ const ExtensionRouteSection = () => {
<Collapsible.Trigger asChild className="group/trigger">
<button className="text-ui-fg-subtle flex w-full items-center justify-between px-2">
<Text size="xsmall" weight="plus" leading="compact">
{t("nav.extensions")}
{t("app.nav.common.extensions")}
</Text>
<div className="text-ui-fg-muted">
<ChevronDownMini className="group-data-[state=open]/trigger:hidden" />
@@ -202,7 +329,7 @@ const ExtensionRouteSection = () => {
</Collapsible.Trigger>
</div>
<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) => {
return (
<NavItem
@@ -214,10 +341,37 @@ const ExtensionRouteSection = () => {
/>
)
})}
</div>
</nav>
</Collapsible.Content>
</Collapsible.Root>
</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>
)
}

View File

@@ -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 { ReactNode, useEffect, useState } from "react"
import { Link, useLocation } from "react-router-dom"
import {
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 = {
label: string
@@ -19,6 +28,60 @@ export type NavItemProps = {
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 = ({
icon,
label,
@@ -27,111 +90,125 @@ export const NavItem = ({
type = "core",
from,
}: NavItemProps) => {
const location = useLocation()
const [open, setOpen] = useState(
[to, ...(items?.map((i) => i.to) ?? [])].some((p) =>
location.pathname.startsWith(p)
)
)
const { pathname } = useLocation()
const [open, setOpen] = useState(getIsOpen(to, items, pathname))
useEffect(() => {
setOpen(
[to, ...(items?.map((i) => i.to) ?? [])].some((p) =>
location.pathname.startsWith(p)
)
)
}, [location.pathname, to, items])
setOpen(getIsOpen(to, items, pathname))
}, [pathname, to, items])
const navLinkClassNames = useCallback(
({
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 (
<div className="px-3">
<Link
to={to}
state={
from
? {
from,
}
: 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,
<NavItemTooltip to={to}>
<NavLink
to={to}
state={
from
? {
from,
}
: undefined
}
)}
>
<Icon icon={icon} type={type} />
<Text size="small" weight="plus" leading="compact">
{label}
</Text>
</Link>
className={(props) =>
clx(navLinkClassNames({ ...props, isSetting }), {
"max-lg:hidden": !!items?.length,
})
}
>
{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 && (
<Collapsible.Root open={open} onOpenChange={setOpen}>
<Collapsible.Trigger
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">
{label}
</Text>
</Collapsible.Trigger>
<Collapsible.Content className="flex flex-col pt-1">
<div className="flex w-full items-center gap-x-1 pl-2 md:hidden">
<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={to}
className={clx(
"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",
{
"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),
}
)}
<Collapsible.Content>
<div className="flex flex-col gap-y-0.5 pb-2 pt-0.5">
<ul className="flex flex-col gap-y-0.5">
<li className="flex w-full items-center gap-x-1 lg:hidden">
<NavItemTooltip to={to}>
<NavLink
to={to}
className={(props) =>
clx(
navLinkClassNames({
...props,
isNested: true,
isSetting,
})
)
}
>
<Text size="small" weight="plus" leading="compact">
{item.label}
{label}
</Text>
</Link>
</li>
)
})}
</ul>
</NavLink>
</NavItemTooltip>
</li>
{items.map((item) => {
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.Root>
)}

View File

@@ -128,7 +128,7 @@ const Sidebar = ({
return (
<div
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
)}
{...props}

View File

@@ -1,5 +1,5 @@
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 { Fragment, useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
@@ -11,6 +11,7 @@ import { NavItem, NavItemProps } from "../nav-item"
import { Shell } from "../shell"
import routes from "virtual:medusa/routes/links"
import { UserMenu } from "../user-menu"
export const SettingsLayout = () => {
return (
@@ -25,10 +26,6 @@ const useSettingRoutes = (): NavItemProps[] => {
return useMemo(
() => [
{
label: t("profile.domain"),
to: "/settings/profile",
},
{
label: t("store.domain"),
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 links = routes.links
@@ -115,12 +126,64 @@ const SettingsSidebar = () => {
const routes = useSettingRoutes()
const developerRoutes = useDeveloperRoutes()
const extensionRoutes = useExtensionRoutes()
const myAccountRoutes = useMyAccountRoutes()
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 { t } = useTranslation()
const location = useLocation()
useEffect(() => {
if (location.state?.from) {
setFrom(getSafeFromValue(location.state.from))
@@ -128,107 +191,70 @@ const SettingsSidebar = () => {
}, [location])
return (
<aside className="flex flex-1 flex-col justify-between overflow-y-auto">
<div className="p-3">
<div className="flex items-center gap-x-3 px-2 py-1.5">
<IconButton size="2xsmall" variant="transparent" asChild>
<Link
to={from}
replace
className="flex items-center justify-center"
>
<ArrowUturnLeft />
</Link>
</IconButton>
<div className="bg-ui-bg-subtle p-3">
<Link
to={from}
replace
className={clx(
"bg-ui-bg-subtle transition-fg flex items-center rounded-md outline-none",
"hover:bg-ui-bg-subtle-hover",
"focus-visible:shadow-borders-focus"
)}
>
<div className="flex items-center gap-x-2.5 px-2 py-1">
<div className="flex items-center justify-center">
<ArrowUturnLeft className="text-ui-fg-subtle" />
</div>
<Text leading="compact" weight="plus" size="small">
{t("nav.settings")}
{t("app.nav.settings.header")}
</Text>
</div>
</div>
<div className="flex items-center justify-center px-3">
<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>
</aside>
</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>
<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" />
</div>
<UserMenu />
</div>
)
}

View File

@@ -1,48 +1,19 @@
import * as Dialog from "@radix-ui/react-dialog"
import {
ArrowRightOnRectangle,
BellAlert,
BookOpen,
Calendar,
CircleHalfSolid,
CogSixTooth,
Keyboard,
MagnifyingGlass,
SidebarLeft,
TriangleRightMini,
User as UserIcon,
XMark,
} from "@medusajs/icons"
import {
Avatar,
Button,
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 { IconButton, clx } from "@medusajs/ui"
import { PropsWithChildren } from "react"
import { Link, Outlet, UIMatch, useMatches } from "react-router-dom"
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 { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks"
import { useSearch } from "../../../providers/search-provider"
import { useSidebar } from "../../../providers/sidebar-provider"
import { useTheme } from "../../../providers/theme-provider"
import { Skeleton } from "../../common/skeleton"
export const Shell = ({ children }: PropsWithChildren) => {
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 = () => {
return (
<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 { toggle } = useSidebar()
@@ -410,6 +128,7 @@ const ToggleSidebar = () => {
className="hidden lg:flex"
variant="transparent"
onClick={() => toggle("desktop")}
size="small"
>
<SidebarLeft className="text-ui-fg-muted" />
</IconButton>
@@ -417,6 +136,7 @@ const ToggleSidebar = () => {
className="hidden max-lg:flex"
variant="transparent"
onClick={() => toggle("mobile")}
size="small"
>
<SidebarLeft className="text-ui-fg-muted" />
</IconButton>
@@ -426,20 +146,13 @@ const ToggleSidebar = () => {
const Topbar = () => {
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">
<ToggleSidebar />
<Breadcrumbs />
</div>
<div className="flex items-center justify-center">
<Searchbar />
</div>
<div className="flex items-center justify-end gap-x-3">
<div className="text-ui-fg-muted flex items-center gap-x-1">
<ToggleNotifications />
<SettingsLink />
</div>
<LoggedInUser />
<ToggleNotifications />
</div>
</div>
)
@@ -460,13 +173,41 @@ const DesktopSidebarContainer = ({ children }: PropsWithChildren) => {
}
const MobileSidebarContainer = ({ children }: PropsWithChildren) => {
const { t } = useTranslation()
const { mobile, toggle } = useSidebar()
return (
<Dialog.Root open={mobile} onOpenChange={() => toggle("mobile")}>
<Dialog.Portal>
<Dialog.Overlay className="bg-ui-bg-overlay fixed inset-0" />
<Dialog.Content className="bg-ui-bg-subtle fixed inset-y-0 left-0 h-screen w-full max-w-[240px] border-r">
<Dialog.Overlay
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}
</Dialog.Content>
</Dialog.Portal>

View File

@@ -0,0 +1 @@
export * from "./user-menu"

View File

@@ -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>
)
}

View File

@@ -10,7 +10,7 @@ import {
} from "react"
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 { useGlobalShortcuts } from "../../providers/keybind-provider/hooks"
import { useSearch } from "../../providers/search-provider"
@@ -20,6 +20,7 @@ export const Search = () => {
const globalCommands = useGlobalShortcuts()
const location = useLocation()
const { t } = useTranslation()
const navigate = useNavigate()
useEffect(() => {
onOpenChange(false)
@@ -40,9 +41,18 @@ export const Search = () => {
}))
}, [globalCommands])
const handleSelect = (callback: () => void) => {
callback()
const handleSelect = (shortcut: Shortcut) => {
onOpenChange(false)
if (shortcut.to) {
navigate(shortcut.to)
return
}
if (shortcut.callback) {
shortcut.callback()
return
}
}
return (
@@ -60,13 +70,25 @@ export const Search = () => {
return (
<CommandItem
key={item.label}
onSelect={() => handleSelect(item.callback)}
onSelect={() => handleSelect(item)}
className="flex items-center justify-between"
>
<span>{item.label}</span>
<div className="flex items-center gap-x-1.5">
{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>
</CommandItem>
@@ -104,8 +126,8 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
<Dialog.Root {...props}>
<Dialog.Portal>
<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">
<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]">
<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 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}
</CommandPalette>
<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
ref={ref}
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
)}
{...props}

View File

@@ -14,7 +14,7 @@ import {
useState,
} from "react"
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"
type BulkCommand = {
@@ -83,7 +83,6 @@ export const DataTableRoot = <TData,>({
layout = "fit",
}: DataTableRootProps<TData>) => {
const { t } = useTranslation()
const navigate = useNavigate()
const [showStickyBorder, setShowStickyBorder] = useState(false)
const scrollableRef = useRef<HTMLDivElement>(null)
@@ -204,6 +203,13 @@ export const DataTableRoot = <TData,>({
return (
<Table.Row
key={row.id}
onKeyDown={(e) => {
console.log("e.key", e.key, e.target)
if (e.key === "x") {
row.toggleSelected()
}
}}
data-selected={row.getIsSelected()}
className={clx(
"transition-fg group/row group relative [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",