From 9791f65a185b9c26fa6e49a5c7d9813d9f1778d7 Mon Sep 17 00:00:00 2001
From: hallidayo <22655069+Hallidayo@users.noreply.github.com>
Date: Mon, 13 Apr 2026 10:00:19 +0100
Subject: [PATCH] feat: connect sheet deep linking (#44021)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.
YES
## What kind of change does this PR introduce?
Supabase Dashboard - Connect
## What is the current behavior?
On the new `` component their is no deep linking like
the previous `` component
## What is the new behavior?
Deep linking added onto framework and other options, Example local
links:
http://localhost:8082/project/default?showConnect=true&connectTab=framework&framework=nextjs&using=pages
http://localhost:8082/project/default?showConnect=true&connectTab=mcp&mcpClient=goose
## Summary by CodeRabbit
* **New Features**
* Connect Sheet supports URL query parameters for pre-configuring
connection settings (framework, using, method, type, mcpClient).
* Legacy tab identifiers are accepted for compatibility.
* **Improvements**
* Opening, switching, and closing the Connect Sheet now more reliably
syncs and clears related parameters to avoid stale state.
* **Tests**
* Added end-to-end tests covering deep-linking, legacy aliases, and
parameter clearing on close/mode change.
---------
Co-authored-by: Joshen Lim
---
.../interfaces/ConnectSheet/ConnectSheet.tsx | 83 ++++++++++-
e2e/studio/features/connect.spec.ts | 130 ++++++++++++++++++
2 files changed, 207 insertions(+), 6 deletions(-)
diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx
index 16844bd955..d01013598f 100644
--- a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx
+++ b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx
@@ -20,6 +20,19 @@ function isConnectMode(value: string): value is ConnectMode {
return CONNECT_MODES.some((mode) => mode === value)
}
+function mapConnectTabToMode(tab: string | null): ConnectMode | null {
+ if (!tab) return null
+ switch (tab) {
+ case 'frameworks':
+ case 'mobiles':
+ return 'framework'
+ case 'orms':
+ return 'orm'
+ default:
+ return isConnectMode(tab) ? tab : null
+ }
+}
+
export const ConnectSheet = () => {
const { ref: projectRef } = useParams()
@@ -30,6 +43,11 @@ export const ConnectSheet = () => {
parseAsBoolean.withDefault(false)
)
const [connectTab, setConnectTab] = useQueryState('connectTab', parseAsString)
+ const [queryFramework, setQueryFramework] = useQueryState('framework', parseAsString)
+ const [queryUsing, setQueryUsing] = useQueryState('using', parseAsString)
+ const [queryMethod, setQueryMethod] = useQueryState('method', parseAsString)
+ const [queryType, setQueryType] = useQueryState('type', parseAsString)
+ const [queryMcpClient, setQueryMcpClient] = useQueryState('mcpClient', parseAsString)
const { connectSheetSource, setConnectSheetSource } = useAppStateSnapshot()
const track = useTrack()
const prevShowConnect = useRef(false)
@@ -46,23 +64,51 @@ export const ConnectSheet = () => {
track('connect_sheet_opened', { source: connectSheetSource })
setConnectSheetSource('header_button')
- if (connectTab && isConnectMode(connectTab) && availableModeIds.includes(connectTab)) {
- setMode(connectTab)
+ const mappedMode = mapConnectTabToMode(connectTab)
+ if (mappedMode && availableModeIds.includes(mappedMode)) {
+ setMode(mappedMode)
+ }
+
+ if (mappedMode === 'framework') {
+ if (queryFramework) {
+ updateField('framework', queryFramework)
+ if (queryUsing) updateField('frameworkVariant', queryUsing)
+ }
+ } else if (mappedMode === 'orm') {
+ if (queryFramework) updateField('orm', queryFramework)
+ } else if (mappedMode === 'direct') {
+ if (queryMethod) updateField('connectionMethod', queryMethod)
+ if (queryType) updateField('connectionType', queryType)
+ } else if (mappedMode === 'mcp') {
+ if (queryMcpClient) updateField('mcpClient', queryMcpClient)
}
}, [
showConnect,
connectSheetSource,
connectTab,
+ queryFramework,
+ queryUsing,
+ queryMethod,
+ queryType,
+ queryMcpClient,
availableModeIds,
track,
setConnectSheetSource,
setMode,
+ updateField,
])
+ const clearAllQueryParams = () => {
+ setConnectTab(null)
+ setQueryFramework(null)
+ setQueryUsing(null)
+ setQueryMethod(null)
+ setQueryType(null)
+ setQueryMcpClient(null)
+ }
+
const handleOpenChange = (sheetOpen: boolean) => {
- if (!sheetOpen) {
- setConnectTab(null)
- }
+ if (!sheetOpen) clearAllQueryParams()
setShowConnect(sheetOpen)
}
@@ -93,6 +139,31 @@ export const ConnectSheet = () => {
const handleModeChange = (mode: ConnectMode) => {
setMode(mode)
setConnectTab(mode)
+ setQueryFramework(null)
+ setQueryUsing(null)
+ setQueryMethod(null)
+ setQueryType(null)
+ setQueryMcpClient(null)
+ }
+
+ const handleFieldChange = (fieldId: string, value: string | boolean | string[]) => {
+ updateField(fieldId, value)
+ const str = String(value)
+ if (fieldId === 'framework') {
+ setQueryFramework(str)
+ setQueryUsing(null)
+ } else if (fieldId === 'frameworkVariant') {
+ setQueryUsing(str)
+ } else if (fieldId === 'orm') {
+ setQueryFramework(str)
+ } else if (fieldId === 'connectionMethod') {
+ setQueryMethod(str)
+ setQueryType(null)
+ } else if (fieldId === 'connectionType') {
+ setQueryType(str)
+ } else if (fieldId === 'mcpClient') {
+ setQueryMcpClient(str)
+ }
}
return (
@@ -116,7 +187,7 @@ export const ConnectSheet = () => {
diff --git a/e2e/studio/features/connect.spec.ts b/e2e/studio/features/connect.spec.ts
index 893657634f..0cb908f774 100644
--- a/e2e/studio/features/connect.spec.ts
+++ b/e2e/studio/features/connect.spec.ts
@@ -54,3 +54,133 @@ test.describe('Connect', async () => {
await expect(page).toHaveURL(/showConnect=true/)
})
})
+
+test.describe('Connect Sheet deep linking', async () => {
+ test('pre-selects framework and variant from URL params', async ({ page, ref }) => {
+ await page.goto(
+ toUrl(
+ `/project/${ref}?showConnect=true&connectTab=framework&framework=nextjs&using=pages`
+ )
+ )
+
+ await expect(
+ page.getByRole('heading', { name: 'Connect to your project' }),
+ 'ConnectSheet should open from deep link'
+ ).toBeVisible({ timeout: 30000 })
+
+ await expect(
+ page.getByRole('combobox').filter({ hasText: 'Next.js' }),
+ 'Framework select should show Next.js'
+ ).toBeVisible()
+
+ await expect(
+ page.getByRole('combobox').filter({ hasText: 'Pages Router' }),
+ 'Variant select should show Pages Router'
+ ).toBeVisible()
+ })
+
+ test('supports legacy frameworks tab alias', async ({ page, ref }) => {
+ await page.goto(
+ toUrl(`/project/${ref}?showConnect=true&connectTab=frameworks&framework=nextjs`)
+ )
+
+ await expect(
+ page.getByRole('heading', { name: 'Connect to your project' }),
+ 'ConnectSheet should open with legacy tab alias'
+ ).toBeVisible({ timeout: 30000 })
+
+ await expect(
+ page.getByRole('combobox').filter({ hasText: 'Next.js' }),
+ 'Framework select should show Next.js via legacy connectTab alias'
+ ).toBeVisible()
+ })
+
+ test('pre-selects ORM from URL params', async ({ page, ref }) => {
+ // Use drizzle (non-default) to verify the param takes effect
+ await page.goto(toUrl(`/project/${ref}?showConnect=true&connectTab=orm&framework=drizzle`))
+
+ await expect(
+ page.getByRole('heading', { name: 'Connect to your project' }),
+ 'ConnectSheet should open from deep link'
+ ).toBeVisible({ timeout: 30000 })
+
+ await expect(
+ page.locator('[data-state="checked"]').filter({ hasText: 'Drizzle' }),
+ 'Drizzle radio should be selected'
+ ).toBeVisible()
+ })
+
+ test('pre-selects MCP client from URL params', async ({ page, ref }) => {
+ await page.goto(toUrl(`/project/${ref}?showConnect=true&connectTab=mcp&mcpClient=goose`))
+
+ await expect(
+ page.getByRole('heading', { name: 'Connect to your project' }),
+ 'ConnectSheet should open from deep link'
+ ).toBeVisible({ timeout: 30000 })
+
+ await expect(
+ page.getByRole('combobox').filter({ hasText: 'Goose' }),
+ 'MCP client select should show Goose'
+ ).toBeVisible()
+ })
+
+ test('pre-selects direct connection method from URL params', async ({ page, ref }) => {
+ await page.goto(
+ toUrl(`/project/${ref}?showConnect=true&connectTab=direct&method=transaction`)
+ )
+
+ await expect(
+ page.getByRole('heading', { name: 'Connect to your project' }),
+ 'ConnectSheet should open from deep link'
+ ).toBeVisible({ timeout: 30000 })
+
+ await expect(
+ page.locator('[data-state="checked"]').filter({ hasText: 'Transaction pooler' }),
+ 'Transaction pooler radio should be selected'
+ ).toBeVisible()
+ })
+
+ test('closing the sheet clears all deep-link params from URL', async ({ page, ref }) => {
+ await page.goto(
+ toUrl(
+ `/project/${ref}?showConnect=true&connectTab=framework&framework=nextjs&using=pages`
+ )
+ )
+
+ await expect(
+ page.getByRole('heading', { name: 'Connect to your project' }),
+ 'ConnectSheet should open'
+ ).toBeVisible({ timeout: 30000 })
+
+ await page.keyboard.press('Escape')
+
+ await expect(
+ page.getByRole('heading', { name: 'Connect to your project' }),
+ 'ConnectSheet should close'
+ ).not.toBeVisible({ timeout: 10000 })
+
+ await expect(page, 'connectTab param should be removed').not.toHaveURL(/connectTab/)
+ await expect(page, 'framework param should be removed').not.toHaveURL(/[?&]framework=/)
+ await expect(page, 'using param should be removed').not.toHaveURL(/using=/)
+ })
+
+ test('changing mode clears previous mode params from URL', async ({ page, ref }) => {
+ await page.goto(
+ toUrl(`/project/${ref}?showConnect=true&connectTab=framework&framework=nextjs`)
+ )
+
+ await expect(
+ page.getByRole('heading', { name: 'Connect to your project' }),
+ 'ConnectSheet should open'
+ ).toBeVisible({ timeout: 30000 })
+
+ await expect(page, 'framework param should be in URL initially').toHaveURL(/framework=nextjs/)
+
+ await page.getByRole('button', { name: /ORM/ }).click()
+
+ await expect(page, 'framework param should be cleared after mode change').not.toHaveURL(
+ /framework=nextjs/
+ )
+ await expect(page, 'connectTab should update to orm').toHaveURL(/connectTab=orm/)
+ })
+})