chore: migrate shortcuts to new hooks API (#44955)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Cleanup shortcuts with new hooks

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Centralized keyboard shortcut system for consistent shortcut behavior
across the app and moved preference toggles to a unified registry.

* **New Features**
* Added explicit shortcuts for Command Menu, AI Assistant, Inline
Editor, and result copy/download actions.
* Hotkey preferences UI now renders dynamically from the centralized
shortcut list.

* **Tests**
* Test helpers updated to include the command menu provider for accurate
shortcut behavior in tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ali Waseem authored and GitHub committed 2026-04-17 10:02:56 -06:00
1 parent c04f2465e4
commit e8df67d5d5
16 files changed
+154 -269

No files matched your search

@@ -1,7 +1,4 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useForm } from 'react-hook-form'
import { Card, Form_Shadcn_ } from 'ui'
import { Card } from 'ui'
import {
PageSection,
PageSectionContent,
@@ -10,65 +7,13 @@ import {
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import * as z from 'zod'
import { HotkeyToggle } from './HotkeyToggle'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { SHORTCUT_DEFINITIONS } from '@/state/shortcuts/registry'
const HotkeySchema = z.object({
commandMenuEnabled: z.boolean(),
aiAssistantEnabled: z.boolean(),
inlineEditorEnabled: z.boolean(),
copyMarkdownEnabled: z.boolean(),
copyJsonEnabled: z.boolean(),
copyCsvEnabled: z.boolean(),
downloadCsvEnabled: z.boolean(),
})
const SHORTCUT_ORDER = Object.values(SHORTCUT_DEFINITIONS)
export const HotkeySettings = () => {
const [inlineEditorEnabled, setInlineEditorEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.EDITOR_PANEL),
true
)
const [commandMenuEnabled, setCommandMenuEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.HOTKEY_COMMAND_MENU,
true
)
const [aiAssistantEnabled, setAiAssistantEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.AI_ASSISTANT),
true
)
const [copyMarkdownEnabled, setCopyMarkdownEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.HOTKEY_COPY_MARKDOWN,
true
)
const [copyJsonEnabled, setCopyJsonEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.HOTKEY_COPY_JSON,
true
)
const [copyCsvEnabled, setCopyCsvEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.HOTKEY_COPY_CSV,
true
)
const [downloadCsvEnabled, setDownloadCsvEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.HOTKEY_DOWNLOAD_CSV,
true
)
const form = useForm<z.infer<typeof HotkeySchema>>({
resolver: zodResolver(HotkeySchema),
values: {
commandMenuEnabled: commandMenuEnabled ?? true,
aiAssistantEnabled: aiAssistantEnabled ?? true,
inlineEditorEnabled: inlineEditorEnabled ?? true,
copyMarkdownEnabled: copyMarkdownEnabled ?? true,
copyJsonEnabled: copyJsonEnabled ?? true,
copyCsvEnabled: copyCsvEnabled ?? true,
downloadCsvEnabled: downloadCsvEnabled ?? true,
},
})
return (
<PageSection>
<PageSectionMeta>
@@ -80,60 +25,15 @@ export const HotkeySettings = () => {
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form_Shadcn_ {...form}>
<Card>
<Card>
{SHORTCUT_ORDER.map((definition, index) => (
<HotkeyToggle
form={form}
name="commandMenuEnabled"
keys={['Meta', 'k']}
label="Command menu"
onToggle={setCommandMenuEnabled}
key={definition.id}
definition={definition}
isLast={index === SHORTCUT_ORDER.length - 1}
/>
<HotkeyToggle
form={form}
name="aiAssistantEnabled"
keys={['Meta', 'i']}
label="AI Assistant panel"
onToggle={setAiAssistantEnabled}
/>
<HotkeyToggle
form={form}
name="inlineEditorEnabled"
keys={['Meta', 'e']}
label="Inline SQL Editor panel"
onToggle={setInlineEditorEnabled}
/>
<HotkeyToggle
form={form}
name="copyMarkdownEnabled"
keys={['Shift', 'Meta', 'm']}
label="Copy results as Markdown"
onToggle={setCopyMarkdownEnabled}
/>
<HotkeyToggle
form={form}
name="copyJsonEnabled"
keys={['Shift', 'Meta', 'j']}
label="Copy results as JSON"
onToggle={setCopyJsonEnabled}
/>
<HotkeyToggle
form={form}
name="copyCsvEnabled"
keys={['Shift', 'Meta', 'c']}
label="Copy results as CSV"
onToggle={setCopyCsvEnabled}
/>
<HotkeyToggle
form={form}
name="downloadCsvEnabled"
keys={['Shift', 'Meta', 'd']}
label="Download results as CSV"
onToggle={setDownloadCsvEnabled}
isLast
/>
</Card>
</Form_Shadcn_>
))}
</Card>
</PageSectionContent>
</PageSection>
)
@@ -1,46 +1,40 @@
import type { FieldValues, Path, UseFormReturn } from 'react-hook-form'
import { CardContent, FormControl_Shadcn_, FormField_Shadcn_, KeyboardShortcut, Switch } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { Fragment } from 'react'
import { CardContent, KeyboardShortcut, Switch } from 'ui'
interface HotkeyToggleProps<T extends FieldValues> {
form: UseFormReturn<T>
name: Path<T>
keys: string[]
label: string
onToggle: (value: boolean) => void
import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
import type { ShortcutId } from '@/state/shortcuts/registry'
import { useShortcutPreferences } from '@/state/shortcuts/state'
import type { ShortcutDefinition } from '@/state/shortcuts/types'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
interface HotkeyToggleProps {
definition: ShortcutDefinition
isLast?: boolean
}
export function HotkeyToggle<T extends FieldValues>({
form,
name,
keys,
label,
onToggle,
isLast,
}: HotkeyToggleProps<T>) {
export function HotkeyToggle({ definition, isLast }: HotkeyToggleProps) {
const enabled = useIsShortcutEnabled(definition.id as ShortcutId)
const { setShortcutEnabled } = useShortcutPreferences()
return (
<CardContent className={isLast ? undefined : 'border-b'}>
<FormField_Shadcn_
control={form.control}
name={name}
render={({ field }) => (
<FormItemLayout layout="flex-row-reverse" label={label}>
<div className="flex w-full items-center justify-end gap-x-3">
<KeyboardShortcut keys={keys} />
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={(value) => {
field.onChange(value)
onToggle(value)
}}
/>
</FormControl_Shadcn_>
</div>
</FormItemLayout>
)}
/>
<div className="flex items-center justify-between gap-x-3">
<label className="text-sm text-foreground">{definition.label}</label>
<div className="flex items-center gap-x-3">
<div className="flex items-center gap-1">
{definition.sequence.map((step, i) => (
<Fragment key={i}>
{i > 0 && <span className="text-foreground-lighter text-[11px]">then</span>}
<KeyboardShortcut keys={hotkeyToKeys(step)} />
</Fragment>
))}
</div>
<Switch
checked={enabled}
onCheckedChange={(checked) => setShortcutEnabled(definition.id as ShortcutId, checked)}
/>
</div>
</div>
</CardContent>
)
}
@@ -1,16 +1,13 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import type { PropsWithChildren } from 'react'
import { CommandProvider } from 'ui-patterns/CommandMenu'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useStudioCommandMenuTelemetry } from '@/hooks/misc/useStudioCommandMenuTelemetry'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
export function StudioCommandProvider({ children }: PropsWithChildren) {
const { onTelemetry } = useStudioCommandMenuTelemetry()
const [commandMenuHotkeyEnabled] = useLocalStorageQuery<boolean>(
LOCAL_STORAGE_KEYS.HOTKEY_COMMAND_MENU,
true
)
const commandMenuHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.COMMAND_MENU_OPEN)
return (
<CommandProvider
@@ -13,6 +13,8 @@ import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useProfile } from '@/lib/profile'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
import { useTabsStateSnapshot } from '@/state/tabs'
@@ -64,10 +66,7 @@ const MonacoEditor = ({
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
true
)
const [isAIAssistantHotkeyEnabled] = useLocalStorageQuery<boolean>(
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.AI_ASSISTANT),
true
)
const isAIAssistantHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE)
// [Joshen] Lodash debounce doesn't seem to be working here, so opting to use useDebounce
const [value, setValue] = useState('')
@@ -80,6 +79,9 @@ const MonacoEditor = ({
const executeQueryRef = useRef(executeQuery)
executeQueryRef.current = executeQuery
const aiHotkeyEnabledRef = useRef(isAIAssistantHotkeyEnabled)
aiHotkeyEnabledRef.current = isAIAssistantHotkeyEnabled
const handleEditorOnMount: OnMount = async (editor, monaco) => {
editorRef.current = editor
monacoRef.current = monaco
@@ -134,7 +136,7 @@ const MonacoEditor = ({
label: 'Toggle AI Assistant',
keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.KeyI],
run: () => {
if (isAIAssistantHotkeyEnabled) {
if (aiHotkeyEnabledRef.current) {
toggleSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
}
},
@@ -1,18 +1,14 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import { AiIconAnimation, cn, KeyboardShortcut } from 'ui'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
export const AssistantButton = () => {
const { activeSidebar, toggleSidebar } = useSidebarManagerSnapshot()
const [isAIAssistantHotkeyEnabled] = useLocalStorageQuery<boolean>(
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.AI_ASSISTANT),
true
)
const isAIAssistantHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE)
const isOpen = activeSidebar?.id === SIDEBAR_KEYS.AI_ASSISTANT
@@ -1,17 +1,14 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import { SqlEditor } from 'icons'
import { cn, KeyboardShortcut } from 'ui'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
const InlineEditorKeyboardTooltip = () => {
const [hotkeyEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.EDITOR_PANEL),
true
)
const hotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.INLINE_EDITOR_TOGGLE)
return hotkeyEnabled ? <KeyboardShortcut keys={['Meta', 'E']} /> : null
}
@@ -1,4 +1,4 @@
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useParams } from 'common'
import dayjs from 'dayjs'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronLeft } from 'lucide-react'
@@ -28,10 +28,11 @@ import { ProjectDropdown } from '@/components/layouts/AppLayout/ProjectDropdown'
import { HelpButton } from '@/components/ui/HelpPanel/HelpButton'
import { getResourcesExceededLimitsOrg } from '@/components/ui/OveragesBanner/OveragesBanner.utils'
import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { IS_PLATFORM } from '@/lib/constants'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
const LayoutHeaderDivider = ({ className, ...props }: React.HTMLProps<HTMLSpanElement>) => (
<span className={cn('text-border-stronger pr-2', className)} {...props}>
@@ -71,7 +72,7 @@ export const LayoutHeader = ({
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const showFloatingMobileToolbar = useIsFloatingMobileToolbarEnabled()
const [commandMenuEnabled] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.HOTKEY_COMMAND_MENU, true)
const commandMenuEnabled = useIsShortcutEnabled(SHORTCUT_IDS.COMMAND_MENU_OPEN)
const isAccountPage = router.pathname.startsWith('/account')
@@ -10,7 +10,13 @@ import useLatest from '@/hooks/misc/useLatest'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useRegisterSidebar, useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
import {
sidebarManagerState,
useRegisterSidebar,
useSidebarManagerSnapshot,
} from '@/state/sidebar-manager-state'
const AdvisorPanel = dynamic(() =>
import('@/components/ui/AdvisorPanel/AdvisorPanel').then((m) => m.AdvisorPanel)
@@ -48,9 +54,9 @@ export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => {
const sidebarURLParamRef = useLatest(sidebarURLParam)
const sidebarLocalStorageRef = useLatest(sidebarLocalStorage)
useRegisterSidebar(SIDEBAR_KEYS.AI_ASSISTANT, () => <AIAssistant />, {}, 'i', !!project)
useRegisterSidebar(SIDEBAR_KEYS.EDITOR_PANEL, () => <EditorPanel />, {}, 'e', !!project)
useRegisterSidebar(SIDEBAR_KEYS.ADVISOR_PANEL, () => <AdvisorPanel />, {}, undefined, true)
useRegisterSidebar(SIDEBAR_KEYS.AI_ASSISTANT, () => <AIAssistant />, {}, !!project)
useRegisterSidebar(SIDEBAR_KEYS.EDITOR_PANEL, () => <EditorPanel />, {}, !!project)
useRegisterSidebar(SIDEBAR_KEYS.ADVISOR_PANEL, () => <AdvisorPanel />, {}, true)
useRegisterSidebar(
SIDEBAR_KEYS.HELP_PANEL,
() => (
@@ -65,10 +71,16 @@ export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => {
/>
),
{},
undefined,
true
)
useShortcut(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE, () =>
sidebarManagerState.toggleSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
)
useShortcut(SHORTCUT_IDS.INLINE_EDITOR_TOGGLE, () =>
sidebarManagerState.toggleSidebar(SIDEBAR_KEYS.EDITOR_PANEL)
)
useEffect(() => {
if (!!project) {
if (activeSidebar) {
@@ -1,4 +1,4 @@
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useParams } from 'common'
import { IS_PLATFORM, useParams } from 'common'
import saveAs from 'file-saver'
import { ChevronDown, Copy, Download, Settings } from 'lucide-react'
import Link from 'next/link'
@@ -20,8 +20,8 @@ import {
convertResultsToJSON,
convertResultsToMarkdown,
} from '@/components/interfaces/SQLEditor/UtilityPanel/Results.utils'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useHotKey } from '@/hooks/ui/useHotKey'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface DownloadResultsButtonProps {
iconOnly?: boolean
@@ -51,11 +51,6 @@ export const DownloadResultsButton = ({
const { ref } = useParams()
const pathname = usePathname()
const isLogs = pathname?.includes?.('/logs') ?? false
const [copyMarkdownEnabled] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.HOTKEY_COPY_MARKDOWN, true)
const [copyJsonEnabled] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.HOTKEY_COPY_JSON, true)
const [copyCsvEnabled] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.HOTKEY_COPY_CSV, true)
const [downloadCsvEnabled] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.HOTKEY_DOWNLOAD_CSV, true)
const isEmpty = useMemo(() => results.length === 0, [results])
const downloadAsCSV = () => {
@@ -107,41 +102,22 @@ export const DownloadResultsButton = ({
})
}
useHotKey(
(e) => {
e.preventDefault()
copyAsMarkdown()
},
'm',
{ enabled: copyMarkdownEnabled ?? isEmpty, shift: true }
)
useHotKey(
(e) => {
e.preventDefault()
copyAsJSON()
},
'j',
{ enabled: copyJsonEnabled ?? isEmpty, shift: true }
)
useHotKey(
(e) => {
e.preventDefault()
copyAsCSV()
},
'c',
{ enabled: copyCsvEnabled ?? isEmpty, shift: true }
)
useHotKey(
(e) => {
e.preventDefault()
downloadAsCSV()
},
'd',
{ enabled: downloadCsvEnabled ?? isEmpty, shift: true }
)
useShortcut(SHORTCUT_IDS.RESULTS_COPY_MARKDOWN, copyAsMarkdown, {
enabled: !isEmpty,
registerInCommandMenu: true,
})
useShortcut(SHORTCUT_IDS.RESULTS_COPY_JSON, copyAsJSON, {
enabled: !isEmpty,
registerInCommandMenu: true,
})
useShortcut(SHORTCUT_IDS.RESULTS_COPY_CSV, copyAsCSV, {
enabled: !isEmpty,
registerInCommandMenu: true,
})
useShortcut(SHORTCUT_IDS.RESULTS_DOWNLOAD_CSV, downloadAsCSV, {
enabled: !isEmpty,
registerInCommandMenu: true,
})
return (
<DropdownMenu>
@@ -1,7 +1,7 @@
import type { Monaco } from '@monaco-editor/react'
import { useQueryClient } from '@tanstack/react-query'
import { useDebounce } from '@uidotdev/usehooks'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useParams } from 'common'
import {
AlertCircle,
Book,
@@ -57,12 +57,13 @@ import { useContentQuery, type Content } from '@/data/content/content-query'
import { useContentUpsertMutation } from '@/data/content/content-upsert-mutation'
import { contentKeys } from '@/data/content/keys'
import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { BASE_PATH } from '@/lib/constants'
import { useProfile } from '@/lib/profile'
import { editorPanelState, useEditorPanelStateSnapshot } from '@/state/editor-panel-state'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
@@ -107,14 +108,8 @@ export const EditorPanel = () => {
setActiveSnippet({ ...activeSnippet, name: newName })
setIsEditingTitle(false)
}
const [isInlineEditorHotkeyEnabled] = useLocalStorageQuery<boolean>(
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.EDITOR_PANEL),
true
)
const [isAIAssistantHotkeyEnabled] = useLocalStorageQuery<boolean>(
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.AI_ASSISTANT),
true
)
const isInlineEditorHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.INLINE_EDITOR_TOGGLE)
const isAIAssistantHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE)
const currentValue = value || ''
@@ -0,0 +1,8 @@
/**
* Convert a TanStack Hotkeys hotkey string (e.g. `"Mod+Shift+M"`) into the
* key-array format consumed by the `<KeyboardShortcut />` component from `ui`
* (e.g. `["Meta", "Shift", "M"]`). `KeyboardShortcut` resolves `Meta` to ⌘ on
* macOS or `Ctrl` elsewhere, so this mapping is platform-safe.
*/
export const hotkeyToKeys = (hotkey: string): string[] =>
hotkey.split('+').map((part) => (part === 'Mod' ? 'Meta' : part))
+36
View File
@@ -8,7 +8,13 @@ import { ShortcutDefinition } from './types'
* The `<surface>` groups related shortcuts (sql-editor, table-editor, results, etc).
*/
export const SHORTCUT_IDS = {
COMMAND_MENU_OPEN: 'command-menu.open',
AI_ASSISTANT_TOGGLE: 'ai-assistant.toggle',
INLINE_EDITOR_TOGGLE: 'inline-editor.toggle',
RESULTS_COPY_MARKDOWN: 'results.copy-markdown',
RESULTS_COPY_JSON: 'results.copy-json',
RESULTS_COPY_CSV: 'results.copy-csv',
RESULTS_DOWNLOAD_CSV: 'results.download-csv',
} as const
/**
@@ -38,9 +44,39 @@ export type ShortcutId = (typeof SHORTCUT_IDS)[keyof typeof SHORTCUT_IDS]
* // useShortcut(SHORTCUT_IDS.SQL_EDITOR_RUN, runQuery)
*/
export const SHORTCUT_DEFINITIONS: Record<ShortcutId, ShortcutDefinition> = {
[SHORTCUT_IDS.COMMAND_MENU_OPEN]: {
id: SHORTCUT_IDS.COMMAND_MENU_OPEN,
label: 'Open command menu',
sequence: ['Mod+K'],
},
[SHORTCUT_IDS.AI_ASSISTANT_TOGGLE]: {
id: SHORTCUT_IDS.AI_ASSISTANT_TOGGLE,
label: 'Toggle AI Assistant panel',
sequence: ['Mod+I'],
},
[SHORTCUT_IDS.INLINE_EDITOR_TOGGLE]: {
id: SHORTCUT_IDS.INLINE_EDITOR_TOGGLE,
label: 'Toggle inline SQL editor',
sequence: ['Mod+E'],
},
[SHORTCUT_IDS.RESULTS_COPY_MARKDOWN]: {
id: SHORTCUT_IDS.RESULTS_COPY_MARKDOWN,
label: 'Copy results as Markdown',
sequence: ['Mod+Shift+M'],
},
[SHORTCUT_IDS.RESULTS_COPY_JSON]: {
id: SHORTCUT_IDS.RESULTS_COPY_JSON,
label: 'Copy results as JSON',
sequence: ['Mod+Shift+J'],
},
[SHORTCUT_IDS.RESULTS_COPY_CSV]: {
id: SHORTCUT_IDS.RESULTS_COPY_CSV,
label: 'Copy results as CSV',
sequence: ['Mod+Shift+C'],
},
[SHORTCUT_IDS.RESULTS_DOWNLOAD_CSV]: {
id: SHORTCUT_IDS.RESULTS_DOWNLOAD_CSV,
label: 'Download results as CSV',
sequence: ['Mod+Shift+D'],
},
}
@@ -1,9 +1,7 @@
import { LOCAL_STORAGE_KEYS } from 'common/constants'
import { ReactNode, useEffect } from 'react'
import { proxy, snapshot, useSnapshot } from 'valtio'
import useLatest from '@/hooks/misc/useLatest'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
type SidebarHandlers = {
onOpen?: () => void
@@ -167,14 +165,8 @@ export const useRegisterSidebar = (
id: string,
component: () => ReactNode,
handlers: SidebarHandlers = {},
hotKey?: string,
enabled?: boolean
) => {
const [isSidebarHotkeyEnabled] = useLocalStorageQuery<boolean>(
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(id),
true
)
const componentRef = useLatest(component)
const handlersRef = useLatest(handlers)
@@ -187,21 +179,4 @@ export const useRegisterSidebar = (
sidebarManagerState.unregisterSidebar(id)
}
}, [id, enabled])
useEffect(() => {
if (!hotKey) return
function hotKeyHandler(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key === hotKey && !e.altKey && !e.shiftKey) {
sidebarManagerState.toggleSidebar(id)
}
}
if (isSidebarHotkeyEnabled) {
window.addEventListener('keydown', hotKeyHandler)
return () => {
window.removeEventListener('keydown', hotKeyHandler)
}
}
}, [id, hotKey, isSidebarHotkeyEnabled])
}
+4 -1
View File
@@ -3,6 +3,7 @@ import { fireEvent, getByText, render as originalRender, screen } from '@testing
import type React from 'react'
import { useState } from 'react'
import { TooltipProvider } from 'ui'
import { CommandProvider } from 'ui-patterns/CommandMenu'
import { ProjectInfoInfinite } from '@/data/projects/projects-infinite-query'
import type { Organization } from '@/types'
@@ -106,7 +107,9 @@ const ReactQueryTestConfig: React.FC<React.PropsWithChildren> = ({ children }) =
return (
<TooltipProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
<QueryClientProvider client={queryClient}>
<CommandProvider openKey="">{children}</CommandProvider>
</QueryClientProvider>
</TooltipProvider>
)
}
+4 -1
View File
@@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, renderHook, type RenderOptions } from '@testing-library/react'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import { TooltipProvider } from 'ui'
import { CommandProvider } from 'ui-patterns/CommandMenu'
// End of third-party imports
@@ -33,7 +34,9 @@ const CustomWrapper = ({
const content = (
<QueryClientProvider client={_queryClient}>
<NuqsTestingAdapter {...nuqs}>
<TooltipProvider>{children}</TooltipProvider>
<TooltipProvider>
<CommandProvider openKey="">{children}</CommandProvider>
</TooltipProvider>
</NuqsTestingAdapter>
</QueryClientProvider>
)
@@ -89,18 +89,8 @@ export const LOCAL_STORAGE_KEYS = {
USER_IMPERSONATION_SELECTOR_PREVIOUS_SEARCHES: (ref: string) =>
`user-impersonation-selector-previous-searches-${ref}`,
HOTKEY_COMMAND_MENU: 'supabase-dashboard-hotkey-command-menu',
LAST_OPENED_SIDE_BAR: (ref: string) => `last-opened-sidebar-${ref}`,
// Project sidebar hotkeys
HOTKEY_SIDEBAR: (sidebarId: string) => `supabase-dashboard-hotkey-sidebar-${sidebarId}`,
HOTKEY_COPY_MARKDOWN: 'supabase-dashboard-hotkey-copy-markdown',
HOTKEY_COPY_JSON: 'supabase-dashboard-hotkey-copy-json',
HOTKEY_COPY_CSV: 'supabase-dashboard-hotkey-copy-csv',
HOTKEY_DOWNLOAD_CSV: 'supabase-dashboard-hotkey-download-csv',
// Index Advisor notice dismissed
INDEX_ADVISOR_NOTICE_DISMISSED: (ref: string) => `index-advisor-notice-dismissed-${ref}`,