diff --git a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx
index 21ddb5194a..e05442dfd8 100644
--- a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx
+++ b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx
@@ -7,12 +7,22 @@ import {
Calendar,
CircleHalfSolid,
CogSixTooth,
+ Keyboard,
MagnifyingGlass,
SidebarLeft,
User as UserIcon,
} from "@medusajs/icons"
-import { Avatar, DropdownMenu, IconButton, Kbd, Text, clx } from "@medusajs/ui"
-import { PropsWithChildren } from "react"
+import {
+ Avatar,
+ Button,
+ DropdownMenu,
+ Heading,
+ IconButton,
+ Kbd,
+ Text,
+ clx,
+} from "@medusajs/ui"
+import { PropsWithChildren, useState } from "react"
import {
Link,
Outlet,
@@ -22,31 +32,37 @@ import {
useNavigate,
} from "react-router-dom"
-import { Skeleton } from "../../common/skeleton"
-
+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()
+
return (
-
-
-
{children}
-
{children}
+
+
+
+ {children}
+ {children}
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
)
}
@@ -206,7 +222,7 @@ const Logout = () => {
const navigate = useNavigate()
const { mutateAsync: logoutMutation } = useLogout()
- const handleLayout = async () => {
+ const handleLogout = async () => {
await logoutMutation(undefined, {
onSuccess: () => {
/**
@@ -219,7 +235,7 @@ const Logout = () => {
}
return (
-
+
Logout
@@ -241,35 +257,95 @@ const Profile = () => {
)
}
-const LoggedInUser = () => {
+const GlobalKeybindsModal = (props: {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}) => {
+ const { t } = useTranslation()
+ const globalShortcuts = useGlobalShortcuts()
+
return (
-
-
-
-
-
-
-
-
- Documentation
+
+
+
+
+
+
+ Keyboard Shortcuts
+
+
+
+ {globalShortcuts.map((shortcut, index) => {
+ return (
+
+
{shortcut.label}
+
+ {shortcut.keys.Mac?.map((key, index) => {
+ return {key}
+ })}
+
+
+ )
+ })}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const LoggedInUser = () => {
+ const [openMenu, setOpenMenu] = useState(false)
+ const [openModal, setOpenModal] = useState(false)
+
+ const toggleModal = () => {
+ setOpenMenu(false)
+ setOpenModal(!openModal)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ Documentation
+
-
-
-
-
- Changelog
+
+
+
+ Changelog
+
-
-
-
-
-
-
-
+
+
+ Keyboard shortcuts
+
+
+
+
+
+
+
+
+
)
}
@@ -306,6 +382,7 @@ const ToggleNotifications = () => {
}
const Searchbar = () => {
+ const { t } = useTranslation()
const { toggleSearch } = useSearch()
return (
@@ -316,7 +393,7 @@ const Searchbar = () => {
- Jump to or search
+ {t("app.search.placeholder")}
⌘K
diff --git a/packages/admin-next/dashboard/src/components/search/search.tsx b/packages/admin-next/dashboard/src/components/search/search.tsx
index cb78cf7727..04cf9c5e7f 100644
--- a/packages/admin-next/dashboard/src/components/search/search.tsx
+++ b/packages/admin-next/dashboard/src/components/search/search.tsx
@@ -1,34 +1,74 @@
-import { MagnifyingGlass } from "@medusajs/icons"
-import { Kbd, Text, clx } from "@medusajs/ui"
+import { Badge, Kbd, Text, clx } from "@medusajs/ui"
import * as Dialog from "@radix-ui/react-dialog"
import { Command } from "cmdk"
import {
ComponentPropsWithoutRef,
ElementRef,
- HTMLAttributes,
forwardRef,
+ useEffect,
useMemo,
} 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"
export const Search = () => {
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()
+
+ 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 (
-
+
- No results found.
+ {t("general.noResultsTitle")}
{links.map((group) => {
return (
-
+
{group.items.map((item) => {
return (
-
+ handleSelect(item.callback)}
+ className="flex items-center justify-between"
+ >
{item.label}
+
+ {item.keys.Mac?.map((key, index) => {
+ return {key}
+ })}
+
)
})}
@@ -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<
ElementRef,
ComponentPropsWithoutRef
@@ -133,36 +93,38 @@ const CommandPalette = forwardRef<
{...props}
/>
))
-Command.displayName = Command.displayName
+CommandPalette.displayName = Command.displayName
interface CommandDialogProps extends Dialog.DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ const { t } = useTranslation()
+
return (
-
+
{children}
-
-
+
- Navigation
+ {t("app.search.navigation")}
- ↓
- ↑
+ ↓
+ ↑
+
- Open Result
+ {t("app.search.openResult")}
- ↵
+ ↵
@@ -175,19 +137,26 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
const CommandInput = forwardRef<
ElementRef
,
ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-
-))
+>(({ className, ...props }, ref) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {/* TODO: Add filter once we have search engine */}
+ {t("app.search.allAreas")}
+
+
+
+ )
+})
CommandInput.displayName = Command.Input.displayName
@@ -198,7 +167,7 @@ const CommandList = forwardRef<
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
)}
{...props}
@@ -259,19 +228,3 @@ const CommandItem = forwardRef<
))
CommandItem.displayName = Command.Item.displayName
-
-const CommandShortcut = ({
- className,
- ...props
-}: HTMLAttributes) => {
- return (
-
- )
-}
-CommandShortcut.displayName = "CommandShortcut"
diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json
index ee25945326..29e0e42fc1 100644
--- a/packages/admin-next/dashboard/src/i18n/translations/en.json
+++ b/packages/admin-next/dashboard/src/i18n/translations/en.json
@@ -85,7 +85,42 @@
"clearAll": "Clear all",
"apply": "Apply",
"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": {
"date": {
diff --git a/packages/admin-next/dashboard/src/providers/keybind-provider/hooks.tsx b/packages/admin-next/dashboard/src/providers/keybind-provider/hooks.tsx
new file mode 100644
index 0000000000..0f7ebb1ad1
--- /dev/null
+++ b/packages/admin-next/dashboard/src/providers/keybind-provider/hooks.tsx
@@ -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([])
+
+ // 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
+}
diff --git a/packages/admin-next/dashboard/src/providers/keybind-provider/index.ts b/packages/admin-next/dashboard/src/providers/keybind-provider/index.ts
new file mode 100644
index 0000000000..d821142bcf
--- /dev/null
+++ b/packages/admin-next/dashboard/src/providers/keybind-provider/index.ts
@@ -0,0 +1,3 @@
+export { useRegisterShortcut } from "./hooks"
+export * from "./keybind-provider"
+export type { Shortcut, ShortcutType } from "./types"
diff --git a/packages/admin-next/dashboard/src/providers/keybind-provider/keybind-context.tsx b/packages/admin-next/dashboard/src/providers/keybind-provider/keybind-context.tsx
new file mode 100644
index 0000000000..7d5f95a6c0
--- /dev/null
+++ b/packages/admin-next/dashboard/src/providers/keybind-provider/keybind-context.tsx
@@ -0,0 +1,4 @@
+import { createContext } from "react"
+import { KeybindContextState } from "./types"
+
+export const KeybindContext = createContext(null)
diff --git a/packages/admin-next/dashboard/src/providers/keybind-provider/keybind-provider.tsx b/packages/admin-next/dashboard/src/providers/keybind-provider/keybind-provider.tsx
new file mode 100644
index 0000000000..302ec37040
--- /dev/null
+++ b/packages/admin-next/dashboard/src/providers/keybind-provider/keybind-provider.tsx
@@ -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(
+ () => ({
+ shortcuts: storeShortcuts,
+ registerShortcut,
+ getKeysByPlatform,
+ }),
+ [storeShortcuts, registerShortcut, getKeysByPlatform]
+ )
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/providers/keybind-provider/types.ts b/packages/admin-next/dashboard/src/providers/keybind-provider/types.ts
new file mode 100644
index 0000000000..e4a9dde9ee
--- /dev/null
+++ b/packages/admin-next/dashboard/src/providers/keybind-provider/types.ts
@@ -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
+}
diff --git a/packages/admin-next/dashboard/src/providers/keybind-provider/utils.ts b/packages/admin-next/dashboard/src/providers/keybind-provider/utils.ts
new file mode 100644
index 0000000000..08187bd897
--- /dev/null
+++ b/packages/admin-next/dashboard/src/providers/keybind-provider/utils.ts
@@ -0,0 +1,92 @@
+import { Keys, Platform, Shortcut } from "./types"
+
+export const findFirstPlatformMatch = (keys: Keys) => {
+ const match =
+ Object.entries(keys as Record).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,
+ }
+}