Skip to content

Commit e8df67d

Browse files
authored
chore: migrate shortcuts to new hooks API (supabase#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 -->
1 parent c04f246 commit e8df67d

16 files changed

Lines changed: 154 additions & 269 deletions

File tree

apps/studio/components/interfaces/Account/Preferences/HotkeySettings.tsx

Lines changed: 10 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import { zodResolver } from '@hookform/resolvers/zod'
2-
import { LOCAL_STORAGE_KEYS } from 'common'
3-
import { useForm } from 'react-hook-form'
4-
import { Card, Form_Shadcn_ } from 'ui'
1+
import { Card } from 'ui'
52
import {
63
PageSection,
74
PageSectionContent,
@@ -10,65 +7,13 @@ import {
107
PageSectionSummary,
118
PageSectionTitle,
129
} from 'ui-patterns/PageSection'
13-
import * as z from 'zod'
1410

1511
import { HotkeyToggle } from './HotkeyToggle'
16-
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
17-
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
12+
import { SHORTCUT_DEFINITIONS } from '@/state/shortcuts/registry'
1813

19-
const HotkeySchema = z.object({
20-
commandMenuEnabled: z.boolean(),
21-
aiAssistantEnabled: z.boolean(),
22-
inlineEditorEnabled: z.boolean(),
23-
copyMarkdownEnabled: z.boolean(),
24-
copyJsonEnabled: z.boolean(),
25-
copyCsvEnabled: z.boolean(),
26-
downloadCsvEnabled: z.boolean(),
27-
})
14+
const SHORTCUT_ORDER = Object.values(SHORTCUT_DEFINITIONS)
2815

2916
export const HotkeySettings = () => {
30-
const [inlineEditorEnabled, setInlineEditorEnabled] = useLocalStorageQuery(
31-
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.EDITOR_PANEL),
32-
true
33-
)
34-
const [commandMenuEnabled, setCommandMenuEnabled] = useLocalStorageQuery(
35-
LOCAL_STORAGE_KEYS.HOTKEY_COMMAND_MENU,
36-
true
37-
)
38-
const [aiAssistantEnabled, setAiAssistantEnabled] = useLocalStorageQuery(
39-
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.AI_ASSISTANT),
40-
true
41-
)
42-
const [copyMarkdownEnabled, setCopyMarkdownEnabled] = useLocalStorageQuery(
43-
LOCAL_STORAGE_KEYS.HOTKEY_COPY_MARKDOWN,
44-
true
45-
)
46-
const [copyJsonEnabled, setCopyJsonEnabled] = useLocalStorageQuery(
47-
LOCAL_STORAGE_KEYS.HOTKEY_COPY_JSON,
48-
true
49-
)
50-
const [copyCsvEnabled, setCopyCsvEnabled] = useLocalStorageQuery(
51-
LOCAL_STORAGE_KEYS.HOTKEY_COPY_CSV,
52-
true
53-
)
54-
const [downloadCsvEnabled, setDownloadCsvEnabled] = useLocalStorageQuery(
55-
LOCAL_STORAGE_KEYS.HOTKEY_DOWNLOAD_CSV,
56-
true
57-
)
58-
59-
const form = useForm<z.infer<typeof HotkeySchema>>({
60-
resolver: zodResolver(HotkeySchema),
61-
values: {
62-
commandMenuEnabled: commandMenuEnabled ?? true,
63-
aiAssistantEnabled: aiAssistantEnabled ?? true,
64-
inlineEditorEnabled: inlineEditorEnabled ?? true,
65-
copyMarkdownEnabled: copyMarkdownEnabled ?? true,
66-
copyJsonEnabled: copyJsonEnabled ?? true,
67-
copyCsvEnabled: copyCsvEnabled ?? true,
68-
downloadCsvEnabled: downloadCsvEnabled ?? true,
69-
},
70-
})
71-
7217
return (
7318
<PageSection>
7419
<PageSectionMeta>
@@ -80,60 +25,15 @@ export const HotkeySettings = () => {
8025
</PageSectionSummary>
8126
</PageSectionMeta>
8227
<PageSectionContent>
83-
<Form_Shadcn_ {...form}>
84-
<Card>
85-
<HotkeyToggle
86-
form={form}
87-
name="commandMenuEnabled"
88-
keys={['Meta', 'k']}
89-
label="Command menu"
90-
onToggle={setCommandMenuEnabled}
91-
/>
92-
<HotkeyToggle
93-
form={form}
94-
name="aiAssistantEnabled"
95-
keys={['Meta', 'i']}
96-
label="AI Assistant panel"
97-
onToggle={setAiAssistantEnabled}
98-
/>
99-
<HotkeyToggle
100-
form={form}
101-
name="inlineEditorEnabled"
102-
keys={['Meta', 'e']}
103-
label="Inline SQL Editor panel"
104-
onToggle={setInlineEditorEnabled}
105-
/>
106-
<HotkeyToggle
107-
form={form}
108-
name="copyMarkdownEnabled"
109-
keys={['Shift', 'Meta', 'm']}
110-
label="Copy results as Markdown"
111-
onToggle={setCopyMarkdownEnabled}
112-
/>
113-
<HotkeyToggle
114-
form={form}
115-
name="copyJsonEnabled"
116-
keys={['Shift', 'Meta', 'j']}
117-
label="Copy results as JSON"
118-
onToggle={setCopyJsonEnabled}
119-
/>
120-
<HotkeyToggle
121-
form={form}
122-
name="copyCsvEnabled"
123-
keys={['Shift', 'Meta', 'c']}
124-
label="Copy results as CSV"
125-
onToggle={setCopyCsvEnabled}
126-
/>
28+
<Card>
29+
{SHORTCUT_ORDER.map((definition, index) => (
12730
<HotkeyToggle
128-
form={form}
129-
name="downloadCsvEnabled"
130-
keys={['Shift', 'Meta', 'd']}
131-
label="Download results as CSV"
132-
onToggle={setDownloadCsvEnabled}
133-
isLast
31+
key={definition.id}
32+
definition={definition}
33+
isLast={index === SHORTCUT_ORDER.length - 1}
13434
/>
135-
</Card>
136-
</Form_Shadcn_>
35+
))}
36+
</Card>
13737
</PageSectionContent>
13838
</PageSection>
13939
)
Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,40 @@
1-
import type { FieldValues, Path, UseFormReturn } from 'react-hook-form'
2-
import { CardContent, FormControl_Shadcn_, FormField_Shadcn_, KeyboardShortcut, Switch } from 'ui'
3-
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
1+
import { Fragment } from 'react'
2+
import { CardContent, KeyboardShortcut, Switch } from 'ui'
43

5-
interface HotkeyToggleProps<T extends FieldValues> {
6-
form: UseFormReturn<T>
7-
name: Path<T>
8-
keys: string[]
9-
label: string
10-
onToggle: (value: boolean) => void
4+
import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
5+
import type { ShortcutId } from '@/state/shortcuts/registry'
6+
import { useShortcutPreferences } from '@/state/shortcuts/state'
7+
import type { ShortcutDefinition } from '@/state/shortcuts/types'
8+
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
9+
10+
interface HotkeyToggleProps {
11+
definition: ShortcutDefinition
1112
isLast?: boolean
1213
}
1314

14-
export function HotkeyToggle<T extends FieldValues>({
15-
form,
16-
name,
17-
keys,
18-
label,
19-
onToggle,
20-
isLast,
21-
}: HotkeyToggleProps<T>) {
15+
export function HotkeyToggle({ definition, isLast }: HotkeyToggleProps) {
16+
const enabled = useIsShortcutEnabled(definition.id as ShortcutId)
17+
const { setShortcutEnabled } = useShortcutPreferences()
18+
2219
return (
2320
<CardContent className={isLast ? undefined : 'border-b'}>
24-
<FormField_Shadcn_
25-
control={form.control}
26-
name={name}
27-
render={({ field }) => (
28-
<FormItemLayout layout="flex-row-reverse" label={label}>
29-
<div className="flex w-full items-center justify-end gap-x-3">
30-
<KeyboardShortcut keys={keys} />
31-
<FormControl_Shadcn_>
32-
<Switch
33-
checked={field.value}
34-
onCheckedChange={(value) => {
35-
field.onChange(value)
36-
onToggle(value)
37-
}}
38-
/>
39-
</FormControl_Shadcn_>
40-
</div>
41-
</FormItemLayout>
42-
)}
43-
/>
21+
<div className="flex items-center justify-between gap-x-3">
22+
<label className="text-sm text-foreground">{definition.label}</label>
23+
<div className="flex items-center gap-x-3">
24+
<div className="flex items-center gap-1">
25+
{definition.sequence.map((step, i) => (
26+
<Fragment key={i}>
27+
{i > 0 && <span className="text-foreground-lighter text-[11px]">then</span>}
28+
<KeyboardShortcut keys={hotkeyToKeys(step)} />
29+
</Fragment>
30+
))}
31+
</div>
32+
<Switch
33+
checked={enabled}
34+
onCheckedChange={(checked) => setShortcutEnabled(definition.id as ShortcutId, checked)}
35+
/>
36+
</div>
37+
</div>
4438
</CardContent>
4539
)
4640
}

apps/studio/components/interfaces/App/CommandMenu/StudioCommandProvider.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
import { LOCAL_STORAGE_KEYS } from 'common'
21
import type { PropsWithChildren } from 'react'
32
import { CommandProvider } from 'ui-patterns/CommandMenu'
43

5-
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
64
import { useStudioCommandMenuTelemetry } from '@/hooks/misc/useStudioCommandMenuTelemetry'
5+
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
6+
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
77

88
export function StudioCommandProvider({ children }: PropsWithChildren) {
99
const { onTelemetry } = useStudioCommandMenuTelemetry()
10-
const [commandMenuHotkeyEnabled] = useLocalStorageQuery<boolean>(
11-
LOCAL_STORAGE_KEYS.HOTKEY_COMMAND_MENU,
12-
true
13-
)
10+
const commandMenuHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.COMMAND_MENU_OPEN)
1411

1512
return (
1613
<CommandProvider

apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
1313
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
1414
import { useProfile } from '@/lib/profile'
1515
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
16+
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
17+
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
1618
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
1719
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
1820
import { useTabsStateSnapshot } from '@/state/tabs'
@@ -64,10 +66,7 @@ const MonacoEditor = ({
6466
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
6567
true
6668
)
67-
const [isAIAssistantHotkeyEnabled] = useLocalStorageQuery<boolean>(
68-
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.AI_ASSISTANT),
69-
true
70-
)
69+
const isAIAssistantHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE)
7170

7271
// [Joshen] Lodash debounce doesn't seem to be working here, so opting to use useDebounce
7372
const [value, setValue] = useState('')
@@ -80,6 +79,9 @@ const MonacoEditor = ({
8079
const executeQueryRef = useRef(executeQuery)
8180
executeQueryRef.current = executeQuery
8281

82+
const aiHotkeyEnabledRef = useRef(isAIAssistantHotkeyEnabled)
83+
aiHotkeyEnabledRef.current = isAIAssistantHotkeyEnabled
84+
8385
const handleEditorOnMount: OnMount = async (editor, monaco) => {
8486
editorRef.current = editor
8587
monacoRef.current = monaco
@@ -134,7 +136,7 @@ const MonacoEditor = ({
134136
label: 'Toggle AI Assistant',
135137
keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.KeyI],
136138
run: () => {
137-
if (isAIAssistantHotkeyEnabled) {
139+
if (aiHotkeyEnabledRef.current) {
138140
toggleSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
139141
}
140142
},

apps/studio/components/layouts/AppLayout/AssistantButton.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1-
import { LOCAL_STORAGE_KEYS } from 'common'
21
import { AiIconAnimation, cn, KeyboardShortcut } from 'ui'
32

43
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
54
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
6-
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
7-
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
5+
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
6+
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
87
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
98

109
export const AssistantButton = () => {
1110
const { activeSidebar, toggleSidebar } = useSidebarManagerSnapshot()
12-
const [isAIAssistantHotkeyEnabled] = useLocalStorageQuery<boolean>(
13-
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.AI_ASSISTANT),
14-
true
15-
)
11+
const isAIAssistantHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE)
1612

1713
const isOpen = activeSidebar?.id === SIDEBAR_KEYS.AI_ASSISTANT
1814

apps/studio/components/layouts/AppLayout/InlineEditorButton.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
import { LOCAL_STORAGE_KEYS } from 'common'
21
import { SqlEditor } from 'icons'
32
import { cn, KeyboardShortcut } from 'ui'
43

54
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
65
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
7-
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
6+
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
7+
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
88
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
99

1010
const InlineEditorKeyboardTooltip = () => {
11-
const [hotkeyEnabled] = useLocalStorageQuery(
12-
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.EDITOR_PANEL),
13-
true
14-
)
11+
const hotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.INLINE_EDITOR_TOGGLE)
1512

1613
return hotkeyEnabled ? <KeyboardShortcut keys={['Meta', 'E']} /> : null
1714
}

apps/studio/components/layouts/Navigation/LayoutHeader/LayoutHeader.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
1+
import { useParams } from 'common'
22
import dayjs from 'dayjs'
33
import { AnimatePresence, motion } from 'framer-motion'
44
import { ChevronLeft } from 'lucide-react'
@@ -28,10 +28,11 @@ import { ProjectDropdown } from '@/components/layouts/AppLayout/ProjectDropdown'
2828
import { HelpButton } from '@/components/ui/HelpPanel/HelpButton'
2929
import { getResourcesExceededLimitsOrg } from '@/components/ui/OveragesBanner/OveragesBanner.utils'
3030
import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
31-
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
3231
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
3332
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
3433
import { IS_PLATFORM } from '@/lib/constants'
34+
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
35+
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
3536

3637
const LayoutHeaderDivider = ({ className, ...props }: React.HTMLProps<HTMLSpanElement>) => (
3738
<span className={cn('text-border-stronger pr-2', className)} {...props}>
@@ -71,7 +72,7 @@ export const LayoutHeader = ({
7172
const { data: selectedOrganization } = useSelectedOrganizationQuery()
7273

7374
const showFloatingMobileToolbar = useIsFloatingMobileToolbarEnabled()
74-
const [commandMenuEnabled] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.HOTKEY_COMMAND_MENU, true)
75+
const commandMenuEnabled = useIsShortcutEnabled(SHORTCUT_IDS.COMMAND_MENU_OPEN)
7576

7677
const isAccountPage = router.pathname.startsWith('/account')
7778

apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import useLatest from '@/hooks/misc/useLatest'
1010
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
1111
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
1212
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
13-
import { useRegisterSidebar, useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
13+
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
14+
import { useShortcut } from '@/state/shortcuts/useShortcut'
15+
import {
16+
sidebarManagerState,
17+
useRegisterSidebar,
18+
useSidebarManagerSnapshot,
19+
} from '@/state/sidebar-manager-state'
1420

1521
const AdvisorPanel = dynamic(() =>
1622
import('@/components/ui/AdvisorPanel/AdvisorPanel').then((m) => m.AdvisorPanel)
@@ -48,9 +54,9 @@ export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => {
4854
const sidebarURLParamRef = useLatest(sidebarURLParam)
4955
const sidebarLocalStorageRef = useLatest(sidebarLocalStorage)
5056

51-
useRegisterSidebar(SIDEBAR_KEYS.AI_ASSISTANT, () => <AIAssistant />, {}, 'i', !!project)
52-
useRegisterSidebar(SIDEBAR_KEYS.EDITOR_PANEL, () => <EditorPanel />, {}, 'e', !!project)
53-
useRegisterSidebar(SIDEBAR_KEYS.ADVISOR_PANEL, () => <AdvisorPanel />, {}, undefined, true)
57+
useRegisterSidebar(SIDEBAR_KEYS.AI_ASSISTANT, () => <AIAssistant />, {}, !!project)
58+
useRegisterSidebar(SIDEBAR_KEYS.EDITOR_PANEL, () => <EditorPanel />, {}, !!project)
59+
useRegisterSidebar(SIDEBAR_KEYS.ADVISOR_PANEL, () => <AdvisorPanel />, {}, true)
5460
useRegisterSidebar(
5561
SIDEBAR_KEYS.HELP_PANEL,
5662
() => (
@@ -65,10 +71,16 @@ export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => {
6571
/>
6672
),
6773
{},
68-
undefined,
6974
true
7075
)
7176

77+
useShortcut(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE, () =>
78+
sidebarManagerState.toggleSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
79+
)
80+
useShortcut(SHORTCUT_IDS.INLINE_EDITOR_TOGGLE, () =>
81+
sidebarManagerState.toggleSidebar(SIDEBAR_KEYS.EDITOR_PANEL)
82+
)
83+
7284
useEffect(() => {
7385
if (!!project) {
7486
if (activeSidebar) {

0 commit comments

Comments
 (0)