feat: Introduce new connector schemas and validation, enhance connector dialog with improved query parameter handling, and implement scroll detection in indexing configuration view.

This commit is contained in:
Anish Sarkar 2025-12-30 21:25:48 +05:30
parent 32b4a09c0e
commit 29a3dcf091
8 changed files with 376 additions and 143 deletions

View file

@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import type { LogSummary, LogActiveTask } from "@/contracts/types/log.types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
TabsContent, TabsContent,
@ -20,7 +21,7 @@ interface ActiveConnectorsTabProps {
activeDocumentTypes: Array<[string, number]>; activeDocumentTypes: Array<[string, number]>;
connectors: SearchSourceConnector[]; connectors: SearchSourceConnector[];
indexingConnectorIds: Set<number>; indexingConnectorIds: Set<number>;
logsSummary: any; logsSummary: LogSummary | undefined;
searchSpaceId: string; searchSpaceId: string;
onTabChange: (value: string) => void; onTabChange: (value: string) => void;
} }
@ -67,7 +68,7 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
{connectors.map((connector) => { {connectors.map((connector) => {
const isIndexing = indexingConnectorIds.has(connector.id); const isIndexing = indexingConnectorIds.has(connector.id);
const activeTask = logsSummary?.active_tasks?.find( const activeTask = logsSummary?.active_tasks?.find(
(task: any) => (task: LogActiveTask) =>
task.source?.includes(`connector_${connector.id}`) || task.source?.includes(`connector_${connector.id}`) ||
task.source?.includes(`connector-${connector.id}`) task.source?.includes(`connector-${connector.id}`)
); );

View file

@ -1,10 +1,7 @@
"use client"; "use client";
import { ChevronRight } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { type FC } from "react"; import { type FC } from "react";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import { OAUTH_CONNECTORS, OTHER_CONNECTORS } from "./connector-constants"; import { OAUTH_CONNECTORS, OTHER_CONNECTORS } from "./connector-constants";
import { ConnectorCard } from "./connector-card"; import { ConnectorCard } from "./connector-card";
@ -88,32 +85,24 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
const isConnected = connectedTypes.has(connector.connectorType); const isConnected = connectedTypes.has(connector.connectorType);
return ( return (
<Link <ConnectorCard
key={connector.id} key={connector.id}
href={`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`} id={connector.id}
className="group flex items-center gap-4 p-4 rounded-xl transition-all duration-150 border border-border bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10" title={connector.title}
> description={connector.description}
<div className="flex h-12 w-12 items-center justify-center rounded-lg transition-colors bg-slate-400/5 dark:bg-white/5 border border-slate-400/5 dark:border-white/5"> connectorType={connector.connectorType}
{getConnectorIcon(connector.connectorType, "size-6")} isConnected={isConnected}
</div> onConnect={() =>
<div className="flex-1 min-w-0"> router.push(
<div className="flex items-center gap-2"> `/dashboard/${searchSpaceId}/connectors/add/${connector.id}`
<span className="text-[14px] font-semibold leading-tight"> )
{connector.title} }
</span> onManage={() =>
{isConnected && ( router.push(
<span `/dashboard/${searchSpaceId}/connectors/add/${connector.id}`
className="size-1.5 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]" )
title="Connected" }
/> />
)}
</div>
<p className="text-[11px] text-muted-foreground truncate mt-1">
{connector.description}
</p>
</div>
<ChevronRight className="size-4 text-muted-foreground/50 group-hover:text-foreground transition-colors flex-shrink-0" />
</Link>
); );
})} })}
</div> </div>

View file

@ -34,12 +34,6 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[14px] font-semibold leading-tight">{title}</span> <span className="text-[14px] font-semibold leading-tight">{title}</span>
{isConnected && (
<span
className="size-1.5 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]"
title="Connected"
/>
)}
</div> </div>
<p className="text-[11px] text-muted-foreground truncate mt-1"> <p className="text-[11px] text-muted-foreground truncate mt-1">
{isConnected ? "Connected" : description} {isConnected ? "Connected" : description}

View file

@ -2,6 +2,13 @@ import { EnumConnectorName } from "@/contracts/enums/connector";
// OAuth Connectors (Quick Connect) // OAuth Connectors (Quick Connect)
export const OAUTH_CONNECTORS = [ export const OAUTH_CONNECTORS = [
{
id: "google-drive-connector",
title: "Google Drive",
description: "Search your Drive files",
connectorType: EnumConnectorName.GOOGLE_DRIVE_CONNECTOR,
authEndpoint: "/api/v1/auth/google/drive/connector/add/",
},
{ {
id: "google-gmail-connector", id: "google-gmail-connector",
title: "Gmail", title: "Gmail",
@ -125,10 +132,6 @@ export const OTHER_CONNECTORS = [
}, },
] as const; ] as const;
// Type for the indexing configuration state // Re-export IndexingConfigState from schemas for backward compatibility
export interface IndexingConfigState { export type { IndexingConfigState } from "./connector-popup.schemas";
connectorType: string;
connectorId: number;
connectorTitle: string;
}

View file

@ -0,0 +1,108 @@
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"]).optional(),
connector: z.string().optional(),
success: z.enum(["true", "false"]).optional(),
});
export type ConnectorPopupQueryParams = z.infer<typeof connectorPopupQueryParamsSchema>;
/**
* Schema for OAuth API response (auth_url)
*/
export const oauthAuthResponseSchema = z.object({
auth_url: z.string().url("Invalid auth URL format"),
});
export type OAuthAuthResponse = z.infer<typeof oauthAuthResponseSchema>;
/**
* Schema for IndexingConfigState
*/
export const indexingConfigStateSchema = z.object({
connectorType: searchSourceConnectorTypeEnum,
connectorId: z.number().int().positive("Connector ID must be a positive integer"),
connectorTitle: z.string().min(1, "Connector title is required"),
});
export type IndexingConfigState = z.infer<typeof indexingConfigStateSchema>;
/**
* Schema for frequency minutes (must be one of the allowed values)
*/
export const frequencyMinutesSchema = z.enum(["15", "60", "360", "720", "1440", "10080"], {
errorMap: () => ({ message: "Invalid frequency value" }),
});
export type FrequencyMinutes = z.infer<typeof frequencyMinutesSchema>;
/**
* Schema for date range validation
*/
export const dateRangeSchema = z
.object({
startDate: z.date().optional(),
endDate: z.date().optional(),
})
.refine(
(data) => {
if (data.startDate && data.endDate) {
return data.startDate <= data.endDate;
}
return true;
},
{
message: "Start date must be before or equal to end date",
path: ["endDate"],
}
);
export type DateRange = z.infer<typeof dateRangeSchema>;
/**
* Schema for connector ID validation (used in URL params)
*/
export const connectorIdSchema = z.string().min(1, "Connector ID is required");
/**
* Helper function to safely parse query params
*/
export function parseConnectorPopupQueryParams(
params: URLSearchParams | Record<string, string | null>
): ConnectorPopupQueryParams {
const obj: Record<string, string | undefined> = {};
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
*/
export function parseOAuthAuthResponse(data: unknown): OAuthAuthResponse {
return oauthAuthResponseSchema.parse(data);
}
/**
* Helper function to validate indexing config state
*/
export function validateIndexingConfigState(data: unknown): IndexingConfigState {
return indexingConfigStateSchema.parse(data);
}

View file

@ -14,6 +14,24 @@ export { ActiveConnectorsTab } from "./active-connectors-tab";
export { OAUTH_CONNECTORS, OTHER_CONNECTORS } from "./connector-constants"; export { OAUTH_CONNECTORS, OTHER_CONNECTORS } from "./connector-constants";
export type { IndexingConfigState } from "./connector-constants"; export type { IndexingConfigState } from "./connector-constants";
// Schemas and validation
export {
connectorPopupQueryParamsSchema,
oauthAuthResponseSchema,
indexingConfigStateSchema,
frequencyMinutesSchema,
dateRangeSchema,
parseConnectorPopupQueryParams,
parseOAuthAuthResponse,
validateIndexingConfigState,
} from "./connector-popup.schemas";
export type {
ConnectorPopupQueryParams,
OAuthAuthResponse,
FrequencyMinutes,
DateRange,
} from "./connector-popup.schemas";
// Hooks // Hooks
export { useConnectorDialog } from "./use-connector-dialog"; export { useConnectorDialog } from "./use-connector-dialog";

View file

@ -1,9 +1,10 @@
"use client"; "use client";
import { ArrowLeft, Check, Loader2 } from "lucide-react"; import { ArrowLeft, Check, Loader2 } from "lucide-react";
import { type FC } from "react"; import { type FC, useState, useCallback, useRef, useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import { cn } from "@/lib/utils";
import type { IndexingConfigState } from "./connector-constants"; import type { IndexingConfigState } from "./connector-constants";
import { DateRangeSelector } from "./date-range-selector"; import { DateRangeSelector } from "./date-range-selector";
import { PeriodicSyncConfig } from "./periodic-sync-config"; import { PeriodicSyncConfig } from "./periodic-sync-config";
@ -37,10 +38,49 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
onStartIndexing, onStartIndexing,
onSkip, onSkip,
}) => { }) => {
const [isScrolled, setIsScrolled] = useState(false);
const [hasMoreContent, setHasMoreContent] = useState(false);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const checkScrollState = useCallback(() => {
if (!scrollContainerRef.current) return;
const target = scrollContainerRef.current;
const scrolled = target.scrollTop > 0;
const hasMore = target.scrollHeight > target.clientHeight &&
target.scrollTop + target.clientHeight < target.scrollHeight - 10;
setIsScrolled(scrolled);
setHasMoreContent(hasMore);
}, []);
const handleScroll = useCallback(() => {
checkScrollState();
}, [checkScrollState]);
// Check initial scroll state and on resize
useEffect(() => {
checkScrollState();
const resizeObserver = new ResizeObserver(() => {
checkScrollState();
});
if (scrollContainerRef.current) {
resizeObserver.observe(scrollContainerRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, [checkScrollState]);
return ( return (
<div className="flex-1 flex flex-col min-h-0 overflow-hidden"> <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
{/* Fixed Header */} {/* Fixed Header */}
<div className="flex-shrink-0 px-6 sm:px-12 pt-8 sm:pt-10"> <div className={cn(
"flex-shrink-0 px-6 sm:px-12 pt-8 sm:pt-10 transition-shadow duration-200 relative z-10",
isScrolled && "shadow-sm"
)}>
{/* Back button */} {/* Back button */}
<button <button
type="button" type="button"
@ -68,39 +108,53 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
</div> </div>
{/* Scrollable Content */} {/* Scrollable Content */}
<div className="flex-1 min-h-0 overflow-y-auto px-6 sm:px-12"> <div className="flex-1 min-h-0 relative overflow-hidden">
<div className="space-y-6 pb-6"> <div
<DateRangeSelector ref={scrollContainerRef}
startDate={startDate} className="h-full overflow-y-auto px-6 sm:px-12"
endDate={endDate} onScroll={handleScroll}
onStartDateChange={onStartDateChange} >
onEndDateChange={onEndDateChange} <div className="space-y-6 pb-6 pt-2">
/> <DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartDateChange={onStartDateChange}
onEndDateChange={onEndDateChange}
/>
<PeriodicSyncConfig <PeriodicSyncConfig
enabled={periodicEnabled} enabled={periodicEnabled}
frequencyMinutes={frequencyMinutes} frequencyMinutes={frequencyMinutes}
onEnabledChange={onPeriodicEnabledChange} onEnabledChange={onPeriodicEnabledChange}
onFrequencyChange={onFrequencyChange} onFrequencyChange={onFrequencyChange}
/> />
{/* Info box */} {/* Info box */}
<div className="rounded-xl border border-border bg-primary/5 p-4 flex items-start gap-3"> <div className="rounded-xl border border-border bg-primary/5 p-4 flex items-start gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 shrink-0 mt-0.5"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 shrink-0 mt-0.5">
{getConnectorIcon(config.connectorType, "size-4")} {getConnectorIcon(config.connectorType, "size-4")}
</div> </div>
<div className="text-sm"> <div className="text-sm">
<p className="font-medium">Indexing runs in the background</p> <p className="font-medium">Indexing runs in the background</p>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1">
You can continue using SurfSense while we sync your data. Check the Active tab to see progress. You can continue using SurfSense while we sync your data. Check the Active tab to see progress.
</p> </p>
</div>
</div> </div>
</div> </div>
</div> </div>
{/* Top fade shadow - appears when scrolled */}
{isScrolled && (
<div className="absolute top-0 left-0 right-0 h-6 bg-gradient-to-b from-muted/50 to-transparent pointer-events-none z-10" />
)}
{/* Bottom fade shadow - appears when there's more content */}
{hasMoreContent && (
<div className="absolute bottom-0 left-0 right-0 h-3 bg-gradient-to-t from-muted/50 to-transparent pointer-events-none z-10" />
)}
</div> </div>
{/* Fixed Footer - Action buttons */} {/* Fixed Footer - Action buttons */}
<div className="flex-shrink-0 flex items-center justify-between px-6 sm:px-12 py-6 border-t border-border bg-muted"> <div className="flex-shrink-0 flex items-center justify-between px-6 sm:px-12 py-6 bg-muted">
<Button variant="ghost" onClick={onSkip} disabled={isStartingIndexing}> <Button variant="ghost" onClick={onSkip} disabled={isStartingIndexing}>
Skip for now Skip for now
</Button> </Button>

View file

@ -10,8 +10,16 @@ import { queryClient } from "@/lib/query-client/client";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
import { format } from "date-fns"; import { format } from "date-fns";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { searchSourceConnector } from "@/contracts/types/connector.types";
import { OAUTH_CONNECTORS } from "./connector-constants"; import { OAUTH_CONNECTORS } from "./connector-constants";
import type { IndexingConfigState } from "./connector-constants"; import type { IndexingConfigState } from "./connector-constants";
import {
parseConnectorPopupQueryParams,
parseOAuthAuthResponse,
validateIndexingConfigState,
frequencyMinutesSchema,
dateRangeSchema,
} from "./connector-popup.schemas";
export const useConnectorDialog = () => { export const useConnectorDialog = () => {
const router = useRouter(); const router = useRouter();
@ -48,65 +56,101 @@ export const useConnectorDialog = () => {
// Synchronize state with URL query params // Synchronize state with URL query params
useEffect(() => { useEffect(() => {
const modalParam = searchParams.get("modal"); try {
const tabParam = searchParams.get("tab"); const params = parseConnectorPopupQueryParams(searchParams);
const viewParam = searchParams.get("view");
const connectorParam = searchParams.get("connector");
if (modalParam === "connectors") {
if (!isOpen) setIsOpen(true);
if (tabParam === "active" || tabParam === "all") { if (params.modal === "connectors") {
if (activeTab !== tabParam) setActiveTab(tabParam); setIsOpen(true);
}
if (params.tab === "active" || params.tab === "all") {
if (viewParam === "configure" && connectorParam && !indexingConfig) { setActiveTab(params.tab);
const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === connectorParam); }
if (oauthConnector && allConnectors) {
const existingConnector = allConnectors.find( // Clear indexing config if view is not "configure" anymore
(c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType if (params.view !== "configure" && indexingConfig) {
); setIndexingConfig(null);
if (existingConnector) { }
setIndexingConfig({
connectorType: oauthConnector.connectorType, if (params.view === "configure" && params.connector && !indexingConfig) {
connectorId: existingConnector.id, const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === params.connector);
connectorTitle: oauthConnector.title, if (oauthConnector && allConnectors) {
}); const existingConnector = allConnectors.find(
(c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType
);
if (existingConnector) {
// Validate connector data before setting state
const connectorValidation = searchSourceConnector.safeParse(existingConnector);
if (connectorValidation.success) {
const config = validateIndexingConfigState({
connectorType: oauthConnector.connectorType,
connectorId: existingConnector.id,
connectorTitle: oauthConnector.title,
});
setIndexingConfig(config);
}
}
} }
} }
} else {
setIsOpen(false);
// Clear indexing config when modal is closed
if (indexingConfig) {
setIndexingConfig(null);
setStartDate(undefined);
setEndDate(undefined);
setPeriodicEnabled(false);
setFrequencyMinutes("1440");
setIsScrolled(false);
setSearchQuery("");
}
} }
} else { } catch (error) {
if (isOpen) setIsOpen(false); // Invalid query params - log but don't crash
console.warn("Invalid connector popup query params:", error);
} }
}, [searchParams, isOpen, activeTab, indexingConfig, allConnectors]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams, allConnectors]);
// Detect OAuth success and transition to config view // Detect OAuth success and transition to config view
useEffect(() => { useEffect(() => {
const success = searchParams.get("success"); try {
const connectorParam = searchParams.get("connector"); const params = parseConnectorPopupQueryParams(searchParams);
const modalParam = searchParams.get("modal");
if (params.success === "true" && params.connector && searchSpaceId && params.modal === "connectors") {
if (success === "true" && connectorParam && searchSpaceId && modalParam === "connectors") { const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === params.connector);
const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === connectorParam); if (oauthConnector) {
if (oauthConnector) { refetchAllConnectors().then((result) => {
refetchAllConnectors().then((result) => { if (!result.data) return;
const newConnector = result.data?.find(
(c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType const newConnector = result.data.find(
); (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType
if (newConnector) { );
setIndexingConfig({ if (newConnector) {
connectorType: oauthConnector.connectorType, // Validate connector data before setting state
connectorId: newConnector.id, const connectorValidation = searchSourceConnector.safeParse(newConnector);
connectorTitle: oauthConnector.title, if (connectorValidation.success) {
}); const config = validateIndexingConfigState({
setIsOpen(true); connectorType: oauthConnector.connectorType,
const url = new URL(window.location.href); connectorId: newConnector.id,
url.searchParams.delete("success"); connectorTitle: oauthConnector.title,
url.searchParams.set("view", "configure"); });
window.history.replaceState({}, "", url.toString()); setIndexingConfig(config);
} setIsOpen(true);
}); const url = new URL(window.location.href);
url.searchParams.delete("success");
url.searchParams.set("view", "configure");
window.history.replaceState({}, "", url.toString());
} else {
console.warn("Invalid connector data after OAuth:", connectorValidation.error);
toast.error("Failed to validate connector data");
}
}
});
}
} }
} catch (error) {
// Invalid query params - log but don't crash
console.warn("Invalid connector popup query params in OAuth success handler:", error);
} }
}, [searchParams, searchSpaceId, refetchAllConnectors]); }, [searchParams, searchSpaceId, refetchAllConnectors]);
@ -115,8 +159,10 @@ export const useConnectorDialog = () => {
async (connector: (typeof OAUTH_CONNECTORS)[0]) => { async (connector: (typeof OAUTH_CONNECTORS)[0]) => {
if (!searchSpaceId || !connector.authEndpoint) return; if (!searchSpaceId || !connector.authEndpoint) return;
// Set connecting state immediately to disable button and show spinner
setConnectingId(connector.id);
try { try {
setConnectingId(connector.id);
const response = await authenticatedFetch( const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${connector.authEndpoint}?space_id=${searchSpaceId}`, `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${connector.authEndpoint}?space_id=${searchSpaceId}`,
{ method: "GET" } { method: "GET" }
@ -127,11 +173,21 @@ export const useConnectorDialog = () => {
} }
const data = await response.json(); const data = await response.json();
window.location.href = data.auth_url;
// Validate OAuth response with Zod
const validatedData = parseOAuthAuthResponse(data);
// Don't clear connectingId here - let the redirect happen with button still disabled
// The component will unmount on redirect anyway
window.location.href = validatedData.auth_url;
} catch (error) { } catch (error) {
console.error(`Error connecting to ${connector.title}:`, error); console.error(`Error connecting to ${connector.title}:`, error);
toast.error(`Failed to connect to ${connector.title}`); if (error instanceof Error && error.message.includes("Invalid auth URL")) {
} finally { toast.error(`Invalid response from ${connector.title} OAuth endpoint`);
} else {
toast.error(`Failed to connect to ${connector.title}`);
}
// Only clear connectingId on error so user can retry
setConnectingId(null); setConnectingId(null);
} }
}, },
@ -142,6 +198,22 @@ export const useConnectorDialog = () => {
const handleStartIndexing = useCallback(async (refreshConnectors: () => void) => { const handleStartIndexing = useCallback(async (refreshConnectors: () => void) => {
if (!indexingConfig || !searchSpaceId) return; if (!indexingConfig || !searchSpaceId) return;
// Validate date range
const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate });
if (!dateRangeValidation.success) {
toast.error(dateRangeValidation.error.errors[0]?.message || "Invalid date range");
return;
}
// Validate frequency minutes if periodic is enabled
if (periodicEnabled) {
const frequencyValidation = frequencyMinutesSchema.safeParse(frequencyMinutes);
if (!frequencyValidation.success) {
toast.error("Invalid frequency value");
return;
}
}
setIsStartingIndexing(true); setIsStartingIndexing(true);
try { try {
const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined;
@ -173,18 +245,14 @@ export const useConnectorDialog = () => {
: "You can continue working while we sync your data.", : "You can continue working while we sync your data.",
}); });
setIndexingConfig(null); // Update URL - the effect will handle closing the modal and clearing state
setStartDate(undefined);
setEndDate(undefined);
setPeriodicEnabled(false);
setFrequencyMinutes("1440");
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.delete("view"); url.searchParams.delete("modal");
url.searchParams.delete("tab");
url.searchParams.delete("success");
url.searchParams.delete("connector"); url.searchParams.delete("connector");
url.searchParams.set("tab", "active"); url.searchParams.delete("view");
window.history.replaceState({}, "", url.toString()); router.replace(url.pathname + url.search, { scroll: false });
setActiveTab("active");
refreshConnectors(); refreshConnectors();
queryClient.invalidateQueries({ queryClient.invalidateQueries({
@ -196,21 +264,19 @@ export const useConnectorDialog = () => {
} finally { } finally {
setIsStartingIndexing(false); setIsStartingIndexing(false);
} }
}, [indexingConfig, searchSpaceId, startDate, endDate, indexConnector, updateConnector, periodicEnabled, frequencyMinutes, getFrequencyLabel]); }, [indexingConfig, searchSpaceId, startDate, endDate, indexConnector, updateConnector, periodicEnabled, frequencyMinutes, getFrequencyLabel, router]);
// Handle skipping indexing // Handle skipping indexing
const handleSkipIndexing = useCallback(() => { const handleSkipIndexing = useCallback(() => {
setIndexingConfig(null); // Update URL - the effect will handle closing the modal and clearing state
setStartDate(undefined);
setEndDate(undefined);
setPeriodicEnabled(false);
setFrequencyMinutes("1440");
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.delete("view"); url.searchParams.delete("modal");
url.searchParams.delete("tab");
url.searchParams.delete("success");
url.searchParams.delete("connector"); url.searchParams.delete("connector");
window.history.replaceState({}, "", url.toString()); url.searchParams.delete("view");
}, []); router.replace(url.pathname + url.search, { scroll: false });
}, [router]);
// Handle dialog open/close // Handle dialog open/close
const handleOpenChange = useCallback( const handleOpenChange = useCallback(