mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 22:18:00 +08:00
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 `<ConnectSheet />` component their is no deep linking like the previous `<Connect />` 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 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
@@ -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 = () => {
|
||||
<ConnectConfigSection
|
||||
state={state}
|
||||
activeFields={activeFields}
|
||||
onFieldChange={updateField}
|
||||
onFieldChange={handleFieldChange}
|
||||
getFieldOptions={getFieldOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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/)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user