mirror of
https://github.com/supabase/supabase.git
synced 2026-05-31 09:52:58 +08:00
## Summary Adds keyboard shortcuts to the Integrations Marketplace landing and per-integration detail pages. Introduces a `useDynamicShortcut` hook since per-integration tab counts/labels can't be pre-declared in the static registry. ## Shortcuts | Page | Keys | Action | |---|---|---| | Marketplace landing | `Shift+F` | Focus the integrations search input | | Marketplace landing | `F` then `C` | Clear search + category/type/source filters | | Marketplace landing search | `Esc` | Clear value (1st press), blur (2nd press) | | Integration detail | `1`–`9` | Jump to the Nth tab (label adapts per integration, e.g. "Go to Queues", "Go to Jobs") | Linear: [FE-3416](https://linear.app/supabase/issue/FE-3416) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Use number keys 1–9 to jump to integration detail tabs. * Marketplace search shortcuts: focus/select the search field and reset filters via keyboard; Escape now clears the search input. * Shortcuts now appear in the command menu under a dedicated integrations navigation group. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46348?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
124 lines
4.9 KiB
TypeScript
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 },
|
|
}
|
|
)
|
|
}
|