mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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')
|
||||
|
||||
|
||||
+17
-5
@@ -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))
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
|
||||
Reference in new issue
Block a user