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