feat(dashboard): Add global commands (#7782)
* add global commands * update lock * shorten keybinds --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
co-authored by
Oli Juhl
parent
27bb93c5b5
commit
aee75f6ba0
@@ -7,12 +7,22 @@ import {
|
|||||||
Calendar,
|
Calendar,
|
||||||
CircleHalfSolid,
|
CircleHalfSolid,
|
||||||
CogSixTooth,
|
CogSixTooth,
|
||||||
|
Keyboard,
|
||||||
MagnifyingGlass,
|
MagnifyingGlass,
|
||||||
SidebarLeft,
|
SidebarLeft,
|
||||||
User as UserIcon,
|
User as UserIcon,
|
||||||
} from "@medusajs/icons"
|
} from "@medusajs/icons"
|
||||||
import { Avatar, DropdownMenu, IconButton, Kbd, Text, clx } from "@medusajs/ui"
|
import {
|
||||||
import { PropsWithChildren } from "react"
|
Avatar,
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Kbd,
|
||||||
|
Text,
|
||||||
|
clx,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import { PropsWithChildren, useState } from "react"
|
||||||
import {
|
import {
|
||||||
Link,
|
Link,
|
||||||
Outlet,
|
Outlet,
|
||||||
@@ -22,31 +32,37 @@ import {
|
|||||||
useNavigate,
|
useNavigate,
|
||||||
} from "react-router-dom"
|
} from "react-router-dom"
|
||||||
|
|
||||||
import { Skeleton } from "../../common/skeleton"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { useLogout } from "../../../hooks/api/auth"
|
import { useLogout } from "../../../hooks/api/auth"
|
||||||
import { useMe } from "../../../hooks/api/users"
|
import { useMe } from "../../../hooks/api/users"
|
||||||
import { queryClient } from "../../../lib/query-client"
|
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 { useSearch } from "../../../providers/search-provider"
|
||||||
import { useSidebar } from "../../../providers/sidebar-provider"
|
import { useSidebar } from "../../../providers/sidebar-provider"
|
||||||
import { useTheme } from "../../../providers/theme-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()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col items-start overflow-hidden lg:flex-row">
|
<KeybindProvider shortcuts={globalShortcuts}>
|
||||||
<div>
|
<div className="flex h-screen flex-col items-start overflow-hidden lg:flex-row">
|
||||||
<MobileSidebarContainer>{children}</MobileSidebarContainer>
|
<div>
|
||||||
<DesktopSidebarContainer>{children}</DesktopSidebarContainer>
|
<MobileSidebarContainer>{children}</MobileSidebarContainer>
|
||||||
|
<DesktopSidebarContainer>{children}</DesktopSidebarContainer>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-screen w-full flex-col overflow-auto">
|
||||||
|
<Topbar />
|
||||||
|
<main className="flex h-full w-full flex-col items-center overflow-y-auto">
|
||||||
|
<Gutter>
|
||||||
|
<Outlet />
|
||||||
|
</Gutter>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-screen w-full flex-col overflow-auto">
|
</KeybindProvider>
|
||||||
<Topbar />
|
|
||||||
<main className="flex h-full w-full flex-col items-center overflow-y-auto">
|
|
||||||
<Gutter>
|
|
||||||
<Outlet />
|
|
||||||
</Gutter>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +222,7 @@ const Logout = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { mutateAsync: logoutMutation } = useLogout()
|
const { mutateAsync: logoutMutation } = useLogout()
|
||||||
|
|
||||||
const handleLayout = async () => {
|
const handleLogout = async () => {
|
||||||
await logoutMutation(undefined, {
|
await logoutMutation(undefined, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
/**
|
/**
|
||||||
@@ -219,7 +235,7 @@ const Logout = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Item onClick={handleLayout}>
|
<DropdownMenu.Item onClick={handleLogout}>
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<ArrowRightOnRectangle className="text-ui-fg-subtle" />
|
<ArrowRightOnRectangle className="text-ui-fg-subtle" />
|
||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
@@ -241,35 +257,95 @@ const Profile = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoggedInUser = () => {
|
const GlobalKeybindsModal = (props: {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const globalShortcuts = useGlobalShortcuts()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<Dialog.Root {...props}>
|
||||||
<UserBadge />
|
<Dialog.Portal>
|
||||||
<DropdownMenu.Content align="center">
|
<Dialog.Overlay className="bg-ui-bg-overlay fixed inset-0" />
|
||||||
<Profile />
|
<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">
|
||||||
<DropdownMenu.Separator />
|
<div className="px-6 py-4">
|
||||||
<Link
|
<Dialog.Title asChild>
|
||||||
// TODO change link once docs are public
|
<Heading>Keyboard Shortcuts</Heading>
|
||||||
to="https://medusa-docs-v2-git-docs-v2-medusajs.vercel.app/"
|
</Dialog.Title>
|
||||||
target="_blank"
|
</div>
|
||||||
>
|
<div className="flex flex-col divide-y overflow-y-auto">
|
||||||
<DropdownMenu.Item>
|
{globalShortcuts.map((shortcut, index) => {
|
||||||
<BookOpen className="text-ui-fg-subtle mr-2" />
|
return (
|
||||||
Documentation
|
<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
|
||||||
|
// TODO change link once docs are public
|
||||||
|
to="https://medusa-docs-v2-git-docs-v2-medusajs.vercel.app/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<BookOpen className="text-ui-fg-subtle mr-2" />
|
||||||
|
Documentation
|
||||||
|
</Link>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</Link>
|
<DropdownMenu.Item asChild>
|
||||||
<Link to="https://medusajs.com/changelog/" target="_blank">
|
<Link to="https://medusajs.com/changelog/" target="_blank">
|
||||||
<DropdownMenu.Item>
|
<Calendar className="text-ui-fg-subtle mr-2" />
|
||||||
<Calendar className="text-ui-fg-subtle mr-2" />
|
Changelog
|
||||||
Changelog
|
</Link>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</Link>
|
<DropdownMenu.Item onClick={toggleModal}>
|
||||||
<DropdownMenu.Separator />
|
<Keyboard className="text-ui-fg-subtle mr-2" />
|
||||||
<ThemeToggle />
|
Keyboard shortcuts
|
||||||
<DropdownMenu.Separator />
|
</DropdownMenu.Item>
|
||||||
<Logout />
|
<DropdownMenu.Separator />
|
||||||
</DropdownMenu.Content>
|
<ThemeToggle />
|
||||||
</DropdownMenu>
|
<DropdownMenu.Separator />
|
||||||
|
<Logout />
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
<GlobalKeybindsModal open={openModal} onOpenChange={setOpenModal} />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,6 +382,7 @@ const ToggleNotifications = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Searchbar = () => {
|
const Searchbar = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { toggleSearch } = useSearch()
|
const { toggleSearch } = useSearch()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -316,7 +393,7 @@ const Searchbar = () => {
|
|||||||
<MagnifyingGlass />
|
<MagnifyingGlass />
|
||||||
<div className="flex-1 text-left">
|
<div className="flex-1 text-left">
|
||||||
<Text size="small" leading="compact">
|
<Text size="small" leading="compact">
|
||||||
Jump to or search
|
{t("app.search.placeholder")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Kbd className="rounded-full">⌘K</Kbd>
|
<Kbd className="rounded-full">⌘K</Kbd>
|
||||||
|
|||||||
@@ -1,34 +1,74 @@
|
|||||||
import { MagnifyingGlass } from "@medusajs/icons"
|
import { Badge, Kbd, Text, clx } from "@medusajs/ui"
|
||||||
import { Kbd, Text, clx } from "@medusajs/ui"
|
|
||||||
import * as Dialog from "@radix-ui/react-dialog"
|
import * as Dialog from "@radix-ui/react-dialog"
|
||||||
import { Command } from "cmdk"
|
import { Command } from "cmdk"
|
||||||
import {
|
import {
|
||||||
ComponentPropsWithoutRef,
|
ComponentPropsWithoutRef,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
HTMLAttributes,
|
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
} from "react"
|
} from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useLocation } from "react-router-dom"
|
||||||
|
import { Shortcut, ShortcutType } from "../../providers/keybind-provider"
|
||||||
|
import { useGlobalShortcuts } from "../../providers/keybind-provider/hooks"
|
||||||
import { useSearch } from "../../providers/search-provider"
|
import { useSearch } from "../../providers/search-provider"
|
||||||
|
|
||||||
export const Search = () => {
|
export const Search = () => {
|
||||||
const { open, onOpenChange } = useSearch()
|
const { open, onOpenChange } = useSearch()
|
||||||
const links = useLinks()
|
const globalCommands = useGlobalShortcuts()
|
||||||
|
const location = useLocation()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onOpenChange(false)
|
||||||
|
}, [location.pathname, onOpenChange])
|
||||||
|
|
||||||
|
const links = useMemo(() => {
|
||||||
|
const groups = new Map<ShortcutType, Shortcut[]>()
|
||||||
|
|
||||||
|
globalCommands.forEach((command) => {
|
||||||
|
const group = groups.get(command.type) || []
|
||||||
|
group.push(command)
|
||||||
|
groups.set(command.type, group)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(groups).map(([title, items]) => ({
|
||||||
|
title,
|
||||||
|
items,
|
||||||
|
}))
|
||||||
|
}, [globalCommands])
|
||||||
|
|
||||||
|
const handleSelect = (callback: () => void) => {
|
||||||
|
callback()
|
||||||
|
onOpenChange(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog open={open} onOpenChange={onOpenChange}>
|
<CommandDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<CommandInput placeholder="Search" />
|
<CommandInput placeholder={t("app.search.placeholder")} />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>{t("general.noResultsTitle")}</CommandEmpty>
|
||||||
{links.map((group) => {
|
{links.map((group) => {
|
||||||
return (
|
return (
|
||||||
<CommandGroup key={group.title} heading={group.title}>
|
<CommandGroup
|
||||||
|
key={group.title}
|
||||||
|
heading={t(`app.keyboardShortcuts.${group.title}`)}
|
||||||
|
>
|
||||||
{group.items.map((item) => {
|
{group.items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem key={item.label}>
|
<CommandItem
|
||||||
|
key={item.label}
|
||||||
|
onSelect={() => handleSelect(item.callback)}
|
||||||
|
className="flex items-center justify-between"
|
||||||
|
>
|
||||||
<span>{item.label}</span>
|
<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>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -40,86 +80,6 @@ export const Search = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandItemProps = {
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommandGroupProps = {
|
|
||||||
title: string
|
|
||||||
items: CommandItemProps[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const useLinks = (): CommandGroupProps[] => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
title: "Pages",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: t("products.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("categories.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("collections.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("giftCards.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("orders.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("draftOrders.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("customers.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("customerGroups.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("discounts.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("pricing.domain"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Settings",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: t("profile.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("store.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("users.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("regions.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("locations.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("salesChannels.domain"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("apiKeyManagement.domainTitle"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CommandPalette = forwardRef<
|
const CommandPalette = forwardRef<
|
||||||
ElementRef<typeof Command>,
|
ElementRef<typeof Command>,
|
||||||
ComponentPropsWithoutRef<typeof Command>
|
ComponentPropsWithoutRef<typeof Command>
|
||||||
@@ -133,36 +93,38 @@ const CommandPalette = forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
Command.displayName = Command.displayName
|
CommandPalette.displayName = Command.displayName
|
||||||
|
|
||||||
interface CommandDialogProps extends Dialog.DialogProps {}
|
interface CommandDialogProps extends Dialog.DialogProps {}
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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%] 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-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
<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]">
|
||||||
{children}
|
{children}
|
||||||
</CommandPalette>
|
</CommandPalette>
|
||||||
<div className="flex items-center justify-between border-t px-4 pb-4 pt-3">
|
<div className="bg-ui-bg-field text-ui-fg-subtle flex items-center justify-end border-t px-4 py-3">
|
||||||
<div></div>
|
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<Text size="xsmall" leading="compact">
|
<Text size="xsmall" leading="compact">
|
||||||
Navigation
|
{t("app.search.navigation")}
|
||||||
</Text>
|
</Text>
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<Kbd>↓</Kbd>
|
<Kbd className="bg-ui-bg-field-component">↓</Kbd>
|
||||||
<Kbd>↑</Kbd>
|
<Kbd className="bg-ui-bg-field-component">↑</Kbd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bg-ui-border-strong h-3 w-px" />
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<Text size="xsmall" leading="compact">
|
<Text size="xsmall" leading="compact">
|
||||||
Open Result
|
{t("app.search.openResult")}
|
||||||
</Text>
|
</Text>
|
||||||
<Kbd>↵</Kbd>
|
<Kbd className="bg-ui-bg-field-component">↵</Kbd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,19 +137,26 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
|||||||
const CommandInput = forwardRef<
|
const CommandInput = forwardRef<
|
||||||
ElementRef<typeof Command.Input>,
|
ElementRef<typeof Command.Input>,
|
||||||
ComponentPropsWithoutRef<typeof Command.Input>
|
ComponentPropsWithoutRef<typeof Command.Input>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => {
|
||||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
const { t } = useTranslation()
|
||||||
<MagnifyingGlass className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
<Command.Input
|
return (
|
||||||
ref={ref}
|
<div className="flex flex-col border-b">
|
||||||
className={clx(
|
<div className="px-4 pt-4">
|
||||||
"placeholder:text-ui-fg-muted flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
{/* TODO: Add filter once we have search engine */}
|
||||||
className
|
<Badge size="2xsmall">{t("app.search.allAreas")}</Badge>
|
||||||
)}
|
</div>
|
||||||
{...props}
|
<Command.Input
|
||||||
/>
|
ref={ref}
|
||||||
</div>
|
className={clx(
|
||||||
))
|
"placeholder:text-ui-fg-muted flex h-10 w-full rounded-md bg-transparent p-4 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
CommandInput.displayName = Command.Input.displayName
|
CommandInput.displayName = Command.Input.displayName
|
||||||
|
|
||||||
@@ -198,7 +167,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 pb-4",
|
"max-h-[300px] overflow-y-auto overflow-x-hidden px-2 pb-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -223,7 +192,7 @@ const CommandGroup = forwardRef<
|
|||||||
<Command.Group
|
<Command.Group
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clx(
|
className={clx(
|
||||||
"text-ui-fg-base [&_[cmdk-group-heading]]:text-ui-fg-muted [&_[cmdk-group-heading]]:txt-compact-xsmall-plus overflow-hidden px-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:pb-1 [&_[cmdk-group-heading]]:pt-4 [&_[cmdk-item]]:py-2",
|
"text-ui-fg-base [&_[cmdk-group-heading]]:text-ui-fg-muted [&_[cmdk-group-heading]]:txt-compact-xsmall-plus overflow-hidden [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:pb-1 [&_[cmdk-group-heading]]:pt-4 [&_[cmdk-item]]:py-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -251,7 +220,7 @@ const CommandItem = forwardRef<
|
|||||||
<Command.Item
|
<Command.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clx(
|
className={clx(
|
||||||
"aria-selected:bg-ui-bg-base-hover hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-hover txt-compact-small relative flex cursor-default select-none items-center rounded-md p-2 outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"aria-selected:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-hover txt-compact-small [&>svg]:text-ui-fg-subtle relative flex cursor-pointer select-none items-center gap-x-3 rounded-md p-2 outline-none data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -259,19 +228,3 @@ const CommandItem = forwardRef<
|
|||||||
))
|
))
|
||||||
|
|
||||||
CommandItem.displayName = Command.Item.displayName
|
CommandItem.displayName = Command.Item.displayName
|
||||||
|
|
||||||
const CommandShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={clx(
|
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
CommandShortcut.displayName = "CommandShortcut"
|
|
||||||
|
|||||||
@@ -85,7 +85,42 @@
|
|||||||
"clearAll": "Clear all",
|
"clearAll": "Clear all",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"select": "Select"
|
"select": "Select",
|
||||||
|
"logout": "Logout"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"search": {
|
||||||
|
"allAreas": "All areas",
|
||||||
|
"navigation": "Navigation",
|
||||||
|
"openResult": "Open result",
|
||||||
|
"placeholder": "Jump to or find anything..."
|
||||||
|
},
|
||||||
|
"keyboardShortcuts": {
|
||||||
|
"pageShortcut": "Jump to",
|
||||||
|
"settingShortcut": "Settings",
|
||||||
|
"commandShortcut": "Commands",
|
||||||
|
"goToOrders": "Go to orders",
|
||||||
|
"goToProducts": "Go to products",
|
||||||
|
"goToCollections": "Go to collections",
|
||||||
|
"goToCategories": "Go to categories",
|
||||||
|
"goToCustomers": "Go to customers",
|
||||||
|
"goToCustomerGroups": "Go to customer groups",
|
||||||
|
"goToInventory": "Go to inventory",
|
||||||
|
"goToReservations": "Go to reservations",
|
||||||
|
"goToPriceLists": "Go to price lists",
|
||||||
|
"goToPromotions": "Go to promotions",
|
||||||
|
"goToCampaigns": "Go to campaigns",
|
||||||
|
"goToStore": "Go to store",
|
||||||
|
"goToUsers": "Go to users",
|
||||||
|
"goToRegions": "Go to regions",
|
||||||
|
"goToTaxRegions": "Go to tax region",
|
||||||
|
"goToSalesChannels": "Go to sales channels",
|
||||||
|
"goToProductTypes": "Go to product types",
|
||||||
|
"goToLocations": "Go to locations",
|
||||||
|
"goToPublishableApiKeys": "Go to publishable API keys",
|
||||||
|
"goToSecretApiKeys": "Go to secret API keys",
|
||||||
|
"goToWorkflows": "Go to workflows"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"date": {
|
"date": {
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
import debounceFn from "lodash/debounce"
|
||||||
|
import { useCallback, useContext, useEffect, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
|
import { useLogout } from "../../hooks/api/auth"
|
||||||
|
import { queryClient } from "../../lib/query-client"
|
||||||
|
import { KeybindContext } from "./keybind-context"
|
||||||
|
import { Shortcut } from "./types"
|
||||||
|
import { findShortcut } from "./utils"
|
||||||
|
|
||||||
|
export const useKeybind = () => {
|
||||||
|
const context = useContext(KeybindContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useKeybind must be used within a KeybindProvider")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRegisterShortcut = () => {}
|
||||||
|
|
||||||
|
export const useShortcuts = ({
|
||||||
|
shortcuts = [],
|
||||||
|
debounce,
|
||||||
|
}: {
|
||||||
|
shortcuts?: Shortcut[]
|
||||||
|
debounce: number
|
||||||
|
}) => {
|
||||||
|
const [keys, setKeys] = useState<string[]>([])
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const removeKeys = useCallback(
|
||||||
|
debounceFn(() => setKeys([]), debounce),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const invokeShortcut = useCallback(
|
||||||
|
debounceFn((shortcut: Shortcut | null) => {
|
||||||
|
if (shortcut && shortcut.callback) {
|
||||||
|
shortcut.callback()
|
||||||
|
setKeys([])
|
||||||
|
}
|
||||||
|
}, debounce / 2),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (keys.length > 0 && shortcuts.length > 0) {
|
||||||
|
const shortcut = findShortcut(shortcuts, keys)
|
||||||
|
invokeShortcut(shortcut)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => invokeShortcut.cancel()
|
||||||
|
}, [keys, shortcuts, invokeShortcut])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (event: KeyboardEvent) => {
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ignore key events from input, textarea and contenteditable elements
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
target.tagName === "INPUT" ||
|
||||||
|
target.tagName === "TEXTAREA" ||
|
||||||
|
target.contentEditable === "true"
|
||||||
|
) {
|
||||||
|
removeKeys()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setKeys((oldKeys) => [...oldKeys, event.key])
|
||||||
|
removeKeys()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", listener)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", listener)
|
||||||
|
}
|
||||||
|
}, [removeKeys])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGlobalShortcuts = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { mutateAsync } = useLogout()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await mutateAsync(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.clear()
|
||||||
|
navigate("/login")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalShortcuts: Shortcut[] = [
|
||||||
|
// Pages
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "O"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToOrders"),
|
||||||
|
type: "pageShortcut",
|
||||||
|
callback: () => navigate("/orders"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "P"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToProducts"),
|
||||||
|
type: "pageShortcut",
|
||||||
|
callback: () => navigate("/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: {
|
||||||
|
Mac: ["G", "C"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToCustomers"),
|
||||||
|
type: "pageShortcut",
|
||||||
|
callback: () => navigate("/customers"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "C", "G"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToCustomerGroups"),
|
||||||
|
type: "pageShortcut",
|
||||||
|
callback: () => navigate("/customer-groups"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "I"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToInventory"),
|
||||||
|
type: "pageShortcut",
|
||||||
|
callback: () => navigate("/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("/pricing"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "R"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToPromotions"),
|
||||||
|
type: "pageShortcut",
|
||||||
|
callback: () => navigate("/promotions"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "R", "C"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToCampaigns"),
|
||||||
|
type: "pageShortcut",
|
||||||
|
callback: () => navigate("/campaigns"),
|
||||||
|
},
|
||||||
|
//
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "S", "S"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToStore"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
callback: () => navigate("/settings/store"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "S", "U"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToUsers"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
callback: () => navigate("/settings/users"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "S", "R"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToRegions"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
callback: () => navigate("/settings/regions"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "S", "T"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToTaxRegions"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
callback: () => navigate("/settings/taxes"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "S", "A"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToSalesChannels"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
callback: () => navigate("/settings/sales-channels"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "S", "P"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToProductTypes"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
callback: () => navigate("/settings/product-types"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "S", "L"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToLocations"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
callback: () => navigate("/settings/locations"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "S", "J"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToPublishableApiKeys"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
callback: () => navigate("/settings/publishable-api-keys"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "S", "K"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToSecretApiKeys"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
callback: () => navigate("/settings/secret-api-keys"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["G", "S", "W"],
|
||||||
|
},
|
||||||
|
label: t("app.keyboardShortcuts.goToWorkflows"),
|
||||||
|
type: "settingShortcut",
|
||||||
|
callback: () => navigate("/settings/workflows"),
|
||||||
|
},
|
||||||
|
// Commands
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
Mac: ["B", "Y", "E"],
|
||||||
|
},
|
||||||
|
label: t("actions.logout"),
|
||||||
|
type: "commandShortcut",
|
||||||
|
callback: () => handleLogout(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return globalShortcuts
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { useRegisterShortcut } from "./hooks"
|
||||||
|
export * from "./keybind-provider"
|
||||||
|
export type { Shortcut, ShortcutType } from "./types"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { createContext } from "react"
|
||||||
|
import { KeybindContextState } from "./types"
|
||||||
|
|
||||||
|
export const KeybindContext = createContext<KeybindContextState | null>(null)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { PropsWithChildren, useCallback, useMemo, useState } from "react"
|
||||||
|
|
||||||
|
import { useShortcuts } from "./hooks"
|
||||||
|
import { KeybindContext } from "./keybind-context"
|
||||||
|
import { KeybindContextState, Shortcut } from "./types"
|
||||||
|
import {
|
||||||
|
findFirstPlatformMatch,
|
||||||
|
findShortcutIndex,
|
||||||
|
getShortcutKeys,
|
||||||
|
getShortcutWithDefaultValues,
|
||||||
|
} from "./utils"
|
||||||
|
|
||||||
|
type KeybindProviderProps = PropsWithChildren<{
|
||||||
|
shortcuts: Shortcut[]
|
||||||
|
debounce?: number
|
||||||
|
}>
|
||||||
|
|
||||||
|
export const KeybindProvider = ({
|
||||||
|
shortcuts,
|
||||||
|
debounce = 500,
|
||||||
|
children,
|
||||||
|
}: KeybindProviderProps) => {
|
||||||
|
const [storeShortcuts, setStoreCommands] = useState(
|
||||||
|
shortcuts.map((shr) => getShortcutWithDefaultValues(shr))
|
||||||
|
)
|
||||||
|
const registerShortcut = useCallback(
|
||||||
|
(shortcut: Shortcut) => {
|
||||||
|
setStoreCommands((prevShortcuts) => {
|
||||||
|
const idx = findShortcutIndex(shortcuts, getShortcutKeys(shortcut))
|
||||||
|
|
||||||
|
const newShortcuts = [...prevShortcuts]
|
||||||
|
|
||||||
|
if (idx > -1) {
|
||||||
|
newShortcuts[idx] = getShortcutWithDefaultValues(shortcut)
|
||||||
|
return prevShortcuts
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...prevShortcuts, getShortcutWithDefaultValues(shortcut)]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[shortcuts]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getKeysByPlatform = useCallback((command: Shortcut) => {
|
||||||
|
return findFirstPlatformMatch(command.keys)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useShortcuts({ shortcuts: storeShortcuts, debounce })
|
||||||
|
|
||||||
|
const commandsContext = useMemo<KeybindContextState>(
|
||||||
|
() => ({
|
||||||
|
shortcuts: storeShortcuts,
|
||||||
|
registerShortcut,
|
||||||
|
getKeysByPlatform,
|
||||||
|
}),
|
||||||
|
[storeShortcuts, registerShortcut, getKeysByPlatform]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeybindContext.Provider value={commandsContext}>
|
||||||
|
{children}
|
||||||
|
</KeybindContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export type KeybindContextState = {}
|
||||||
|
|
||||||
|
export type Platform = "Mac" | "Windows" | "Linux"
|
||||||
|
|
||||||
|
export type Keys = {
|
||||||
|
[key in Platform]?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShortcutType =
|
||||||
|
| "pageShortcut"
|
||||||
|
| "settingShortcut"
|
||||||
|
| "commandShortcut"
|
||||||
|
|
||||||
|
export type Shortcut = {
|
||||||
|
keys: Keys
|
||||||
|
type: ShortcutType
|
||||||
|
label: string
|
||||||
|
callback: () => void
|
||||||
|
_defaultKeys?: Keys
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Keys, Platform, Shortcut } from "./types"
|
||||||
|
|
||||||
|
export const findFirstPlatformMatch = (keys: Keys) => {
|
||||||
|
const match =
|
||||||
|
Object.entries(keys as Record<any, any>).filter(
|
||||||
|
([, value]) => value.length > 0
|
||||||
|
)[0] ?? []
|
||||||
|
|
||||||
|
return match.length
|
||||||
|
? {
|
||||||
|
platform: match[0] as Platform,
|
||||||
|
keys: match[1] as string[],
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getShortcutKeys = (shortcut: Shortcut) => {
|
||||||
|
const platform: Platform = "Mac"
|
||||||
|
|
||||||
|
const keys: string[] | undefined = shortcut.keys[platform]
|
||||||
|
|
||||||
|
if (!keys) {
|
||||||
|
const defaultPlatform = findFirstPlatformMatch(shortcut.keys)
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
`No keys found for platform "${platform}" in "${shortcut.label}" ${
|
||||||
|
defaultPlatform
|
||||||
|
? `using keys for platform "${defaultPlatform.platform}"`
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
|
||||||
|
return defaultPlatform ? defaultPlatform.keys : []
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysMatch = (keys1: string[], keys2: string[]) => {
|
||||||
|
return (
|
||||||
|
keys1.length === keys2.length &&
|
||||||
|
keys1.every(
|
||||||
|
(key, index) => key.toLowerCase() === keys2[index].toLowerCase()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findShortcutIndex = (shortcuts: Shortcut[], keys: string[]) => {
|
||||||
|
if (!keys.length) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = 0
|
||||||
|
for (const shortcut of shortcuts) {
|
||||||
|
const shortcutKeys = getShortcutKeys(shortcut)
|
||||||
|
|
||||||
|
if (keysMatch(shortcutKeys, keys)) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findShortcut = (shortcuts: Shortcut[], keys: string[]) => {
|
||||||
|
const shortcutIndex = findShortcutIndex(shortcuts, keys)
|
||||||
|
return shortcutIndex > -1 ? shortcuts[shortcutIndex] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getShortcutWithDefaultValues = (
|
||||||
|
shortcut: Shortcut,
|
||||||
|
platform: Platform = "Mac"
|
||||||
|
): Shortcut => {
|
||||||
|
const platforms: Platform[] = ["Mac", "Windows", "Linux"]
|
||||||
|
|
||||||
|
const defaultKeys = Object.values(shortcut.keys)[0] ?? shortcut.keys[platform]
|
||||||
|
|
||||||
|
const keys = platforms.reduce((acc, curr) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[curr]: shortcut.keys[curr] ?? defaultKeys,
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...shortcut,
|
||||||
|
keys,
|
||||||
|
_defaultKeys: shortcut.keys,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user