feat(studio): add keyboard shortcuts for platform webhooks (#46198)

## What kind of change does this PR introduce?

Feature. Resolves FE-3418

## What is the current behaviour?

Platform webhooks (org and project) have no keyboard shortcut coverage.
Every action requires a mouse click.

## What is the new behaviour?

Adds seven shortcuts across the four UI states of the platform webhooks
pages:

**List page**

| Shortcut | Action |
|---|---|
| `Shift+F` | Focus search input |
| `Shift+N` | Open "New endpoint" sheet |

**Endpoint detail page** (when viewing a specific endpoint)

| Shortcut | Action |
|---|---|
| `Shift+E` | Open the edit sheet |
| `Shift+U` | Copy the endpoint URL |

**Create / edit form sheet**

| Shortcut | Action |
|---|---|
| `Mod+Enter` | Submit the form (create or save) |

**Delivery details sheet** (when a delivery row is open)

| Shortcut | Action |
|---|---|
| `Shift+R` | Retry the delivery (only active for non-success
deliveries) |
| `Shift+C` | Copy the active tab's payload (switches label between
"Copy event payload" / "Copy response payload") |

All shortcuts:

- Are surfaced via `ShortcutTooltip` / `Shortcut` tooltips on their
buttons
- Appear in the keyboard shortcuts reference sheet (`Mod+/`) under a new
**Platform Webhooks** group
- Are gated so they only fire in the appropriate UI state (e.g.
`Shift+E` is disabled while the edit sheet is already open)
- Apply to both the org-level (`/org/[slug]/webhooks`) and project-level
(`/project/[ref]/settings/webhooks`) pages as both use the same
`PlatformWebhooksPage` component

**Shared shortcuts reused** (no new IDs): `LIST_PAGE_FOCUS_SEARCH`,
`LIST_PAGE_NEW_ITEM`, `ACTION_BAR_SAVE`.

## To test

The platform webhooks UI is behind a feature flag for internal folks.
Enable it in Studio via **Account → Feature Previews → Platform
Webhooks**. The backend is not yet integrated, so you can test all the
shortcuts on the 1–2 mock endpoints (and their deliveries) that appear.

**List page** (`/org/[slug]/webhooks` or
`/project/[ref]/settings/webhooks`):
- [ ] `Shift+F` moves focus to the search input
- [ ] `Shift+N` opens the "New endpoint" sheet (tooltip visible on hover
of the button)

**New endpoint sheet**:
- [ ] Fill in a name and a valid URL, select at least one event type
- [ ] `Mod+Enter` submits and creates the endpoint

**Endpoint detail page**:
- [ ] `Shift+E` opens the edit sheet (tooltip visible on the Edit
button)
- [ ] `Shift+U` copies the endpoint URL and shows a toast (tooltip
visible on the copy icon next to the URL)

**Edit sheet**:
- [ ] `Mod+Enter` saves changes

**Delivery details sheet** (click a delivery row to open):
- [ ] `Shift+R` retries a failed/pending delivery (button and shortcut
absent for successful deliveries)
- [ ] On the **Event** tab: `Shift+C` copies the event payload, toast
reads "Copied event payload"
- [ ] On the **Response** tab: `Shift+C` copies the response payload,
toast reads "Copied response payload"
- [ ] Tooltip on both Copy buttons reflects the active tab label

**Shortcuts reference sheet** (`Mod+/`):
- [ ] A **Platform Webhooks** group appears when on an endpoint detail
page or with the delivery sheet open with the relevant shortcuts listed
- [ ] The basic shortcuts are shown under **List pages** when on the
root Webhooks page

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Danny WhiteandClaude Sonnet 4.6 authored and GitHub committed 2026-05-22 02:37:12 +00:00
1 parent 38e09e1754
commit a27487fe58
13 files changed
+254 -44

No files matched your search

+1 -1
View File
@@ -55,6 +55,6 @@ Path-specific rules in `.github/instructions/`:
- **E2E Tests**: `studio-e2e-tests.instructions.md` — selector priority, anti-patterns (`waitForTimeout`, `force: true`)
- **Composition Patterns**: `studio-composition-patterns.instructions.md` — avoid boolean props, use compound components
- **shadcn/Radix Components**: `studio-shadcn-components.instructions.md` — accessibility handled by primitives, do not flag
- **Keyboard Shortcuts**: `studio-shortcuts.instructions.md` — shared registry usage, discovery, collision checks
- **Keyboard Shortcuts**: `studio-shortcuts.instructions.md` — shortcut registry pattern, search-input escape handler, when to flag missing coverage
These files are scoped to `apps/studio/` and applied automatically during reviews.
@@ -15,23 +15,42 @@ When Studio UI changes introduce or materially alter repeated user actions, cons
- PR adds a primary repeated action, toolbar action, list/table operation, or sub-page navigation without considering shortcut coverage.
- PR adds a one-off `keydown` listener for a normal Studio action instead of using the shortcut registry and `useShortcut`.
- PR registers a shortcut but does not expose it via `ShortcutTooltip`, `ShortcutBadge`, or command-menu badge where the action is visible.
- PR wires `useShortcut` and `ShortcutTooltip` separately for a single visible element instead of using the `<Shortcut>` wrapper.
- PR uses `G then ...` for a non-navigation action.
- PR adds a broad `Mod+letter` shortcut that overlaps common browser, editor, system, copy/save/search, or devtools behavior.
- PR adds a broad `Mod+letter` shortcut that overlaps common browser, editor, system, copy/save/search, or devtools behaviour.
- PR adds a shortcut without checking existing registry and non-registry listeners for collisions.
- PR adds a search or filter input with custom Escape handling instead of `onSearchInputEscape` from `@/lib/keyboard`.
- PR adds a search/filter `<Input>` without `onKeyDown={onSearchInputEscape(...)}` — see **Search Inputs** below.
## Preferred Pattern
- Add definitions in `apps/studio/state/shortcuts/registry.ts` or `apps/studio/state/shortcuts/registry/*`.
- Add or reuse a cheatsheet group in `apps/studio/state/shortcuts/referenceGroups.ts` when a shortcut belongs to a new surface; prefer existing groups for global actions, navigation, and established feature surfaces.
- Register with `useShortcut`.
- For a single visible element that owns the action (button, icon button, menu trigger), prefer the `<Shortcut>` wrapper in `apps/studio/components/ui/Shortcut.tsx` — it binds `useShortcut` and `ShortcutTooltip` from one `id` so the hotkey and tooltip can't drift. Drop down to `useShortcut` + `ShortcutTooltip`/`ShortcutBadge` separately only when the trigger and the visible affordance live on different elements.
- Gate availability with `enabled`.
- Use `showInSettings: false` for contextual shortcuts that only work inside a page state, panel, sheet, or selected-row mode.
- Surface visible actions with `ShortcutTooltip` or `ShortcutBadge`.
- For sheet-owned actions, mount the shortcut from the sheet or a sheet-owned hook; gate with `enabled` when the action only applies while the sheet is open. See `apps/studio/components/interfaces/ConnectSheet/useConnectSheetShortcut.ts`.
- For search/filter inputs, wire `onKeyDown` to `onSearchInputEscape(value, setValue)` from `@/lib/keyboard` so Escape clears the value, then blurs on a second press, and doesn't bubble to a parent dialog/popover. Don't re-implement this with a local `keydown` listener.
- Prefer scoped, mnemonic sequential chords over global modifier chords.
- Set `showInSettings: false` on contextual shortcuts (scoped to a specific page state, sheet, or panel).
- When a shortcut group should appear in the reference sheet (`Mod+/`), add the group key to `SHORTCUT_REFERENCE_GROUP_ORDER` in `apps/studio/state/shortcuts/referenceGroups.ts` and a human label to `GROUP_LABELS` in `ShortcutsReferenceSheet.tsx`.
- For sheet-scoped shortcuts (active only while a `<Sheet>` is open), mount `useShortcut` inside the sheet component gated by the `open` prop — see `apps/studio/components/interfaces/ConnectSheet/useConnectSheetShortcut.ts` as the canonical example.
## Search Inputs
Every `<Input>` used as a search or filter field must include the staged-Escape handler from `apps/studio/lib/keyboard.ts`:
```tsx
import { onSearchInputEscape } from '@/lib/keyboard'
;<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={onSearchInputEscape(query, setQuery)}
/>
```
Behaviour:
- **Escape while the input has a value** → clears the value, keeps focus (so a second Escape then blurs)
- **Escape while the input is empty** → blurs the input
- Stops propagation on Escape so the keystroke does not accidentally close a parent dialog or sheet
When pairing with `useShortcut(LIST_PAGE_FOCUS_SEARCH, ...)` to focus a search input via keyboard, always also add `onSearchInputEscape` on the same input — focus and escape-to-blur are always a pair.
Canonical implementation context: `apps/studio/state/shortcuts/registry.ts`, `apps/studio/state/shortcuts/useShortcut.tsx`, and `apps/studio/components/ui/Shortcut*.tsx`
@@ -22,6 +22,10 @@ import type { WebhookDelivery } from './PlatformWebhooks.types'
import { formatDeliveryStatus, statusBadgeVariant } from './PlatformWebhooksView.utils'
import { getStatusLevel } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.utils'
import { DataTableColumnStatusCode } from '@/components/ui/DataTable/DataTableColumn/DataTableColumnStatusCode'
import { Shortcut } from '@/components/ui/Shortcut'
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface PlatformWebhooksDeliveryDetailsSheetProps {
deliveryAttempt: number | null
@@ -51,6 +55,18 @@ export const PlatformWebhooksDeliveryDetailsSheet = ({
const retryableDelivery =
selectedDelivery && selectedDelivery.status !== 'success' ? selectedDelivery : null
const activePayload =
deliveryDetailsTab === 'event' ? deliveryEventPayload : deliveryResponsePayload
const activePayloadLabel = deliveryDetailsTab === 'event' ? 'event payload' : 'response payload'
const copyPayloadShortcutLabel =
deliveryDetailsTab === 'event' ? 'Copy event payload' : 'Copy response payload'
useShortcut(
SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_PAYLOAD,
() => onCopy(activePayload, activePayloadLabel),
{ enabled: open, label: copyPayloadShortcutLabel }
)
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent size="default" className="flex flex-col gap-0">
@@ -129,13 +145,18 @@ export const PlatformWebhooksDeliveryDetailsSheet = ({
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<p className="text-sm text-foreground-light">Payload</p>
<Button
type="text"
icon={<Copy size={14} />}
onClick={() => onCopy(deliveryEventPayload, 'event payload')}
<ShortcutTooltip
shortcutId={SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_PAYLOAD}
label="Copy event payload"
>
Copy
</Button>
<Button
type="text"
icon={<Copy size={14} />}
onClick={() => onCopy(deliveryEventPayload, 'event payload')}
>
Copy
</Button>
</ShortcutTooltip>
</div>
<div className="rounded-md border border-default bg-surface-200 p-3">
<pre className="whitespace-pre-wrap text-xs text-foreground">
@@ -167,13 +188,18 @@ export const PlatformWebhooksDeliveryDetailsSheet = ({
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<p className="text-sm text-foreground-light">Response payload</p>
<Button
type="text"
icon={<Copy size={14} />}
onClick={() => onCopy(deliveryResponsePayload, 'response payload')}
<ShortcutTooltip
shortcutId={SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_PAYLOAD}
label="Copy response payload"
>
Copy
</Button>
<Button
type="text"
icon={<Copy size={14} />}
onClick={() => onCopy(deliveryResponsePayload, 'response payload')}
>
Copy
</Button>
</ShortcutTooltip>
</div>
<div className="rounded-md border border-default bg-surface-200 p-3">
<pre className="whitespace-pre-wrap text-xs text-foreground">
@@ -189,13 +215,18 @@ export const PlatformWebhooksDeliveryDetailsSheet = ({
{retryableDelivery && (
<SheetFooter className="shrink-0">
<Button
type="default"
icon={<RotateCcw />}
onClick={() => onRetryDelivery(retryableDelivery.id)}
<Shortcut
id={SHORTCUT_IDS.PLATFORM_WEBHOOKS_RETRY_DELIVERY}
onTrigger={() => onRetryDelivery(retryableDelivery.id)}
>
Retry delivery
</Button>
<Button
type="default"
icon={<RotateCcw />}
onClick={() => onRetryDelivery(retryableDelivery.id)}
>
Retry delivery
</Button>
</Shortcut>
</SheetFooter>
)}
</SheetContent>
@@ -10,6 +10,14 @@ vi.mock('@/components/ui/DataTable/DataTableColumn/DataTableColumnStatusCode', (
DataTableColumnStatusCode: ({ value }: { value: number }) => <span>{value}</span>,
}))
vi.mock('@/components/ui/ShortcutTooltip', () => ({
ShortcutTooltip: ({ children }: { children: unknown }) => <>{children}</>,
}))
vi.mock('@/state/shortcuts/useShortcut', () => ({
useShortcut: vi.fn(),
}))
vi.mock('@/components/ui/ButtonTooltip', () => ({
ButtonTooltip: ({
icon,
@@ -51,6 +59,7 @@ describe('PlatformWebhooksEndpointDetails', () => {
deliverySearch=""
filteredDeliveries={allDeliveries}
selectedEndpoint={selectedEndpoint}
onCopyUrl={vi.fn()}
onDeliverySearchChange={vi.fn()}
onOpenDelivery={vi.fn()}
onRetryDelivery={vi.fn()}
@@ -106,6 +115,7 @@ describe('PlatformWebhooksEndpointDetails', () => {
deliverySearch="project"
filteredDeliveries={projectDeliveries}
selectedEndpoint={selectedEndpoint}
onCopyUrl={vi.fn()}
onDeliverySearchChange={vi.fn()}
onOpenDelivery={vi.fn()}
onRetryDelivery={vi.fn()}
@@ -8,8 +8,8 @@ import {
type PaginationState,
type SortingState,
} from '@tanstack/react-table'
import { ChevronLeft, ChevronRight, RotateCcw, Search } from 'lucide-react'
import { useEffect, useState, type ReactNode } from 'react'
import { ChevronLeft, ChevronRight, Copy, RotateCcw, Search } from 'lucide-react'
import { useEffect, useRef, useState, type ReactNode } from 'react'
import {
Badge,
Button,
@@ -32,6 +32,10 @@ import { statusBadgeVariant } from './PlatformWebhooksView.utils'
import { getStatusLevel } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.utils'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { DataTableColumnStatusCode } from '@/components/ui/DataTable/DataTableColumn/DataTableColumnStatusCode'
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
import { onSearchInputEscape } from '@/lib/keyboard'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface DetailItemProps {
label: string
@@ -50,6 +54,7 @@ interface PlatformWebhooksEndpointDetailsProps {
deliverySearch: string
filteredDeliveries: WebhookDelivery[]
selectedEndpoint: WebhookEndpoint
onCopyUrl: () => void
onDeliverySearchChange: (value: string) => void
onOpenDelivery: (deliveryId: string) => void
onRetryDelivery: (deliveryId: string) => void
@@ -150,6 +155,7 @@ export const PlatformWebhooksEndpointDetails = ({
deliverySearch,
filteredDeliveries,
selectedEndpoint,
onCopyUrl,
onDeliverySearchChange,
onOpenDelivery,
onRetryDelivery,
@@ -157,7 +163,12 @@ export const PlatformWebhooksEndpointDetails = ({
const hasCustomHeaders = selectedEndpoint.customHeaders.length > 0
const hasName = selectedEndpoint.name.trim().length > 0
const hasDescription = selectedEndpoint.description.trim().length > 0
const deliverySearchRef = useRef<HTMLInputElement>(null)
const [sorting, setSorting] = useState<SortingState>(DEFAULT_DELIVERY_SORTING)
useShortcut(SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH, () => deliverySearchRef.current?.focus(), {
label: 'Search deliveries',
})
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: DELIVERIES_PAGE_SIZE,
@@ -198,8 +209,21 @@ export const PlatformWebhooksEndpointDetails = ({
<dl className="grid grid-cols-1 gap-x-10 gap-y-6 md:grid-cols-2">
{hasName && <DetailItem label="Name">{selectedEndpoint.name}</DetailItem>}
<DetailItem label="URL" ddClassName="text-sm break-all">
{selectedEndpoint.url}
<DetailItem label="URL" ddClassName="flex items-start gap-2 text-sm">
<span className="break-all">{selectedEndpoint.url}</span>
<ShortcutTooltip
shortcutId={SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_ENDPOINT_URL}
label="Copy endpoint URL"
>
<Button
type="text"
size="tiny"
className="mt-0.5 shrink-0 h-5 w-5 p-0"
icon={<Copy size={12} />}
aria-label="Copy endpoint URL"
onClick={onCopyUrl}
/>
</ShortcutTooltip>
</DetailItem>
{hasDescription && (
@@ -250,12 +274,14 @@ export const PlatformWebhooksEndpointDetails = ({
<h2 className="text-foreground text-xl">Deliveries</h2>
<div className="flex items-center justify-between gap-2">
<Input
ref={deliverySearchRef}
placeholder="Search deliveries"
size="tiny"
icon={<Search />}
value={deliverySearch}
className="w-full lg:w-52"
onChange={(event) => onDeliverySearchChange(event.target.value)}
onKeyDown={onSearchInputEscape(deliverySearch, onDeliverySearchChange)}
/>
</div>
<Card className="overflow-hidden">
@@ -1,6 +1,6 @@
import { ChevronRight, Eye, MoreVertical, Plus, Search, Trash2, Webhook } from 'lucide-react'
import { useRouter } from 'next/router'
import { useMemo, useState } from 'react'
import { useMemo, useRef, useState } from 'react'
import {
Badge,
Button,
@@ -22,7 +22,11 @@ import { Input } from 'ui-patterns/DataInputs/Input'
import type { WebhookEndpoint } from './PlatformWebhooks.types'
import { getWebhookEndpointDisplayName } from './PlatformWebhooks.utils'
import { Shortcut } from '@/components/ui/Shortcut'
import { onSearchInputEscape } from '@/lib/keyboard'
import { createNavigationHandler } from '@/lib/navigation'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface PlatformWebhooksEndpointListProps {
filteredEndpoints: WebhookEndpoint[]
@@ -44,6 +48,12 @@ export const PlatformWebhooksEndpointList = ({
onViewEndpoint,
}: PlatformWebhooksEndpointListProps) => {
const router = useRouter()
const searchInputRef = useRef<HTMLInputElement>(null)
useShortcut(SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH, () => searchInputRef.current?.focus(), {
label: 'Search endpoints',
})
const formatEventCount = (eventTypes: string[]) => {
if (eventTypes.includes('*')) return 'All events'
if (eventTypes.length === 1) return '1 event'
@@ -96,17 +106,25 @@ export const PlatformWebhooksEndpointList = ({
<div className="flex items-center justify-between gap-x-2">
<Input
ref={searchInputRef}
placeholder="Search endpoints"
size="tiny"
icon={<Search />}
value={search}
className="w-full lg:w-52"
onChange={(event) => onSearchChange(event.target.value)}
onKeyDown={onSearchInputEscape(search, onSearchChange)}
/>
<Button type="primary" icon={<Plus />} onClick={onCreateEndpoint}>
New endpoint
</Button>
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_NEW_ITEM}
label="New endpoint"
onTrigger={onCreateEndpoint}
>
<Button type="primary" icon={<Plus />} onClick={onCreateEndpoint}>
New endpoint
</Button>
</Shortcut>
</div>
{filteredEndpoints.length === 0 ? (
@@ -42,8 +42,10 @@ import type {
import { generateWebhookEndpointName } from './PlatformWebhooks.utils'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { InlineLink } from '@/components/ui/InlineLink'
import { Shortcut } from '@/components/ui/Shortcut'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { httpEndpointUrlSchema } from '@/lib/validation/http-url'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
const endpointFormSchema = z
.object({
@@ -583,9 +585,15 @@ export const PlatformWebhooksEndpointSheet = ({
<Button type="default" onClick={confirmOnClose}>
Cancel
</Button>
<Button form="platform-webhook-endpoint-form" htmlType="submit">
{mode === 'create' ? 'Create endpoint' : 'Save changes'}
</Button>
<Shortcut
id={SHORTCUT_IDS.ACTION_BAR_SAVE}
label={mode === 'create' ? 'Create endpoint' : 'Save changes'}
onTrigger={() => form.handleSubmit(onSubmit)()}
>
<Button form="platform-webhook-endpoint-form" htmlType="submit">
{mode === 'create' ? 'Create endpoint' : 'Save changes'}
</Button>
</Shortcut>
</SheetFooter>
</SheetContent>
<DiscardChangesConfirmationDialog {...discardChangesModalProps} />
@@ -51,7 +51,10 @@ import {
} from './PlatformWebhooksPage.utils'
import { useIsPlatformWebhooksEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { InlineLink } from '@/components/ui/InlineLink'
import { Shortcut } from '@/components/ui/Shortcut'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
const PANEL_VALUES = ['create', 'edit'] as const
@@ -295,6 +298,12 @@ export const PlatformWebhooksPage = ({ scope, endpointId }: PlatformWebhooksPage
const isEndpointSheetOpen = panel === 'create' || (panel === 'edit' && !!selectedEndpoint)
useShortcut(
SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_ENDPOINT_URL,
() => selectedEndpoint && handleCopy(selectedEndpoint.url, 'endpoint URL'),
{ enabled: isEndpointView && !isEndpointSheetOpen }
)
useEffect(() => {
if (!selectedEndpoint && !!deliveryId) {
setDeliveryId(null)
@@ -324,16 +333,25 @@ export const PlatformWebhooksPage = ({ scope, endpointId }: PlatformWebhooksPage
endpointActions={
selectedEndpoint ? (
<>
<Button
type="default"
icon={<Pencil size={14} />}
onClick={() => {
<Shortcut
id={SHORTCUT_IDS.PLATFORM_WEBHOOKS_EDIT_ENDPOINT}
onTrigger={() => {
setEditEnabledOverride(null)
setPanel('edit')
}}
options={{ enabled: !isEndpointSheetOpen }}
>
Edit
</Button>
<Button
type="default"
icon={<Pencil size={14} />}
onClick={() => {
setEditEnabledOverride(null)
setPanel('edit')
}}
>
Edit
</Button>
</Shortcut>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" icon={<EllipsisVertical />} className="w-7" />
@@ -383,6 +401,7 @@ export const PlatformWebhooksPage = ({ scope, endpointId }: PlatformWebhooksPage
deliverySearch={deliverySearch}
filteredDeliveries={filteredDeliveries}
selectedEndpoint={selectedEndpoint}
onCopyUrl={() => handleCopy(selectedEndpoint.url, 'endpoint URL')}
onDeliverySearchChange={setDeliverySearch}
onOpenDelivery={(id) => {
setDeliveryDetailsTab('event')
@@ -74,6 +74,14 @@ const ACTIVE_SURFACE_SHORTCUT_IDS = [
SHORTCUT_IDS.STORAGE_EXPLORER_REFRESH,
] satisfies ShortcutId[]
const ACTIVE_PLATFORM_WEBHOOKS_SHORTCUT_IDS = [
...ACTIVE_SHORTCUT_IDS,
SHORTCUT_IDS.PLATFORM_WEBHOOKS_EDIT_ENDPOINT,
SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_ENDPOINT_URL,
SHORTCUT_IDS.PLATFORM_WEBHOOKS_RETRY_DELIVERY,
SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_PAYLOAD,
] satisfies ShortcutId[]
let sequenceIdCounter = 0
const buildSequenceRegistration = (id: ShortcutId): SequenceRegistrationView => {
@@ -242,6 +250,17 @@ describe('ShortcutsReferenceSheet', () => {
expect(screen.queryByText('storage-explorer')).not.toBeInTheDocument()
})
it('shows the platform webhooks section with human labels when webhooks shortcuts are active', async () => {
renderShortcutsReferenceSheet(ACTIVE_PLATFORM_WEBHOOKS_SHORTCUT_IDS)
expect(await screen.findByText('Platform Webhooks')).toBeInTheDocument()
expect(screen.getByText('Edit endpoint')).toBeInTheDocument()
expect(screen.getByText('Copy endpoint URL')).toBeInTheDocument()
expect(screen.getByText('Retry delivery')).toBeInTheDocument()
expect(screen.getByText('Copy payload')).toBeInTheDocument()
expect(screen.queryByText('platform-webhooks')).not.toBeInTheDocument()
})
it('does not show inactive database shortcuts in search results', async () => {
const user = userEvent.setup()
@@ -51,6 +51,7 @@ const GROUP_LABELS: Record<string, string> = {
'functions-overview': 'Edge Function Overview',
'inline-editor': 'Inline Editor',
'list-page': 'List pages',
'platform-webhooks': 'Platform Webhooks',
'logs-preview': 'Logs Explorer',
nav: 'Navigation',
'operation-queue': 'Operation Queue',
@@ -48,6 +48,7 @@ export const SHORTCUT_REFERENCE_GROUP_ORDER = [
'table-editor',
'schema-visualizer',
'list-page',
'platform-webhooks',
'action-bar',
'operation-queue',
'unified-logs',
+10
View File
@@ -15,6 +15,10 @@ import {
} from './registry/functions-overview'
import { LIST_PAGE_SHORTCUT_IDS, listPageRegistry } from './registry/list-page'
import { LOGS_PREVIEW_SHORTCUT_IDS, logsPreviewRegistry } from './registry/logs-preview'
import {
PLATFORM_WEBHOOKS_SHORTCUT_IDS,
platformWebhooksRegistry,
} from './registry/platform-webhooks'
import {
REALTIME_INSPECTOR_SHORTCUT_IDS,
realtimeInspectorRegistry,
@@ -123,6 +127,9 @@ export const SHORTCUT_IDS = {
// LogsPreviewer shortcuts (Function Logs, Function Invocations, Logs Explorer)
...LOGS_PREVIEW_SHORTCUT_IDS,
// Platform Webhooks page shortcuts (org and project level)
...PLATFORM_WEBHOOKS_SHORTCUT_IDS,
} as const
/**
@@ -443,4 +450,7 @@ export const SHORTCUT_DEFINITIONS: Record<ShortcutId, ShortcutDefinition> = {
// LogsPreviewer shortcut registration
...logsPreviewRegistry,
// Platform Webhooks page shortcut registration
...platformWebhooksRegistry,
}
@@ -0,0 +1,48 @@
import { RegistryDefinations } from '../types'
/**
* Shortcuts scoped to the Platform Webhooks pages (org and project level).
*
* List-page actions (focus search, create new endpoint) reuse the shared
* `list-page` IDs directly — only webhook-specific actions live here.
*/
export const PLATFORM_WEBHOOKS_SHORTCUT_IDS = {
PLATFORM_WEBHOOKS_EDIT_ENDPOINT: 'platform-webhooks.edit-endpoint',
PLATFORM_WEBHOOKS_COPY_ENDPOINT_URL: 'platform-webhooks.copy-endpoint-url',
PLATFORM_WEBHOOKS_RETRY_DELIVERY: 'platform-webhooks.retry-delivery',
PLATFORM_WEBHOOKS_COPY_PAYLOAD: 'platform-webhooks.copy-payload',
}
export type PlatformWebhooksShortcutId =
(typeof PLATFORM_WEBHOOKS_SHORTCUT_IDS)[keyof typeof PLATFORM_WEBHOOKS_SHORTCUT_IDS]
export const platformWebhooksRegistry: RegistryDefinations<PlatformWebhooksShortcutId> = {
[PLATFORM_WEBHOOKS_SHORTCUT_IDS.PLATFORM_WEBHOOKS_EDIT_ENDPOINT]: {
id: PLATFORM_WEBHOOKS_SHORTCUT_IDS.PLATFORM_WEBHOOKS_EDIT_ENDPOINT,
label: 'Edit endpoint',
sequence: ['Shift+E'],
showInSettings: false,
options: { ignoreInputs: true, registerInCommandMenu: true },
},
[PLATFORM_WEBHOOKS_SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_ENDPOINT_URL]: {
id: PLATFORM_WEBHOOKS_SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_ENDPOINT_URL,
label: 'Copy endpoint URL',
sequence: ['Shift+U'],
showInSettings: false,
options: { ignoreInputs: true, registerInCommandMenu: true },
},
[PLATFORM_WEBHOOKS_SHORTCUT_IDS.PLATFORM_WEBHOOKS_RETRY_DELIVERY]: {
id: PLATFORM_WEBHOOKS_SHORTCUT_IDS.PLATFORM_WEBHOOKS_RETRY_DELIVERY,
label: 'Retry delivery',
sequence: ['Shift+R'],
showInSettings: false,
options: { ignoreInputs: true, registerInCommandMenu: true },
},
[PLATFORM_WEBHOOKS_SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_PAYLOAD]: {
id: PLATFORM_WEBHOOKS_SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_PAYLOAD,
label: 'Copy payload',
sequence: ['Shift+C'],
showInSettings: false,
options: { ignoreInputs: true, registerInCommandMenu: true },
},
}