mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 11:55:05 +03:00
added navigation chord shortcuts and shortcut reference dialog demo
This commit is contained in:
1 parent
79227f5450
commit
803174eaae
5 files changed
+190
-13
No files matched your search
@@ -14,6 +14,7 @@ import { LayoutSidebarProvider } from './ProjectLayout/LayoutSidebar/LayoutSideb
|
||||
import { ProjectContextProvider } from './ProjectLayout/ProjectContext'
|
||||
import { AppBannerWrapper } from '@/components/interfaces/App/AppBannerWrapper'
|
||||
import { Sidebar } from '@/components/interfaces/Sidebar'
|
||||
import { GlobalShortcuts } from '@/components/ui/GlobalShortcuts/GlobalShortcuts'
|
||||
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
|
||||
import { useCheckLatestDeploy } from '@/hooks/use-check-latest-deploy'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
@@ -125,6 +126,7 @@ export const DefaultLayout = ({
|
||||
|
||||
<BannerStack />
|
||||
<StudioMobileSheetNav />
|
||||
<GlobalShortcuts />
|
||||
</BannerStackProvider>
|
||||
</MobileSheetProvider>
|
||||
</ProjectContextProvider>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useParams } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
import { ShortcutsReferenceDialog } from './ShortcutsReferenceDialog'
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
||||
|
||||
/**
|
||||
* Mounts the app-wide keyboard shortcuts that aren't tied to a specific
|
||||
* component — navigation chords (G → T/E/S/D/A) and the shortcut reference
|
||||
* dialog (press ?). Place this inside a layout that stays mounted across
|
||||
* route changes (e.g. DefaultLayout).
|
||||
*/
|
||||
export function GlobalShortcuts() {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const [referenceOpen, setReferenceOpen] = useState(false)
|
||||
|
||||
const goTo = useCallback(
|
||||
(path: string) => () => {
|
||||
if (!ref) return
|
||||
router.push(`/project/${ref}${path}`)
|
||||
},
|
||||
[ref, router]
|
||||
)
|
||||
|
||||
const navEnabled = !!ref
|
||||
|
||||
useShortcut(SHORTCUT_IDS.NAV_TABLE_EDITOR, goTo('/editor'), {
|
||||
enabled: navEnabled,
|
||||
registerInCommandMenu: true,
|
||||
})
|
||||
useShortcut(SHORTCUT_IDS.NAV_SQL_EDITOR, goTo('/sql'), {
|
||||
enabled: navEnabled,
|
||||
registerInCommandMenu: true,
|
||||
})
|
||||
useShortcut(SHORTCUT_IDS.NAV_STORAGE, goTo('/storage/files'), {
|
||||
enabled: navEnabled,
|
||||
registerInCommandMenu: true,
|
||||
})
|
||||
useShortcut(SHORTCUT_IDS.NAV_DATABASE, goTo('/database/tables'), {
|
||||
enabled: navEnabled,
|
||||
registerInCommandMenu: true,
|
||||
})
|
||||
useShortcut(SHORTCUT_IDS.NAV_AUTH, goTo('/auth/users'), {
|
||||
enabled: navEnabled,
|
||||
registerInCommandMenu: true,
|
||||
})
|
||||
|
||||
useShortcut(SHORTCUT_IDS.SHORTCUTS_OPEN_REFERENCE, () => setReferenceOpen(true), {
|
||||
registerInCommandMenu: true,
|
||||
})
|
||||
|
||||
return <ShortcutsReferenceDialog open={referenceOpen} onOpenChange={setReferenceOpen} />
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Fragment } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogSection,
|
||||
DialogTitle,
|
||||
KeyboardShortcut,
|
||||
} from 'ui'
|
||||
|
||||
import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
|
||||
import { SHORTCUT_DEFINITIONS } from '@/state/shortcuts/registry'
|
||||
import type { ShortcutDefinition } from '@/state/shortcuts/types'
|
||||
|
||||
interface ShortcutsReferenceDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
const GROUP_LABELS: Record<string, string> = {
|
||||
nav: 'Navigate',
|
||||
results: 'Results',
|
||||
shortcuts: 'Help',
|
||||
}
|
||||
|
||||
const groupDefinitions = (): Array<{ group: string; defs: ShortcutDefinition[] }> => {
|
||||
const grouped = Object.values(SHORTCUT_DEFINITIONS).reduce<Record<string, ShortcutDefinition[]>>(
|
||||
(acc, def) => {
|
||||
const prefix = def.id.split('.')[0]
|
||||
acc[prefix] = acc[prefix] ?? []
|
||||
acc[prefix].push(def)
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
return Object.entries(grouped).map(([group, defs]) => ({ group, defs }))
|
||||
}
|
||||
|
||||
export function ShortcutsReferenceDialog({ open, onOpenChange }: ShortcutsReferenceDialogProps) {
|
||||
const groups = groupDefinitions()
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent size="medium">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Keyboard shortcuts</DialogTitle>
|
||||
<DialogDescription>
|
||||
Every shortcut registered in the dashboard. Press <kbd>?</kbd> anytime to reopen this.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogSection className="flex flex-col gap-6 max-h-[60vh] overflow-y-auto">
|
||||
{groups.map(({ group, defs }) => (
|
||||
<div key={group} className="flex flex-col gap-2">
|
||||
<h3 className="text-xs uppercase tracking-wide text-foreground-lighter">
|
||||
{GROUP_LABELS[group] ?? group}
|
||||
</h3>
|
||||
<ul className="flex flex-col">
|
||||
{defs.map((def) => (
|
||||
<li
|
||||
key={def.id}
|
||||
className="flex items-center justify-between py-2 border-b last:border-b-0"
|
||||
>
|
||||
<span className="text-sm text-foreground">{def.label}</span>
|
||||
<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>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</DialogSection>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
@@ -9,6 +9,14 @@ import { ShortcutDefinition } from './types'
|
||||
*/
|
||||
export const SHORTCUT_IDS = {
|
||||
RESULTS_COPY_MARKDOWN: 'results.copy-markdown',
|
||||
|
||||
NAV_TABLE_EDITOR: 'nav.table-editor',
|
||||
NAV_SQL_EDITOR: 'nav.sql-editor',
|
||||
NAV_STORAGE: 'nav.storage',
|
||||
NAV_DATABASE: 'nav.database',
|
||||
NAV_AUTH: 'nav.auth',
|
||||
|
||||
SHORTCUTS_OPEN_REFERENCE: 'shortcuts.open-reference',
|
||||
} as const
|
||||
|
||||
/**
|
||||
@@ -23,19 +31,6 @@ export type ShortcutId = (typeof SHORTCUT_IDS)[keyof typeof SHORTCUT_IDS]
|
||||
* new entry to `SHORTCUT_IDS` without a matching definition here is a type error.
|
||||
*
|
||||
* See `ShortcutDefinition` for the shape of each entry.
|
||||
*
|
||||
* @example
|
||||
* // Add a new shortcut:
|
||||
* // 1. Add to SHORTCUT_IDS:
|
||||
* // SQL_EDITOR_RUN: 'sql-editor.run'
|
||||
* // 2. Add to SHORTCUT_DEFINITIONS:
|
||||
* // [SHORTCUT_IDS.SQL_EDITOR_RUN]: {
|
||||
* // id: SHORTCUT_IDS.SQL_EDITOR_RUN,
|
||||
* // label: 'Run query',
|
||||
* // sequence: ['Mod+Enter'],
|
||||
* // }
|
||||
* // 3. Use in a component:
|
||||
* // useShortcut(SHORTCUT_IDS.SQL_EDITOR_RUN, runQuery)
|
||||
*/
|
||||
export const SHORTCUT_DEFINITIONS: Record<ShortcutId, ShortcutDefinition> = {
|
||||
[SHORTCUT_IDS.RESULTS_COPY_MARKDOWN]: {
|
||||
@@ -43,4 +38,36 @@ export const SHORTCUT_DEFINITIONS: Record<ShortcutId, ShortcutDefinition> = {
|
||||
label: 'Copy results as Markdown',
|
||||
sequence: ['Mod+Shift+M'],
|
||||
},
|
||||
|
||||
[SHORTCUT_IDS.NAV_TABLE_EDITOR]: {
|
||||
id: SHORTCUT_IDS.NAV_TABLE_EDITOR,
|
||||
label: 'Go to Table Editor',
|
||||
sequence: ['G', 'T'],
|
||||
},
|
||||
[SHORTCUT_IDS.NAV_SQL_EDITOR]: {
|
||||
id: SHORTCUT_IDS.NAV_SQL_EDITOR,
|
||||
label: 'Go to SQL Editor',
|
||||
sequence: ['G', 'E'],
|
||||
},
|
||||
[SHORTCUT_IDS.NAV_STORAGE]: {
|
||||
id: SHORTCUT_IDS.NAV_STORAGE,
|
||||
label: 'Go to Storage',
|
||||
sequence: ['G', 'S'],
|
||||
},
|
||||
[SHORTCUT_IDS.NAV_DATABASE]: {
|
||||
id: SHORTCUT_IDS.NAV_DATABASE,
|
||||
label: 'Go to Database',
|
||||
sequence: ['G', 'D'],
|
||||
},
|
||||
[SHORTCUT_IDS.NAV_AUTH]: {
|
||||
id: SHORTCUT_IDS.NAV_AUTH,
|
||||
label: 'Go to Authentication',
|
||||
sequence: ['G', 'A'],
|
||||
},
|
||||
|
||||
[SHORTCUT_IDS.SHORTCUTS_OPEN_REFERENCE]: {
|
||||
id: SHORTCUT_IDS.SHORTCUTS_OPEN_REFERENCE,
|
||||
label: 'Show all keyboard shortcuts',
|
||||
sequence: ['Shift+A'],
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user