mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
## Summary Adds the first batch of keyboard shortcuts for the SQL editor, following the registry pattern established for the table editor. ## Shortcuts | Shortcut | Action | Notes | | --- | --- | --- | | `Esc` | Blur the SQL editor | Registered as a Monaco command with a context-key precondition so inline widgets keep owning `Esc` (suggest, find, parameter hints, snippet/rename mode, accessibility help, inline suggestions, and selection cancellation). | | `Shift+E` | Focus the SQL editor | Pairs with `Esc` for mouse-free round-trip. | | `Alt+Shift+F` | Prettify SQL | Now wired through the registry; the tooltip and dropdown badge in `UtilityActions` read the keybind from the same source of truth. Works from inside the editor (Monaco action) and from anywhere on the page (`useShortcut`). | | `Mod+Shift+Enter` | Run EXPLAIN ANALYZE | Routes results into the Explain tab. Surfaces in the Monaco context menu next to "Run Query". | | `Shift+N` | Open a new SQL snippet | Navigates to `/sql/new?skip=true` to avoid the redirect-to-last-visited effect that fires on plain `/sql/new`. | All entries appear in the command menu (`Mod+P`) under "Shortcuts" while their host components are mounted. None are surfaced in Account → Preferences → Keyboard shortcuts yet (`showInSettings: false`), matching how the table editor shortcuts shipped. ## Notes - The blur shortcut intentionally lives on the Monaco instance rather than the document-level hotkey listener — the document listener can't preempt Monaco's own `Esc` handling. Other shortcuts that need to fire while the editor has focus (run, save, format, explain) are registered as Monaco actions; everything else uses `useShortcut`. - Format and explain are double-registered (Monaco action + `useShortcut`) so they fire whether the editor is focused or not. The Monaco actions don't read the user's enable/disable preference yet — same asymmetry as the existing run/save actions. - `Shift+N` is scoped to the SQL editor route. To make it work globally we'd register it at a higher layout level. ## Test plan - [x] Inside editor: `Esc` blurs; suggest/find/parameter hints still close on `Esc`; multi-cursor selection collapses on first `Esc`, blurs on second. - [x] Outside editor: `Shift+E` returns focus to the editor. - [x] `Alt+Shift+F` formats from inside and outside the editor; tooltip + dropdown badge show the correct keybind. - [x] `Mod+Shift+Enter` runs EXPLAIN ANALYZE and switches to the Explain tab. - [x] `Shift+N` opens a fresh snippet without bouncing back to the previous one. - [x] All five shortcuts appear in `Mod+P` with the right badges. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Keyboard shortcuts for SQL editor: format SQL, run EXPLAIN ANALYZE, focus/blur editor, and open a new SQL snippet. * Added "Prettify SQL" and "Run EXPLAIN ANALYZE" actions to the editor context menu with shortcuts. * Centralized registration of SQL editor shortcuts so they appear across the app. * **UX Improvements** * Escape key blurs editor focus when appropriate to allow easy exit without disrupting editor widgets. * **Style** * Adjusted success toast capitalization for copied Markdown. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
176 lines
4.9 KiB
TypeScript
176 lines
4.9 KiB
TypeScript
import { IS_PLATFORM, useParams } from 'common'
|
|
import saveAs from 'file-saver'
|
|
import { ChevronDown, Copy, Download, Settings } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { usePathname } from 'next/navigation'
|
|
import { useMemo } from 'react'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Button,
|
|
copyToClipboard,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
KeyboardShortcut,
|
|
} from 'ui'
|
|
|
|
import {
|
|
convertResultsToCSV,
|
|
convertResultsToJSON,
|
|
convertResultsToMarkdown,
|
|
} from '@/components/interfaces/SQLEditor/UtilityPanel/Results.utils'
|
|
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
|
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
|
|
|
interface DownloadResultsButtonProps {
|
|
iconOnly?: boolean
|
|
type?: 'text' | 'default'
|
|
text?: string
|
|
align?: 'start' | 'center' | 'end'
|
|
results: any[]
|
|
fileName: string
|
|
onDownloadAsCSV?: () => void
|
|
onCopyAsMarkdown?: () => void
|
|
onCopyAsJSON?: () => void
|
|
onCopyAsCSV?: () => void
|
|
}
|
|
|
|
export const DownloadResultsButton = ({
|
|
iconOnly = false,
|
|
type = 'default',
|
|
text = 'Export',
|
|
align = 'start',
|
|
results,
|
|
fileName,
|
|
onDownloadAsCSV,
|
|
onCopyAsMarkdown,
|
|
onCopyAsJSON,
|
|
onCopyAsCSV,
|
|
}: DownloadResultsButtonProps) => {
|
|
const { ref } = useParams()
|
|
const pathname = usePathname()
|
|
const isLogs = pathname?.includes?.('/logs') ?? false
|
|
const isEmpty = useMemo(() => results.length === 0, [results])
|
|
|
|
const downloadAsCSV = () => {
|
|
const csv = convertResultsToCSV(results)
|
|
if (!csv) {
|
|
toast('Results are empty')
|
|
return
|
|
}
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
|
saveAs(blob, `${fileName}.csv`)
|
|
toast.success('Downloading results as CSV')
|
|
onDownloadAsCSV?.()
|
|
}
|
|
|
|
const copyAsMarkdown = () => {
|
|
const markdownData = convertResultsToMarkdown(results)
|
|
if (!markdownData) {
|
|
toast('Results are empty')
|
|
return
|
|
}
|
|
copyToClipboard(markdownData, () => {
|
|
toast.success('Copied Markdown to clipboard')
|
|
onCopyAsMarkdown?.()
|
|
})
|
|
}
|
|
|
|
const copyAsJSON = () => {
|
|
const jsonData = convertResultsToJSON(results)
|
|
if (!jsonData) {
|
|
toast('Results are empty')
|
|
return
|
|
}
|
|
copyToClipboard(jsonData, () => {
|
|
toast.success('Copied JSON to clipboard')
|
|
onCopyAsJSON?.()
|
|
})
|
|
}
|
|
|
|
const copyAsCSV = () => {
|
|
const csv = convertResultsToCSV(results)
|
|
if (!csv) {
|
|
toast('Results are empty')
|
|
return
|
|
}
|
|
copyToClipboard(csv, () => {
|
|
toast.success('Copied CSV to clipboard')
|
|
onCopyAsCSV?.()
|
|
})
|
|
}
|
|
|
|
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>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
type={type}
|
|
icon={iconOnly ? <Download /> : undefined}
|
|
iconRight={iconOnly ? undefined : <ChevronDown />}
|
|
disabled={results.length === 0}
|
|
className={iconOnly ? 'w-7' : ''}
|
|
>
|
|
{!iconOnly && text}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align={align} className="w-60">
|
|
{isLogs && IS_PLATFORM && (
|
|
<DropdownMenuItem asChild className="gap-x-2">
|
|
<Link href={`/project/${ref}/settings/log-drains`}>
|
|
<Settings size={14} />
|
|
<p>Add a Log Drain</p>
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
)}
|
|
<DropdownMenuItem onClick={copyAsMarkdown} className="gap-x-2">
|
|
<Copy size={14} />
|
|
<p>Copy as Markdown</p>
|
|
<span className="ml-auto">
|
|
<KeyboardShortcut keys={['Shift', 'Meta', 'm']} />
|
|
</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={copyAsJSON} className="gap-x-2">
|
|
<Copy size={14} />
|
|
<p>Copy as JSON</p>
|
|
<span className="ml-auto">
|
|
<KeyboardShortcut keys={['Shift', 'Meta', 'j']} />
|
|
</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={copyAsCSV} className="gap-x-2">
|
|
<Copy size={14} />
|
|
<p>Copy as CSV</p>
|
|
<span className="ml-auto">
|
|
<KeyboardShortcut keys={['Shift', 'Meta', 'c']} />
|
|
</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="gap-x-2" onClick={() => downloadAsCSV()}>
|
|
<Download size={14} />
|
|
<p>Download CSV</p>
|
|
<span className="ml-auto">
|
|
<KeyboardShortcut keys={['Shift', 'Meta', 'd']} />
|
|
</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|