diff --git a/surfsense_backend/app/routes/airtable_add_connector_route.py b/surfsense_backend/app/routes/airtable_add_connector_route.py index 423d61fb2..fe359d2f3 100644 --- a/surfsense_backend/app/routes/airtable_add_connector_route.py +++ b/surfsense_backend/app/routes/airtable_add_connector_route.py @@ -199,7 +199,7 @@ async def airtable_callback( # Redirect to frontend with error parameter if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=airtable_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=airtable_oauth_denied" ) else: return RedirectResponse( @@ -316,7 +316,7 @@ async def airtable_callback( f"Duplicate Airtable connector detected for user {user_id} with email {user_email}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=airtable-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=duplicate_account&connector=airtable-connector" ) # Generate a unique, user-friendly connector name @@ -348,7 +348,7 @@ async def airtable_callback( # Redirect to the frontend with success params for indexing config # Using query params to auto-open the popup with config view on new-chat page return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=airtable-connector&connectorId={new_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=airtable-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/clickup_add_connector_route.py b/surfsense_backend/app/routes/clickup_add_connector_route.py index 1b2e6795d..2cd63eca2 100644 --- a/surfsense_backend/app/routes/clickup_add_connector_route.py +++ b/surfsense_backend/app/routes/clickup_add_connector_route.py @@ -148,7 +148,7 @@ async def clickup_callback( # Redirect to frontend with error parameter if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=clickup_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=clickup_oauth_denied" ) else: return RedirectResponse( @@ -326,7 +326,7 @@ async def clickup_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=clickup-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=clickup-connector" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/composio_routes.py b/surfsense_backend/app/routes/composio_routes.py index e0c6c1f65..61076c666 100644 --- a/surfsense_backend/app/routes/composio_routes.py +++ b/surfsense_backend/app/routes/composio_routes.py @@ -208,7 +208,7 @@ async def composio_callback( if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=composio_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=composio_oauth_denied" ) else: return RedirectResponse( @@ -370,7 +370,7 @@ async def composio_callback( toolkit_id, "composio-connector" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector={frontend_connector_id}&connectorId={existing_connector.id}&view=configure" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector={frontend_connector_id}&connectorId={existing_connector.id}" ) # This is a NEW account - create a new connector @@ -399,7 +399,7 @@ async def composio_callback( toolkit_id, "composio-connector" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector={frontend_connector_id}&connectorId={db_connector.id}&view=configure" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector={frontend_connector_id}&connectorId={db_connector.id}" ) except IntegrityError as e: diff --git a/surfsense_backend/app/routes/confluence_add_connector_route.py b/surfsense_backend/app/routes/confluence_add_connector_route.py index 24e0f858a..f50383860 100644 --- a/surfsense_backend/app/routes/confluence_add_connector_route.py +++ b/surfsense_backend/app/routes/confluence_add_connector_route.py @@ -170,7 +170,7 @@ async def confluence_callback( # Redirect to frontend with error parameter if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=confluence_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=confluence_oauth_denied" ) else: return RedirectResponse( @@ -310,7 +310,7 @@ async def confluence_callback( f"Duplicate Confluence connector detected for user {user_id} with instance {connector_identifier}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=confluence-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=duplicate_account&connector=confluence-connector" ) # Generate a unique, user-friendly connector name @@ -341,7 +341,7 @@ async def confluence_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=confluence-connector&connectorId={new_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=confluence-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/discord_add_connector_route.py b/surfsense_backend/app/routes/discord_add_connector_route.py index e49acf30b..27bfffc90 100644 --- a/surfsense_backend/app/routes/discord_add_connector_route.py +++ b/surfsense_backend/app/routes/discord_add_connector_route.py @@ -172,7 +172,7 @@ async def discord_callback( # Redirect to frontend with error parameter if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=discord_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=discord_oauth_denied" ) else: return RedirectResponse( @@ -311,7 +311,7 @@ async def discord_callback( f"Duplicate Discord connector detected for user {user_id} with server {connector_identifier}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=discord-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=duplicate_account&connector=discord-connector" ) # Generate a unique, user-friendly connector name @@ -342,7 +342,7 @@ async def discord_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=discord-connector&connectorId={new_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=discord-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/google_calendar_add_connector_route.py b/surfsense_backend/app/routes/google_calendar_add_connector_route.py index 08e5c2f04..6b74bc05d 100644 --- a/surfsense_backend/app/routes/google_calendar_add_connector_route.py +++ b/surfsense_backend/app/routes/google_calendar_add_connector_route.py @@ -137,7 +137,7 @@ async def calendar_callback( # Redirect to frontend with error parameter if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=google_calendar_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=google_calendar_oauth_denied" ) else: return RedirectResponse( @@ -210,7 +210,7 @@ async def calendar_callback( f"Duplicate Google Calendar connector detected for user {user_id} with email {user_email}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=google-calendar-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=duplicate_account&connector=google-calendar-connector" ) try: @@ -236,7 +236,7 @@ async def calendar_callback( # Redirect to the frontend with success params for indexing config # Using query params to auto-open the popup with config view on new-chat page return RedirectResponse( - f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=google-calendar-connector&connectorId={db_connector.id}" + f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=google-calendar-connector&connectorId={db_connector.id}" ) except ValidationError as e: await session.rollback() diff --git a/surfsense_backend/app/routes/google_drive_add_connector_route.py b/surfsense_backend/app/routes/google_drive_add_connector_route.py index 9fa83f3a2..23fba8770 100644 --- a/surfsense_backend/app/routes/google_drive_add_connector_route.py +++ b/surfsense_backend/app/routes/google_drive_add_connector_route.py @@ -257,7 +257,7 @@ async def drive_callback( # Redirect to frontend with error parameter if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=google_drive_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=google_drive_oauth_denied" ) else: return RedirectResponse( @@ -360,7 +360,7 @@ async def drive_callback( url=f"{config.NEXT_FRONTEND_URL}{reauth_return_url}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=google-drive-connector&connectorId={db_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=google-drive-connector&connectorId={db_connector.id}" ) is_duplicate = await check_duplicate_connector( @@ -375,7 +375,7 @@ async def drive_callback( f"Duplicate Google Drive connector detected for user {user_id} with email {user_email}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=google-drive-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=duplicate_account&connector=google-drive-connector" ) # Generate a unique, user-friendly connector name @@ -425,7 +425,7 @@ async def drive_callback( ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=google-drive-connector&connectorId={db_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=google-drive-connector&connectorId={db_connector.id}" ) except HTTPException: diff --git a/surfsense_backend/app/routes/google_gmail_add_connector_route.py b/surfsense_backend/app/routes/google_gmail_add_connector_route.py index 19fa019ce..aafe4d271 100644 --- a/surfsense_backend/app/routes/google_gmail_add_connector_route.py +++ b/surfsense_backend/app/routes/google_gmail_add_connector_route.py @@ -168,7 +168,7 @@ async def gmail_callback( # Redirect to frontend with error parameter if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=google_gmail_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=google_gmail_oauth_denied" ) else: return RedirectResponse( @@ -241,7 +241,7 @@ async def gmail_callback( f"Duplicate Gmail connector detected for user {user_id} with email {user_email}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=google-gmail-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=duplicate_account&connector=google-gmail-connector" ) try: @@ -272,7 +272,7 @@ async def gmail_callback( # Redirect to the frontend with success params for indexing config # Using query params to auto-open the popup with config view on new-chat page return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=google-gmail-connector&connectorId={db_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=google-gmail-connector&connectorId={db_connector.id}" ) except IntegrityError as e: diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py index 58903606a..90267bbab 100644 --- a/surfsense_backend/app/routes/jira_add_connector_route.py +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -167,7 +167,7 @@ async def jira_callback( # Redirect to frontend with error parameter if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=jira_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=jira_oauth_denied" ) else: return RedirectResponse( @@ -328,7 +328,7 @@ async def jira_callback( f"Duplicate Jira connector detected for user {user_id} with instance {connector_identifier}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=jira-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=duplicate_account&connector=jira-connector" ) # Generate a unique, user-friendly connector name @@ -359,7 +359,7 @@ async def jira_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=jira-connector&connectorId={new_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=jira-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/linear_add_connector_route.py b/surfsense_backend/app/routes/linear_add_connector_route.py index 119042668..40b800e3b 100644 --- a/surfsense_backend/app/routes/linear_add_connector_route.py +++ b/surfsense_backend/app/routes/linear_add_connector_route.py @@ -230,7 +230,7 @@ async def linear_callback( # Redirect to frontend with error parameter if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=linear_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=linear_oauth_denied" ) else: return RedirectResponse( @@ -367,7 +367,7 @@ async def linear_callback( url=f"{config.NEXT_FRONTEND_URL}{reauth_return_url}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=linear-connector&connectorId={db_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=linear-connector&connectorId={db_connector.id}" ) # Check for duplicate connector (same organization already connected) @@ -383,7 +383,7 @@ async def linear_callback( f"Duplicate Linear connector detected for user {user_id} with org {org_name}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=linear-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=duplicate_account&connector=linear-connector" ) # Generate a unique, user-friendly connector name @@ -415,7 +415,7 @@ async def linear_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=linear-connector&connectorId={new_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=linear-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/notion_add_connector_route.py b/surfsense_backend/app/routes/notion_add_connector_route.py index 46404acb0..953aa2bb8 100644 --- a/surfsense_backend/app/routes/notion_add_connector_route.py +++ b/surfsense_backend/app/routes/notion_add_connector_route.py @@ -227,7 +227,7 @@ async def notion_callback( # Redirect to frontend with error parameter if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=notion_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=notion_oauth_denied" ) else: return RedirectResponse( @@ -365,7 +365,7 @@ async def notion_callback( url=f"{config.NEXT_FRONTEND_URL}{reauth_return_url}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=notion-connector&connectorId={db_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=notion-connector&connectorId={db_connector.id}" ) # Extract unique identifier from connector credentials @@ -386,7 +386,7 @@ async def notion_callback( f"Duplicate Notion connector detected for user {user_id} with workspace {connector_identifier}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=notion-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=duplicate_account&connector=notion-connector" ) # Generate a unique, user-friendly connector name @@ -417,7 +417,7 @@ async def notion_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=notion-connector&connectorId={new_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=notion-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/slack_add_connector_route.py b/surfsense_backend/app/routes/slack_add_connector_route.py index 0cbfdef44..405ab2c4f 100644 --- a/surfsense_backend/app/routes/slack_add_connector_route.py +++ b/surfsense_backend/app/routes/slack_add_connector_route.py @@ -166,7 +166,7 @@ async def slack_callback( # Redirect to frontend with error parameter if space_id: return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=slack_oauth_denied" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=slack_oauth_denied" ) else: return RedirectResponse( @@ -296,7 +296,7 @@ async def slack_callback( f"Duplicate Slack connector detected for user {user_id} with workspace {connector_identifier}" ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=slack-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?error=duplicate_account&connector=slack-connector" ) # Generate a unique, user-friendly connector name @@ -328,7 +328,7 @@ async def slack_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=slack-connector&connectorId={new_connector.id}" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/connectors/callback?success=true&connector=slack-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/callback/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/callback/page.tsx new file mode 100644 index 000000000..ef5be4640 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/callback/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Spinner } from "@/components/ui/spinner"; + +const OAUTH_RESULT_KEY = "connector_oauth_result"; + +export default function ConnectorCallbackPage({ + params, +}: { + params: { search_space_id: string }; +}) { + const router = useRouter(); + const searchParams = useSearchParams(); + + useEffect(() => { + const result = { + success: searchParams.get("success"), + error: searchParams.get("error"), + connector: searchParams.get("connector"), + connectorId: searchParams.get("connectorId"), + }; + + sessionStorage.setItem(OAUTH_RESULT_KEY, JSON.stringify(result)); + router.replace(`/dashboard/${params.search_space_id}/new-chat`); + }, [searchParams, router, params.search_space_id]); + + return ( +
+ +
+ ); +} diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 4f4bf5cea..e065ce72d 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -2,8 +2,7 @@ import { useAtomValue, useSetAtom } from "jotai"; import { AlertTriangle, Cable, Settings } from "lucide-react"; -import { useSearchParams } from "next/navigation"; -import { type FC, forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; +import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom"; import { @@ -49,7 +48,6 @@ interface ConnectorIndicatorProps { export const ConnectorIndicator = forwardRef( ({ showTrigger = true }, ref) => { const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); - const searchParams = useSearchParams(); const setSearchSpaceSettingsDialog = useSetAtom(searchSpaceSettingsDialogAtom); const { data: currentUser } = useAtomValue(currentUserAtom); const { data: preferences = {}, isFetching: preferencesLoading } = @@ -85,9 +83,6 @@ export const ConnectorIndicator = forwardRef ) : indexingConfig ? ( - { + if (indexingConfig.connectorId) { + startIndexing(indexingConfig.connectorId); } - startDate={startDate} - endDate={endDate} - periodicEnabled={periodicEnabled} - frequencyMinutes={frequencyMinutes} - enableSummary={enableSummary} - isStartingIndexing={isStartingIndexing} - onStartDateChange={setStartDate} - onEndDateChange={setEndDate} - onPeriodicEnabledChange={setPeriodicEnabled} - onFrequencyChange={setFrequencyMinutes} - onEnableSummaryChange={setEnableSummary} - onConfigChange={setIndexingConnectorConfig} - onStartIndexing={() => { - if (indexingConfig.connectorId) { - startIndexing(indexingConfig.connectorId); - } - handleStartIndexing(() => refreshConnectors()); - }} - onSkip={handleSkipIndexing} - /> + handleStartIndexing(() => refreshConnectors()); + }} + onSkip={handleSkipIndexing} + /> ) : ( void; onEndDateChange: (date: Date | undefined) => void; onPeriodicEnabledChange: (enabled: boolean) => void; @@ -43,6 +43,7 @@ export const IndexingConfigurationView: FC = ({ frequencyMinutes, enableSummary, isStartingIndexing, + isFromOAuth = false, onStartDateChange, onEndDateChange, onPeriodicEnabledChange, @@ -52,9 +53,6 @@ export const IndexingConfigurationView: FC = ({ onStartIndexing, onSkip, }) => { - const searchParams = useSearchParams(); - const isFromOAuth = searchParams.get("view") === "configure"; - // Get connector-specific config component const ConnectorConfigComponent = useMemo( () => (connector ? getConnectorConfigComponent(connector.connector_type) : null), diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts index 5a0a8e8c8..b03ad893b 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts @@ -1,24 +1,6 @@ import { z } from "zod"; import { searchSourceConnectorTypeEnum } from "@/contracts/types/connector.types"; -/** - * Schema for URL query parameters used by the connector popup - */ -export const connectorPopupQueryParamsSchema = z.object({ - modal: z.enum(["connectors"]).optional(), - tab: z.enum(["all", "active"]).optional(), - view: z - .enum(["configure", "edit", "connect", "youtube", "accounts", "mcp-list", "composio"]) - .optional(), - connector: z.string().optional(), - connectorId: z.string().optional(), - connectorType: z.string().optional(), - success: z.enum(["true", "false"]).optional(), - error: z.string().optional(), -}); - -export type ConnectorPopupQueryParams = z.infer; - /** * Schema for OAuth API response (auth_url) */ @@ -72,31 +54,10 @@ export const dateRangeSchema = z export type DateRange = z.infer; /** - * Schema for connector ID validation (used in URL params) + * Schema for connector ID validation */ export const connectorIdSchema = z.string().min(1, "Connector ID is required"); -/** - * Helper function to safely parse query params - */ -export function parseConnectorPopupQueryParams( - params: URLSearchParams | Record -): ConnectorPopupQueryParams { - const obj: Record = {}; - - if (params instanceof URLSearchParams) { - params.forEach((value, key) => { - obj[key] = value || undefined; - }); - } else { - Object.entries(params).forEach(([key, value]) => { - obj[key] = value || undefined; - }); - } - - return connectorPopupQueryParamsSchema.parse(obj); -} - /** * Helper function to safely parse OAuth response */ diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index 14183ec75..74cea667c 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -1,6 +1,5 @@ import { format } from "date-fns"; import { useAtom, useAtomValue } from "jotai"; -import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms"; @@ -37,14 +36,13 @@ import { import { dateRangeSchema, frequencyMinutesSchema, - parseConnectorPopupQueryParams, parseOAuthAuthResponse, validateIndexingConfigState, } from "../constants/connector-popup.schemas"; +const OAUTH_RESULT_KEY = "connector_oauth_result"; + export const useConnectorDialog = () => { - const router = useRouter(); - const searchParams = useSearchParams(); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const { data: allConnectors, refetch: refetchAllConnectors } = useAtomValue(connectorsAtom); const { mutateAsync: indexConnector } = useAtomValue(indexConnectorMutationAtom); @@ -102,6 +100,9 @@ export const useConnectorDialog = () => { // Track if we came from MCP list view when entering edit mode const [cameFromMCPList, setCameFromMCPList] = useState(false); + // Track if we came from MCP list view when entering connect mode + const [connectCameFromMCPList, setConnectCameFromMCPList] = useState(false); + // Helper function to get frequency label const getFrequencyLabel = useCallback((minutes: string): string => { switch (minutes) { @@ -181,352 +182,146 @@ export const useConnectorDialog = () => { [searchSpaceId, indexConnector, updateConnector, refetchAllConnectors] ); - // When the dialog is opened externally (via setConnectorDialogOpen atom from - // thread.tsx / DocumentsSidebar.tsx), the URL is not updated. Sync it here - // so that other handlers that read window.location.href see modal=connectors. - const activeTabRef = useRef(activeTab); - activeTabRef.current = activeTab; - useEffect(() => { - if (isOpen) { - const url = new URL(window.location.href); - const modalParam = url.searchParams.get("modal"); - const tabParam = url.searchParams.get("tab"); - if (modalParam !== "connectors" || (tabParam !== "all" && tabParam !== "active")) { - url.searchParams.set("modal", "connectors"); - url.searchParams.set("tab", activeTabRef.current); - window.history.replaceState({ modal: true }, "", url.toString()); - } - } - }, [isOpen]); + // YouTube view state + const [isYouTubeView, setIsYouTubeView] = useState(false); - // Synchronize state with URL query params + // Track whether the current indexing config came from an OAuth redirect + const [isFromOAuth, setIsFromOAuth] = useState(false); + + // Consume OAuth result from sessionStorage (set by /connectors/callback page) useEffect(() => { + const raw = sessionStorage.getItem(OAUTH_RESULT_KEY); + if (!raw || !searchSpaceId) return; + sessionStorage.removeItem(OAUTH_RESULT_KEY); + + let result: { + success: string | null; + error: string | null; + connector: string | null; + connectorId: string | null; + }; try { - const params = parseConnectorPopupQueryParams(searchParams); + result = JSON.parse(raw); + } catch { + return; + } - if (params.modal === "connectors") { - setIsOpen(true); + if (result.error) { + const oauthConnector = result.connector + ? OAUTH_CONNECTORS.find((c) => c.id === result.connector) + : null; + const name = oauthConnector?.title || "connector"; - if (params.tab === "active" || params.tab === "all") { - setActiveTab(params.tab); - } - - // Clear indexing config if view is not "configure" anymore - if (params.view !== "configure" && indexingConfig) { - setIndexingConfig(null); - } - - // Clear editing connector if view is not "edit" anymore - if (params.view !== "edit" && editingConnector) { - setEditingConnector(null); - setConnectorName(null); - } - - // Clear connecting connector type if view is not "connect" anymore - if (params.view !== "connect" && connectingConnectorType) { - setConnectingConnectorType(null); - } - - // Clear viewing accounts type if view is not "accounts" anymore - if (params.view !== "accounts" && viewingAccountsType) { - setViewingAccountsType(null); - } - - // Clear MCP list view if view is not "mcp-list" anymore - if (params.view !== "mcp-list" && viewingMCPList) { - setViewingMCPList(false); - } - - // Handle MCP list view - if (params.view === "mcp-list" && !viewingMCPList) { - setViewingMCPList(true); - } - - // Handle connect view - if (params.view === "connect" && params.connectorType && !connectingConnectorType) { - setConnectingConnectorType(params.connectorType); - } - - // Handle accounts view - if (params.view === "accounts" && params.connectorType) { - // Update state if not set, or if connectorType has changed - const needsUpdate = - !viewingAccountsType || viewingAccountsType.connectorType !== params.connectorType; - - if (needsUpdate) { - // Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS - const oauthConnector = - OAUTH_CONNECTORS.find((c) => c.connectorType === params.connectorType) || - COMPOSIO_CONNECTORS.find((c) => c.connectorType === params.connectorType); - if (oauthConnector) { - setViewingAccountsType({ - connectorType: oauthConnector.connectorType, - connectorTitle: oauthConnector.title, - }); - } - } - } - - // Handle YouTube view - if (params.view === "youtube") { - // YouTube view is active - no additional state needed - } - - // Handle configure view (for page refresh support) - if (params.view === "configure" && params.connector && !indexingConfig && allConnectors) { - // Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS - const oauthConnector = - OAUTH_CONNECTORS.find((c) => c.id === params.connector) || - COMPOSIO_CONNECTORS.find((c) => c.id === params.connector); - if (oauthConnector) { - let existingConnector: SearchSourceConnector | undefined; - if (params.connectorId) { - const connectorId = parseInt(params.connectorId, 10); - existingConnector = allConnectors.find( - (c: SearchSourceConnector) => c.id === connectorId - ); - } else { - existingConnector = allConnectors.find( - (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType - ); - } - if (existingConnector) { - const connectorValidation = searchSourceConnector.safeParse(existingConnector); - if (connectorValidation.success) { - const config = validateIndexingConfigState({ - connectorType: oauthConnector.connectorType, - connectorId: existingConnector.id, - connectorTitle: oauthConnector.title, - }); - setIndexingConfig(config); - setIndexingConnector(existingConnector); - setIndexingConnectorConfig(existingConnector.config); - } - } - } - } - - // Handle edit view - if (params.view === "edit" && params.connectorId && allConnectors && !editingConnector) { - const connectorId = parseInt(params.connectorId, 10); - const connector = allConnectors.find((c: SearchSourceConnector) => c.id === connectorId); - if (connector) { - const connectorValidation = searchSourceConnector.safeParse(connector); - if (connectorValidation.success) { - setEditingConnector(connector); - setConnectorConfig(connector.config); - setConnectorName(connector.name); - // Load existing periodic sync settings (disabled for non-indexable connectors) - setPeriodicEnabled( - !connector.is_indexable ? false : connector.periodic_indexing_enabled - ); - setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); - setEnableSummary(connector.enable_summary ?? false); - // Reset dates - user can set new ones for re-indexing - setStartDate(undefined); - setEndDate(undefined); - } - } - } + if (result.error === "duplicate_account") { + toast.error(`This ${name} account is already connected`, { + description: "Please use a different account or manage the existing connection.", + }); } else { - // Do NOT call setIsOpen(false) here. Closing the dialog is handled - // explicitly by handleOpenChange and the individual action handlers. - // Relying on URL params to close the dialog caused a race condition - // where Next.js router updates from tab switches briefly produced - // stale searchParams without the "modal" key, closing the popup. - - // Still clean up sub-view state when the modal param is gone - // (e.g. after browser back navigation or explicit handler URL cleanup). - if (indexingConfig) { - setIndexingConfig(null); - setIndexingConnector(null); - setIndexingConnectorConfig(null); - setStartDate(undefined); - setEndDate(undefined); - setPeriodicEnabled(false); - setFrequencyMinutes("1440"); - setEnableSummary(false); - setIsScrolled(false); - setSearchQuery(""); - } - if (editingConnector) { - setEditingConnector(null); - setConnectorName(null); - setConnectorConfig(null); - setStartDate(undefined); - setEndDate(undefined); - setPeriodicEnabled(false); - setFrequencyMinutes("1440"); - setEnableSummary(false); - setIsScrolled(false); - setSearchQuery(""); - } - if (connectingConnectorType) { - setConnectingConnectorType(null); - } - if (viewingAccountsType) { - setViewingAccountsType(null); - } - } - } catch (error) { - // Invalid query params - log but don't crash - console.warn("Invalid connector popup query params:", error); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - searchParams, - allConnectors, - editingConnector, - indexingConfig, - connectingConnectorType, - viewingAccountsType, - viewingMCPList, - setIsOpen, - ]); - - // Detect OAuth success / Failure and transition to config view - useEffect(() => { - try { - const params = parseConnectorPopupQueryParams(searchParams); - - // Handle OAuth errors (e.g., duplicate account) - if (params.error && params.modal === "connectors") { - const oauthConnector = params.connector - ? OAUTH_CONNECTORS.find((c) => c.id === params.connector) - : null; - const connectorName = oauthConnector?.title || "connector"; - - if (params.error === "duplicate_account") { - toast.error(`This ${connectorName} account is already connected`, { - description: "Please use a different account or manage the existing connection.", - }); - } else { - toast.error(`Failed to connect ${connectorName}`, { - description: params.error.replace(/_/g, " "), - }); - } - - // Clean up error params from URL - const url = new URL(window.location.href); - url.searchParams.delete("error"); - url.searchParams.delete("connector"); - window.history.replaceState({}, "", url.toString()); - - // Open the popup to show the connectors - setIsOpen(true); - return; - } - - if (params.success === "true" && searchSpaceId && params.modal === "connectors") { - // For auto-index connectors: close modal and show loading toast before refetch - const earlyConnector = params.connector - ? OAUTH_CONNECTORS.find((c) => c.id === params.connector) || - COMPOSIO_CONNECTORS.find((c) => c.id === params.connector) - : null; - - if (earlyConnector && AUTO_INDEX_CONNECTOR_TYPES.has(earlyConnector.connectorType)) { - toast.loading(`Setting up ${earlyConnector.title}...`, { id: "auto-index" }); - setIsOpen(false); - const url = new URL(window.location.href); - url.searchParams.delete("success"); - url.searchParams.delete("connector"); - url.searchParams.delete("connectorId"); - url.searchParams.delete("view"); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - router.replace(url.pathname + url.search, { scroll: false }); - } - - refetchAllConnectors().then(async (result) => { - if (!result.data) { - toast.dismiss("auto-index"); - return; - } - - let newConnector: SearchSourceConnector | undefined; - let oauthConnector: - | (typeof OAUTH_CONNECTORS)[number] - | (typeof COMPOSIO_CONNECTORS)[number] - | undefined; - - // First, try to find connector by connectorId if provided - if (params.connectorId) { - const connectorId = parseInt(params.connectorId, 10); - newConnector = result.data.find((c: SearchSourceConnector) => c.id === connectorId); - - // If we found the connector, find the matching OAuth/Composio connector by type - if (newConnector) { - const connectorType = newConnector.connector_type; - oauthConnector = - OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) || - COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType); - } - } - - // If we don't have a connector yet, try to find by connector param - if (!newConnector && params.connector) { - oauthConnector = - OAUTH_CONNECTORS.find((c) => c.id === params.connector) || - COMPOSIO_CONNECTORS.find((c) => c.id === params.connector); - - if (oauthConnector) { - const oauthConnectorType = oauthConnector.connectorType; - newConnector = result.data.find( - (c: SearchSourceConnector) => c.connector_type === oauthConnectorType - ); - } - } - - if (newConnector && oauthConnector) { - const connectorValidation = searchSourceConnector.safeParse(newConnector); - if (connectorValidation.success) { - trackConnectorConnected( - Number(searchSpaceId), - oauthConnector.connectorType, - newConnector.id - ); - - if ( - newConnector.is_indexable && - AUTO_INDEX_CONNECTOR_TYPES.has(oauthConnector.connectorType) - ) { - await handleAutoIndex( - newConnector, - oauthConnector.title, - oauthConnector.connectorType - ); - } else { - toast.dismiss("auto-index"); - const config = validateIndexingConfigState({ - connectorType: oauthConnector.connectorType, - connectorId: newConnector.id, - connectorTitle: oauthConnector.title, - }); - setIndexingConfig(config); - setIndexingConnector(newConnector); - setIndexingConnectorConfig(newConnector.config); - setIsOpen(true); - const url = new URL(window.location.href); - url.searchParams.delete("success"); - url.searchParams.set("connectorId", newConnector.id.toString()); - url.searchParams.set("view", "configure"); - window.history.replaceState({}, "", url.toString()); - } - } else { - console.warn("Invalid connector data after OAuth:", connectorValidation.error); - toast.dismiss("auto-index"); - toast.error("Failed to validate connector data"); - } - } else { - toast.dismiss("auto-index"); - } + toast.error(`Failed to connect ${name}`, { + description: result.error.replace(/_/g, " "), }); } - } catch (error) { - // Invalid query params - log but don't crash - console.warn("Invalid connector popup query params in OAuth success handler:", error); + + setIsOpen(true); + return; } - }, [searchParams, searchSpaceId, refetchAllConnectors, setIsOpen, handleAutoIndex, router]); + + if (result.success === "true") { + const earlyConnector = result.connector + ? OAUTH_CONNECTORS.find((c) => c.id === result.connector) || + COMPOSIO_CONNECTORS.find((c) => c.id === result.connector) + : null; + + if (earlyConnector && AUTO_INDEX_CONNECTOR_TYPES.has(earlyConnector.connectorType)) { + toast.loading(`Setting up ${earlyConnector.title}...`, { id: "auto-index" }); + setIsOpen(false); + } + + refetchAllConnectors().then(async (fetchResult) => { + if (!fetchResult.data) { + toast.dismiss("auto-index"); + return; + } + + let newConnector: SearchSourceConnector | undefined; + let oauthConnector: + | (typeof OAUTH_CONNECTORS)[number] + | (typeof COMPOSIO_CONNECTORS)[number] + | undefined; + + if (result.connectorId) { + const connectorId = parseInt(result.connectorId, 10); + newConnector = fetchResult.data.find( + (c: SearchSourceConnector) => c.id === connectorId + ); + if (newConnector) { + const connectorType = newConnector.connector_type; + oauthConnector = + OAUTH_CONNECTORS.find( + (c) => c.connectorType === connectorType + ) || + COMPOSIO_CONNECTORS.find( + (c) => c.connectorType === connectorType + ); + } + } + + if (!newConnector && result.connector) { + oauthConnector = + OAUTH_CONNECTORS.find((c) => c.id === result.connector) || + COMPOSIO_CONNECTORS.find((c) => c.id === result.connector); + if (oauthConnector) { + const oauthType = oauthConnector.connectorType; + newConnector = fetchResult.data.find( + (c: SearchSourceConnector) => + c.connector_type === oauthType + ); + } + } + + if (newConnector && oauthConnector) { + const connectorValidation = searchSourceConnector.safeParse(newConnector); + if (connectorValidation.success) { + trackConnectorConnected( + Number(searchSpaceId), + oauthConnector.connectorType, + newConnector.id + ); + + if ( + newConnector.is_indexable && + AUTO_INDEX_CONNECTOR_TYPES.has(oauthConnector.connectorType) + ) { + await handleAutoIndex( + newConnector, + oauthConnector.title, + oauthConnector.connectorType + ); + } else { + toast.dismiss("auto-index"); + const config = validateIndexingConfigState({ + connectorType: oauthConnector.connectorType, + connectorId: newConnector.id, + connectorTitle: oauthConnector.title, + }); + setIndexingConfig(config); + setIndexingConnector(newConnector); + setIndexingConnectorConfig(newConnector.config); + setIsFromOAuth(true); + setIsOpen(true); + } + } else { + console.warn("Invalid connector data after OAuth:", connectorValidation.error); + toast.dismiss("auto-index"); + toast.error("Failed to validate connector data"); + } + } else { + toast.dismiss("auto-index"); + } + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchSpaceId, handleAutoIndex, refetchAllConnectors, setIsOpen]); // Handle OAuth connection const handleConnectOAuth = useCallback( @@ -572,12 +367,7 @@ export const useConnectorDialog = () => { // Handle creating YouTube crawler (not a connector, shows view in popup) const handleCreateYouTubeCrawler = useCallback(() => { if (!searchSpaceId) return; - - // Update URL to show YouTube view - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "youtube"); - window.history.pushState({ modal: true }, "", url.toString()); + setIsYouTubeView(true); }, [searchSpaceId]); // Handle creating webcrawler connector @@ -629,10 +419,6 @@ export const useConnectorDialog = () => { setIndexingConnector(connector); setIndexingConnectorConfig(connector.config || {}); setIsOpen(true); - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "configure"); - window.history.pushState({ modal: true }, "", url.toString()); } } } @@ -648,16 +434,7 @@ export const useConnectorDialog = () => { const handleConnectNonOAuth = useCallback( (connectorType: string) => { if (!searchSpaceId) return; - - // Set connecting state setConnectingConnectorType(connectorType); - - // Update URL to show connect view - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "connect"); - url.searchParams.set("connectorType", connectorType); - window.history.pushState({ modal: true }, "", url.toString()); }, [searchSpaceId] ); @@ -810,27 +587,17 @@ export const useConnectorDialog = () => { : `${connectorTitle} connected and syncing started!`; toast.success(successMessage); - // Close dialog and clean up URL - setIsOpen(false); - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorType"); - router.replace(url.pathname + url.search, { scroll: false }); + setIsOpen(false); - // Clear indexing config state since we're not showing the view - setIndexingConfig(null); - setIndexingConnector(null); - setIndexingConnectorConfig(null); + setIndexingConfig(null); + setIndexingConnector(null); + setIndexingConnectorConfig(null); - // Invalidate queries to refresh data - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); - // Refresh connectors list - await refetchAllConnectors(); + await refetchAllConnectors(); } else { // Non-indexable connector // For Circleback, transition to edit view to show webhook URL @@ -852,20 +619,11 @@ export const useConnectorDialog = () => { setStartDate(undefined); setEndDate(undefined); - toast.success(`${connectorTitle} connected successfully!`, { - description: "Configure the webhook URL in your Circleback settings.", - }); + toast.success(`${connectorTitle} connected successfully!`, { + description: "Configure the webhook URL in your Circleback settings.", + }); - // Transition to edit view - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "edit"); - url.searchParams.set("connectorId", connector.id.toString()); - url.searchParams.delete("connectorType"); - router.replace(url.pathname + url.search, { scroll: false }); - - // Refresh connectors list - await refetchAllConnectors(); + await refetchAllConnectors(); } else { // Other non-indexable connectors - just show success message and close const successMessage = @@ -874,22 +632,13 @@ export const useConnectorDialog = () => { : `${connectorTitle} connected successfully!`; toast.success(successMessage); - // Refresh connectors list before closing modal - await refetchAllConnectors(); + await refetchAllConnectors(); - // Close dialog and clean up URL - setIsOpen(false); - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorType"); - router.replace(url.pathname + url.search, { scroll: false }); + setIsOpen(false); - // Clear indexing config state - setIndexingConfig(null); - setIndexingConnector(null); - setIndexingConnectorConfig(null); + setIndexingConfig(null); + setIndexingConnector(null); + setIndexingConnectorConfig(null); } } } @@ -911,96 +660,64 @@ export const useConnectorDialog = () => { refetchAllConnectors, updateConnector, indexConnector, - router, setIsOpen, ] ); // Handle going back from connect view const handleBackFromConnect = useCallback(() => { - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - - // If we're connecting an MCP and came from list view, go back to list - if (connectingConnectorType === "MCP_CONNECTOR" && viewingMCPList) { - url.searchParams.set("view", "mcp-list"); - } else { - url.searchParams.set("tab", "all"); - url.searchParams.delete("view"); + if (connectCameFromMCPList) { + setViewingMCPList(true); + setConnectCameFromMCPList(false); } - - url.searchParams.delete("connectorType"); - router.replace(url.pathname + url.search, { scroll: false }); - }, [router, connectingConnectorType, viewingMCPList]); + setConnectingConnectorType(null); + }, [connectCameFromMCPList]); // Handle going back from YouTube view const handleBackFromYouTube = useCallback(() => { - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("tab", "all"); - url.searchParams.delete("view"); - router.replace(url.pathname + url.search, { scroll: false }); - }, [router]); + setIsYouTubeView(false); + }, []); // Handle viewing accounts list for OAuth connector type const handleViewAccountsList = useCallback( (connectorType: string, _connectorTitle?: string) => { if (!searchSpaceId) return; - // Update URL to show accounts view, preserving current tab - // The useEffect will handle setting viewingAccountsType based on URL params - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "accounts"); - url.searchParams.set("connectorType", connectorType); - // Keep the current tab in URL so we can go back to it - router.replace(url.pathname + url.search, { scroll: false }); + const oauthConnector = + OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) || + COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType); + if (oauthConnector) { + setViewingAccountsType({ + connectorType: oauthConnector.connectorType, + connectorTitle: oauthConnector.title, + }); + } }, - [searchSpaceId, router] + [searchSpaceId] ); // Handle going back from accounts list view const handleBackFromAccountsList = useCallback(() => { setViewingAccountsType(null); - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - // Keep the current tab (don't change it) - just remove view-specific params - url.searchParams.delete("view"); - url.searchParams.delete("connectorType"); - router.replace(url.pathname + url.search, { scroll: false }); - }, [router]); + }, []); // Handle viewing MCP list const handleViewMCPList = useCallback(() => { if (!searchSpaceId) return; - setViewingMCPList(true); - - // Update URL to show MCP list view - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "mcp-list"); - router.replace(url.pathname + url.search, { scroll: false }); - }, [searchSpaceId, router]); + }, [searchSpaceId]); // Handle going back from MCP list view const handleBackFromMCPList = useCallback(() => { setViewingMCPList(false); - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.delete("view"); - router.replace(url.pathname + url.search, { scroll: false }); - }, [router]); + }, []); // Handle adding new MCP from list view const handleAddNewMCPFromList = useCallback(() => { + setConnectCameFromMCPList(true); + setViewingMCPList(false); setConnectingConnectorType("MCP_CONNECTOR"); - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "connect"); - url.searchParams.set("connectorType", "MCP_CONNECTOR"); - router.replace(url.pathname + url.search, { scroll: false }); - }, [router]); + }, []); // Handle starting indexing const handleStartIndexing = useCallback( @@ -1141,19 +858,12 @@ export const useConnectorDialog = () => { ); } - toast.success(`${indexingConfig.connectorTitle} indexing started`); + toast.success(`${indexingConfig.connectorTitle} indexing started`); - // Close dialog and clean up URL - setIsOpen(false); - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("success"); - url.searchParams.delete("connector"); - url.searchParams.delete("view"); - router.replace(url.pathname + url.search, { scroll: false }); + setIsOpen(false); + setIsFromOAuth(false); - refreshConnectors(); + refreshConnectors(); queryClient.invalidateQueries({ queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), }); @@ -1174,7 +884,6 @@ export const useConnectorDialog = () => { periodicEnabled, frequencyMinutes, enableSummary, - router, indexingConnectorConfig, setIsOpen, ] @@ -1182,16 +891,9 @@ export const useConnectorDialog = () => { // Handle skipping indexing const handleSkipIndexing = useCallback(() => { - // Close dialog and clean up URL setIsOpen(false); - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("success"); - url.searchParams.delete("connector"); - url.searchParams.delete("view"); - router.replace(url.pathname + url.search, { scroll: false }); - }, [router, setIsOpen]); + setIsFromOAuth(false); + }, [setIsOpen]); // Handle starting edit mode const handleStartEdit = useCallback( @@ -1213,20 +915,21 @@ export const useConnectorDialog = () => { return; } - // Track if we came from accounts list view - // If viewingAccountsType matches this connector type, preserve it - if (viewingAccountsType && viewingAccountsType.connectorType === connector.connector_type) { - setCameFromAccountsList(viewingAccountsType); - } else { - setCameFromAccountsList(null); - } + // Track if we came from accounts list view so handleBackFromEdit can restore it + if (viewingAccountsType && viewingAccountsType.connectorType === connector.connector_type) { + setCameFromAccountsList(viewingAccountsType); + } else { + setCameFromAccountsList(null); + } + setViewingAccountsType(null); - // Track if we came from MCP list view - if (viewingMCPList && connector.connector_type === "MCP_CONNECTOR") { - setCameFromMCPList(true); - } else { - setCameFromMCPList(false); - } + // Track if we came from MCP list view so handleBackFromEdit can restore it + if (viewingMCPList && connector.connector_type === "MCP_CONNECTOR") { + setCameFromMCPList(true); + } else { + setCameFromMCPList(false); + } + setViewingMCPList(false); // Track index with date range opened event if (connector.is_indexable) { @@ -1237,24 +940,15 @@ export const useConnectorDialog = () => { ); } - setEditingConnector(connector); - setConnectorName(connector.name); - // Load existing periodic sync settings (disabled for non-indexable connectors) - setPeriodicEnabled(!connector.is_indexable ? false : connector.periodic_indexing_enabled); - setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); - setEnableSummary(connector.enable_summary ?? false); - // Reset dates - user can set new ones for re-indexing - setStartDate(undefined); - setEndDate(undefined); - - // Update URL - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "edit"); - url.searchParams.set("connectorId", connector.id.toString()); - window.history.pushState({ modal: true }, "", url.toString()); - }, - [searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab] + setEditingConnector(connector); + setConnectorName(connector.name); + setPeriodicEnabled(!connector.is_indexable ? false : connector.periodic_indexing_enabled); + setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); + setEnableSummary(connector.enable_summary ?? false); + setStartDate(undefined); + setEndDate(undefined); + }, + [searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab] ); // Handle saving connector changes @@ -1433,43 +1127,35 @@ export const useConnectorDialog = () => { : indexingDescription, }); - // Close dialog and clean up URL - setIsOpen(false); - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorId"); - router.replace(url.pathname + url.search, { scroll: false }); + setIsOpen(false); - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error saving connector:", error); - toast.error("Failed to save connector changes"); - } finally { - setIsSaving(false); - } - }, - [ - editingConnector, - searchSpaceId, - isSaving, - startDate, - endDate, - indexConnector, - updateConnector, - periodicEnabled, - frequencyMinutes, - enableSummary, - getFrequencyLabel, - router, - connectorConfig, - connectorName, - setIsOpen, - ] + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + } catch (error) { + console.error("Error saving connector:", error); + toast.error("Failed to save connector changes"); + } finally { + setIsSaving(false); + } + }, + [ + editingConnector, + searchSpaceId, + isSaving, + startDate, + endDate, + indexConnector, + updateConnector, + periodicEnabled, + frequencyMinutes, + enableSummary, + getFrequencyLabel, + connectorConfig, + connectorName, + setIsOpen, + ] ); // Handle disconnecting connector @@ -1496,25 +1182,16 @@ export const useConnectorDialog = () => { : `${editingConnector.name} disconnected successfully` ); - // Update URL - for MCP from list view, go back to list; otherwise close modal - const url = new URL(window.location.href); - if (editingConnector.connector_type === "MCP_CONNECTOR" && cameFromMCPList) { - // Go back to MCP list view only if we came from there - setViewingMCPList(true); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "mcp-list"); - url.searchParams.delete("connectorId"); - } else { - // Close dialog for all other cases - setIsOpen(false); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorId"); - } - router.replace(url.pathname + url.search, { scroll: false }); + if (editingConnector.connector_type === "MCP_CONNECTOR" && cameFromMCPList) { + setViewingMCPList(true); + setEditingConnector(null); + setConnectorName(null); + setConnectorConfig(null); + } else { + setIsOpen(false); + } - refreshConnectors(); + refreshConnectors(); queryClient.invalidateQueries({ queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), }); @@ -1525,7 +1202,7 @@ export const useConnectorDialog = () => { setIsDisconnecting(false); } }, - [editingConnector, searchSpaceId, deleteConnector, router, cameFromMCPList, setIsOpen] + [editingConnector, searchSpaceId, deleteConnector, cameFromMCPList, setIsOpen] ); // Handle quick index (index with selected date range, or backend defaults if none selected) @@ -1584,66 +1261,35 @@ export const useConnectorDialog = () => { // Handle going back from edit view const handleBackFromEdit = useCallback(() => { - // If editing an MCP connector and came from MCP list, go back to MCP list view if (editingConnector?.connector_type === "MCP_CONNECTOR" && cameFromMCPList) { setViewingMCPList(true); setCameFromMCPList(false); - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "mcp-list"); - url.searchParams.delete("connectorId"); - router.replace(url.pathname + url.search, { scroll: false }); setEditingConnector(null); setConnectorName(null); setConnectorConfig(null); return; } - // If we came from accounts list view, go back there if (cameFromAccountsList && editingConnector) { - // Restore accounts list view setViewingAccountsType(cameFromAccountsList); setCameFromAccountsList(null); - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "accounts"); - url.searchParams.set("connectorType", cameFromAccountsList.connectorType); - url.searchParams.delete("connectorId"); - router.replace(url.pathname + url.search, { scroll: false }); - } else { - // Otherwise, go back to main connector popup (preserve the tab the user was on) - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("tab", activeTab); // Use current tab instead of always "all" - url.searchParams.delete("view"); - url.searchParams.delete("connectorId"); - router.replace(url.pathname + url.search, { scroll: false }); } + setEditingConnector(null); setConnectorName(null); setConnectorConfig(null); - }, [router, cameFromAccountsList, editingConnector, cameFromMCPList, activeTab]); + }, [cameFromAccountsList, editingConnector, cameFromMCPList]); // Handle dialog open/close const handleOpenChange = useCallback( (open: boolean) => { setIsOpen(open); - if (open) { - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("tab", activeTab); - window.history.pushState({ modal: true }, "", url.toString()); - } else { - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("success"); - url.searchParams.delete("connector"); - url.searchParams.delete("view"); - window.history.pushState({ modal: false }, "", url.toString()); + if (!open) { setIsScrolled(false); setSearchQuery(""); + setIsYouTubeView(false); + setIsFromOAuth(false); if (!isStartingIndexing && !isSaving && !isDisconnecting && !isCreatingConnector) { setIndexingConfig(null); setIndexingConnector(null); @@ -1651,10 +1297,13 @@ export const useConnectorDialog = () => { setEditingConnector(null); setConnectorName(null); setConnectorConfig(null); - setConnectingConnectorType(null); - setViewingAccountsType(null); - setCameFromAccountsList(null); - setStartDate(undefined); + setConnectingConnectorType(null); + setViewingAccountsType(null); + setViewingMCPList(false); + setCameFromAccountsList(null); + setCameFromMCPList(false); + setConnectCameFromMCPList(false); + setStartDate(undefined); setEndDate(undefined); setPeriodicEnabled(false); setFrequencyMinutes("1440"); @@ -1662,14 +1311,9 @@ export const useConnectorDialog = () => { } } }, - [activeTab, isStartingIndexing, isDisconnecting, isSaving, isCreatingConnector, setIsOpen] + [isStartingIndexing, isDisconnecting, isSaving, isCreatingConnector, setIsOpen] ); - // Handle tab change — only update React state. - // Avoid window.history.replaceState here: Next.js intercepts it, triggers a - // searchParams update/transition, and the resulting concurrent re-render can - // cause Radix Dialog's DismissableLayer to detect a transient focus-outside - // event, which fires onOpenChange(false) and closes the dialog. const handleTabChange = useCallback((value: string) => { setActiveTab(value); }, []); @@ -1704,6 +1348,8 @@ export const useConnectorDialog = () => { allConnectors, viewingAccountsType, viewingMCPList, + isYouTubeView, + isFromOAuth, // Setters setSearchQuery, diff --git a/surfsense_web/components/assistant-ui/connector-popup/index.ts b/surfsense_web/components/assistant-ui/connector-popup/index.ts index adc0e0770..f3209a777 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/index.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/index.ts @@ -12,19 +12,16 @@ export type { IndexingConfigState } from "./constants/connector-constants"; // Constants and types export { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS } from "./constants/connector-constants"; export type { - ConnectorPopupQueryParams, DateRange, FrequencyMinutes, OAuthAuthResponse, } from "./constants/connector-popup.schemas"; // Schemas and validation export { - connectorPopupQueryParamsSchema, dateRangeSchema, frequencyMinutesSchema, indexingConfigStateSchema, oauthAuthResponseSchema, - parseConnectorPopupQueryParams, parseOAuthAuthResponse, validateIndexingConfigState, } from "./constants/connector-popup.schemas";