mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 14:05:05 +08:00
## Summary Adds a contextual `D + <letter>` chord pattern for jumping between Database sub-pages, mounted only while `DatabaseLayout` is active. Establishes the pattern we can repeat for other sections (Auth, Storage, Functions, etc.). Linear: [FE-3140](https://linear.app/supabase/issue/FE-3140/define-subnavigation-pattern-for-database-management-page) ## Pattern - Chords are 2-key sequences (`D`, `<letter>`) — no global leader, no `G` prefix. - Registration is contextual: `<DatabaseNavShortcuts />` lives inside `DatabaseLayout`, so the leading `D` is only "owned" while the user is under `/project/<ref>/database/*`. Doesn't burn a global key. - Hover tooltips on each sub-menu item show the chord, anchored to the label text (Linear-style). Powered by `<ShortcutTooltip>` already used in the main nav. - Items hidden by feature flags (Roles, Column Privileges, Replication) auto-disable the chord — no muscle-memory navigating to a 404. ## Shortcuts added | Sub-page | Chord | Notes | |---|---|---| | Tables | `D T` | | | Functions | `D F` | | | Triggers | `D R` | t**R**iggers — `T` taken by Tables | | Indexes | `D I` | | | Extensions | `D X` | e**X**tensions | | Schema Visualizer | `D V` | | | Enumerated Types | `D E` | | | Publications | `D U` | p**U**blications — avoids collision with Schema Visualizer's `D P` (Download as PNG) | | Column Privileges | `D C` | flag-gated | | Settings | `D ,` | mirrors global `G ,` for project settings — avoids collision with Schema Visualizer's `D S` (Download as SVG) | | Replication | `D L` | rep**L**ication — flag-gated | | Roles | `D O` | r**O**les — flag-gated | | Backups | `D B` | platform-only | | Migrations | `D M` | | External-link sub-menu items (Policies, Wrappers, Webhooks, Security Advisor, Performance Advisor, Query Performance) are intentionally not chorded — they route out of `/database/*` and don't belong to the section's namespace. ## Collision audit Other shortcuts active on database pages (table-list, schema-visualizer) were checked against the new chords: - **Schema Visualizer** (`/database/schemas`): `D P` (Download PNG), `D S` (Download SVG), `O A`, `O S`. Publications and Settings were remapped to `D U` and `D ,` to avoid the `D P` / `D S` clashes. - **List pages** (`/database/tables`, etc.): `Shift+F`, `Shift+N`, `O S`, `F C` — no overlap with `D + <letter>`. ## Files - `state/shortcuts/registry/database-nav.ts` — new registry module with the 14 chord definitions. - `state/shortcuts/registry.ts` — spreads the new IDs/definitions into the canonical registry. - `components/interfaces/DatabaseNavShortcuts.tsx` — null-rendering hook component that wires `useShortcut` for each chord, keyed off `useGenerateDatabaseMenu` so URLs and feature gating stay in sync with the sidebar. - `components/layouts/DatabaseLayout/DatabaseLayout.tsx` — mounts the component. - `components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx` — tags each menu item with its `shortcutId`. - `components/ui/ProductMenu/ProductMenu.types.ts` — adds optional `shortcutId?: ShortcutId` field. - `components/ui/ProductMenu/ProductMenuItem.tsx` — renders the hover tooltip when an item has a `shortcutId`, anchored to the label span. ## Test plan - [ ] On `/project/<ref>/database/tables`, press `D F` — navigates to `/database/functions`. - [ ] On `/project/<ref>/database/schemas`, press `D P` — downloads the PNG (Schema Visualizer wins, no nav conflict). - [ ] On `/project/<ref>/database/schemas`, press `D U` — navigates to `/database/publications`. - [ ] On `/project/<ref>/database/tables`, press `D ,` — navigates to `/database/settings`. - [ ] Hover any sub-menu item with a chord — pill appears next to the label after ~1s. - [ ] On a project with the Replication flag off — `D L` does nothing. - [ ] Navigate to `/auth` — pressing `D F` does nothing (chord unmounts with the layout). - [ ] Type `D` then `F` slowly inside an input — does not navigate (input-focus guard).
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
import { useRouter } from 'next/router'
|
|
import { useCallback, useMemo } from 'react'
|
|
|
|
import { useGenerateDatabaseMenu } from '@/components/layouts/DatabaseLayout/DatabaseMenu.utils'
|
|
import { SHORTCUT_IDS, type ShortcutId } from '@/state/shortcuts/registry'
|
|
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
|
|
|
export const DatabaseNavShortcuts = () => {
|
|
const router = useRouter()
|
|
const groups = useGenerateDatabaseMenu()
|
|
|
|
const urlByShortcut = useMemo(() => {
|
|
const map = new Map<ShortcutId, string>()
|
|
for (const group of groups) {
|
|
for (const item of group.items) {
|
|
if (item.shortcutId && item.url) map.set(item.shortcutId, item.url)
|
|
}
|
|
}
|
|
return map
|
|
}, [groups])
|
|
|
|
const navigate = useCallback(
|
|
(id: ShortcutId) => {
|
|
const url = urlByShortcut.get(id)
|
|
if (url) router.push(url)
|
|
},
|
|
[router, urlByShortcut]
|
|
)
|
|
|
|
useShortcut(SHORTCUT_IDS.NAV_DATABASE_TABLES, () => navigate(SHORTCUT_IDS.NAV_DATABASE_TABLES), {
|
|
enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_TABLES),
|
|
})
|
|
useShortcut(
|
|
SHORTCUT_IDS.NAV_DATABASE_FUNCTIONS,
|
|
() => navigate(SHORTCUT_IDS.NAV_DATABASE_FUNCTIONS),
|
|
{ enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_FUNCTIONS) }
|
|
)
|
|
useShortcut(
|
|
SHORTCUT_IDS.NAV_DATABASE_TRIGGERS,
|
|
() => navigate(SHORTCUT_IDS.NAV_DATABASE_TRIGGERS),
|
|
{ enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_TRIGGERS) }
|
|
)
|
|
useShortcut(
|
|
SHORTCUT_IDS.NAV_DATABASE_INDEXES,
|
|
() => navigate(SHORTCUT_IDS.NAV_DATABASE_INDEXES),
|
|
{ enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_INDEXES) }
|
|
)
|
|
useShortcut(
|
|
SHORTCUT_IDS.NAV_DATABASE_EXTENSIONS,
|
|
() => navigate(SHORTCUT_IDS.NAV_DATABASE_EXTENSIONS),
|
|
{ enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_EXTENSIONS) }
|
|
)
|
|
useShortcut(
|
|
SHORTCUT_IDS.NAV_DATABASE_SCHEMA_VISUALIZER,
|
|
() => navigate(SHORTCUT_IDS.NAV_DATABASE_SCHEMA_VISUALIZER),
|
|
{ enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_SCHEMA_VISUALIZER) }
|
|
)
|
|
useShortcut(SHORTCUT_IDS.NAV_DATABASE_ROLES, () => navigate(SHORTCUT_IDS.NAV_DATABASE_ROLES), {
|
|
enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_ROLES),
|
|
})
|
|
useShortcut(
|
|
SHORTCUT_IDS.NAV_DATABASE_BACKUPS,
|
|
() => navigate(SHORTCUT_IDS.NAV_DATABASE_BACKUPS),
|
|
{ enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_BACKUPS) }
|
|
)
|
|
useShortcut(
|
|
SHORTCUT_IDS.NAV_DATABASE_MIGRATIONS,
|
|
() => navigate(SHORTCUT_IDS.NAV_DATABASE_MIGRATIONS),
|
|
{ enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_MIGRATIONS) }
|
|
)
|
|
useShortcut(SHORTCUT_IDS.NAV_DATABASE_TYPES, () => navigate(SHORTCUT_IDS.NAV_DATABASE_TYPES), {
|
|
enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_TYPES),
|
|
})
|
|
useShortcut(
|
|
SHORTCUT_IDS.NAV_DATABASE_PUBLICATIONS,
|
|
() => navigate(SHORTCUT_IDS.NAV_DATABASE_PUBLICATIONS),
|
|
{ enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_PUBLICATIONS) }
|
|
)
|
|
useShortcut(
|
|
SHORTCUT_IDS.NAV_DATABASE_COLUMN_PRIVILEGES,
|
|
() => navigate(SHORTCUT_IDS.NAV_DATABASE_COLUMN_PRIVILEGES),
|
|
{ enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_COLUMN_PRIVILEGES) }
|
|
)
|
|
useShortcut(
|
|
SHORTCUT_IDS.NAV_DATABASE_SETTINGS,
|
|
() => navigate(SHORTCUT_IDS.NAV_DATABASE_SETTINGS),
|
|
{ enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_SETTINGS) }
|
|
)
|
|
useShortcut(
|
|
SHORTCUT_IDS.NAV_DATABASE_REPLICATION,
|
|
() => navigate(SHORTCUT_IDS.NAV_DATABASE_REPLICATION),
|
|
{ enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_REPLICATION) }
|
|
)
|
|
|
|
return null
|
|
}
|