Files
supabase/apps/studio/state/shortcuts/useShortcut.tsx
Gildas Garcia c6fc456910 chore: cleanup duplicate exports studio (#47387)
## Problem

Knip reports many duplicate exports (both named and default). Besides,
we're moving away from default exports and even have an eslint rule to
enforce it on new code.

## Solution

- Cleanup those exports
- Update imports when necessary

No functional changes. If it builds, it's fine
2026-06-29 15:46:16 +02:00

124 lines
4.9 KiB
TypeScript

import { useHotkeySequence } from '@tanstack/react-hotkeys'
import { Fragment, useCallback, useMemo } from 'react'
import { KeyboardShortcut } from 'ui'
import { useRegisterCommands, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
import { hotkeyToKeys } from './formatShortcut'
import { SHORTCUT_DEFINITIONS, type ShortcutId } from './registry'
import type { ShortcutHotkeyMeta, ShortcutOptions } from './types'
import { useIsShortcutEnabled } from './useIsShortcutEnabled'
import { orderShortcutCommands } from './utils'
import { COMMAND_MENU_SECTIONS } from '@/components/interfaces/App/CommandMenu/CommandMenu.utils'
import { useLatest } from '@/hooks/misc/useLatest'
/**
* Subscribe to a registered keyboard shortcut.
*
* Looks up the shortcut's `sequence` and `label` from `SHORTCUT_DEFINITIONS`,
* wires up a global hotkey listener via `@tanstack/react-hotkeys`, and
* (optionally) registers the shortcut as an entry in the Cmd+P command menu
* under the "Shortcuts" section for as long as the hook is mounted.
*
* Option resolution priority (highest first):
* 1. `options` passed to this hook
* 2. `def.options` from the registry entry
* 3. Hard-coded fallbacks (`enabled: true`, `timeout: undefined`, `registerInCommandMenu: false`)
*
* `enabled` is ANDed with the user's global enable/disable preference — if the
* user has disabled the shortcut in Preferences, it won't fire even if the
* caller or registry say `enabled: true`.
*
* @param id The registered shortcut to bind to. See `SHORTCUT_IDS`.
* @param callback Runs when the sequence matches. Always calls the latest
* reference — no stale closure issues.
* @param options Per-mount overrides. See `ShortcutOptions`.
*
* @example
* useShortcut(SHORTCUT_IDS.RESULTS_COPY_MARKDOWN, handleCopy)
*
* @example
* // Surface in Cmd+P while this component is mounted:
* useShortcut(SHORTCUT_IDS.SQL_EDITOR_RUN, runQuery, {
* registerInCommandMenu: true,
* })
*
* @example
* // Gate on local state — disables hotkey AND hides Cmd+P entry when false:
* useShortcut(SHORTCUT_IDS.SAVE, handleSave, {
* enabled: hasUnsavedChanges,
* registerInCommandMenu: true,
* })
*/
export function useShortcut(id: ShortcutId, callback: () => void, options?: ShortcutOptions) {
const def = SHORTCUT_DEFINITIONS[id]
// Handle override for the shortcut
const globallyEnabled = useIsShortcutEnabled(id)
const callerEnabled = options?.enabled ?? def.options?.enabled ?? true
const enabled = globallyEnabled && callerEnabled
const timeout = options?.timeout ?? def.options?.timeout ?? undefined
const ignoreInputs = options?.ignoreInputs ?? def.options?.ignoreInputs
const registerInCommandMenu =
options?.registerInCommandMenu ?? def.options?.registerInCommandMenu ?? false
const label = options?.label ?? def.label
const conflictBehavior = options?.conflictBehavior ?? def.options?.conflictBehavior
// Stable identity so we don't churn the registration store on every render.
// setOptions in @tanstack/hotkeys notifies subscribers each call, which
// would cascade to every component using useHotkeyRegistrations().
const meta = useMemo<ShortcutHotkeyMeta>(
() => ({ id, name: label, referenceGroup: def.referenceGroup }),
[def.referenceGroup, id, label]
)
// Only include `ignoreInputs` when set. The library resolves it to a concrete
// boolean at register time (false for Meta/Ctrl/Escape, true otherwise), but
// its setOptions does an object spread on every re-render — passing
// `ignoreInputs: undefined` would overwrite the resolved value and re-enable
// the input-focus guard for shortcuts that should always fire.
useHotkeySequence(def.sequence, callback, {
enabled,
timeout,
meta,
...(ignoreInputs !== undefined && { ignoreInputs }),
...(conflictBehavior !== undefined && { conflictBehavior }),
})
// Handle overrides for command menu
const enabledInCommandMenu = enabled && registerInCommandMenu
const depsInCommandMenu = [enabled, label]
const callbackRef = useLatest(callback)
const setCommandMenuOpen = useSetCommandMenuOpen()
const stableAction = useCallback(() => {
setCommandMenuOpen(false)
callbackRef.current()
}, [callbackRef, setCommandMenuOpen])
useRegisterCommands(
COMMAND_MENU_SECTIONS.SHORTCUTS,
[
{
id,
name: label,
action: stableAction,
badge: () => (
<div className="flex items-center gap-1">
{def.sequence.map((step, i) => (
<Fragment key={i}>
{i > 0 && <span className="text-foreground-lighter text-[11px]">then</span>}
<KeyboardShortcut keys={hotkeyToKeys(step)} />
</Fragment>
))}
</div>
),
},
],
{
enabled: enabledInCommandMenu,
deps: depsInCommandMenu,
orderCommands: orderShortcutCommands,
sectionMeta: { priority: 1 },
}
)
}