mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
38e09e1754
commit
a27487fe58
13 files changed
+254
-44
No files matched your search
@@ -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`
|
||||
+49
-18
@@ -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
@@ -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()}
|
||||
|
||||
+30
-4
@@ -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">
|
||||
|
||||
+22
-4
@@ -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 ? (
|
||||
|
||||
+11
-3
@@ -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',
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user