revert Composio Drive to folder tree, harden Picker for native Drive

This commit is contained in:
CREDO23 2026-03-10 23:06:33 +02:00
parent 2c9d01ba2d
commit 3bda6c1679
5 changed files with 558 additions and 436 deletions

View file

@ -4,8 +4,9 @@ import { useAtomValue } from "jotai";
import { AlertTriangle, Cable, Settings } from "lucide-react"; import { AlertTriangle, Cable, Settings } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { type FC, forwardRef, useImperativeHandle, useMemo } from "react"; import { type FC, forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
import { import {
globalNewLLMConfigsAtom, globalNewLLMConfigsAtom,
llmPreferencesAtom, llmPreferencesAtom,
@ -19,8 +20,8 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsContent } from "@/components/ui/tabs"; import { Tabs, TabsContent } from "@/components/ui/tabs";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
import { useConnectorsElectric } from "@/hooks/use-connectors-electric"; import { useConnectorsElectric } from "@/hooks/use-connectors-electric";
import { PICKER_CLOSE_EVENT, PICKER_OPEN_EVENT } from "@/hooks/use-google-picker";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header"; import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view"; import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view";
@ -47,400 +48,426 @@ interface ConnectorIndicatorProps {
export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>( export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>(
({ showTrigger = true }, ref) => { ({ showTrigger = true }, ref) => {
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { data: currentUser } = useAtomValue(currentUserAtom); const { data: currentUser } = useAtomValue(currentUserAtom);
const { data: preferences = {}, isFetching: preferencesLoading } = const { data: preferences = {}, isFetching: preferencesLoading } =
useAtomValue(llmPreferencesAtom); useAtomValue(llmPreferencesAtom);
const { data: globalConfigs = [], isFetching: globalConfigsLoading } = const { data: globalConfigs = [], isFetching: globalConfigsLoading } =
useAtomValue(globalNewLLMConfigsAtom); useAtomValue(globalNewLLMConfigsAtom);
// Check if document summary LLM is properly configured // Check if document summary LLM is properly configured
// - If ID is 0 (Auto mode), we need global configs to be available // - If ID is 0 (Auto mode), we need global configs to be available
// - If ID is positive (user config) or negative (specific global config), it's configured // - If ID is positive (user config) or negative (specific global config), it's configured
// - If ID is null/undefined, it's not configured // - If ID is null/undefined, it's not configured
const docSummaryLlmId = preferences.document_summary_llm_id; const docSummaryLlmId = preferences.document_summary_llm_id;
const isAutoMode = docSummaryLlmId === 0; const isAutoMode = docSummaryLlmId === 0;
const hasGlobalConfigs = globalConfigs.length > 0; const hasGlobalConfigs = globalConfigs.length > 0;
const hasDocumentSummaryLLM = const hasDocumentSummaryLLM =
docSummaryLlmId !== null && docSummaryLlmId !== null &&
docSummaryLlmId !== undefined && docSummaryLlmId !== undefined &&
// If it's Auto mode, we need global configs to actually be available // If it's Auto mode, we need global configs to actually be available
(!isAutoMode || hasGlobalConfigs); (!isAutoMode || hasGlobalConfigs);
const llmConfigLoading = preferencesLoading || globalConfigsLoading; const llmConfigLoading = preferencesLoading || globalConfigsLoading;
// Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min) // Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min)
const { data: documentTypeCounts, isFetching: documentTypesLoading } = const { data: documentTypeCounts, isFetching: documentTypesLoading } =
useAtomValue(documentTypeCountsAtom); useAtomValue(documentTypeCountsAtom);
// Read status inbox items from shared atom (populated by LayoutDataProvider) // Read status inbox items from shared atom (populated by LayoutDataProvider)
// instead of creating a duplicate useInbox("status") hook. // instead of creating a duplicate useInbox("status") hook.
const statusInboxItems = useAtomValue(statusInboxItemsAtom); const statusInboxItems = useAtomValue(statusInboxItemsAtom);
const inboxItems = useMemo( const inboxItems = useMemo(
() => statusInboxItems.filter((item) => item.type === "connector_indexing"), () => statusInboxItems.filter((item) => item.type === "connector_indexing"),
[statusInboxItems] [statusInboxItems]
); );
// Check if YouTube view is active // Check if YouTube view is active
const isYouTubeView = searchParams.get("view") === "youtube"; const isYouTubeView = searchParams.get("view") === "youtube";
// Use the custom hook for dialog state management // Use the custom hook for dialog state management
const { const {
isOpen, isOpen,
activeTab, activeTab,
connectingId, connectingId,
isScrolled, isScrolled,
searchQuery, searchQuery,
indexingConfig, indexingConfig,
indexingConnector, indexingConnector,
indexingConnectorConfig, indexingConnectorConfig,
editingConnector, editingConnector,
connectingConnectorType, connectingConnectorType,
isCreatingConnector, isCreatingConnector,
startDate, startDate,
endDate, endDate,
isStartingIndexing, isStartingIndexing,
isSaving, isSaving,
isDisconnecting, isDisconnecting,
periodicEnabled, periodicEnabled,
frequencyMinutes, frequencyMinutes,
enableSummary, enableSummary,
allConnectors, allConnectors,
viewingAccountsType, viewingAccountsType,
viewingMCPList, viewingMCPList,
setSearchQuery, setSearchQuery,
setStartDate, setStartDate,
setEndDate, setEndDate,
setPeriodicEnabled, setPeriodicEnabled,
setFrequencyMinutes, setFrequencyMinutes,
setEnableSummary, setEnableSummary,
handleOpenChange, handleOpenChange,
handleTabChange, handleTabChange,
handleScroll, handleScroll,
handleConnectOAuth, handleConnectOAuth,
handleConnectNonOAuth, handleConnectNonOAuth,
handleCreateWebcrawler, handleCreateWebcrawler,
handleCreateYouTubeCrawler, handleCreateYouTubeCrawler,
handleSubmitConnectForm, handleSubmitConnectForm,
handleStartIndexing, handleStartIndexing,
handleSkipIndexing, handleSkipIndexing,
handleStartEdit, handleStartEdit,
handleSaveConnector, handleSaveConnector,
handleDisconnectConnector, handleDisconnectConnector,
handleBackFromEdit, handleBackFromEdit,
handleBackFromConnect, handleBackFromConnect,
handleBackFromYouTube, handleBackFromYouTube,
handleViewAccountsList, handleViewAccountsList,
handleBackFromAccountsList, handleBackFromAccountsList,
handleBackFromMCPList, handleBackFromMCPList,
handleAddNewMCPFromList, handleAddNewMCPFromList,
handleQuickIndexConnector, handleQuickIndexConnector,
connectorConfig, connectorConfig,
setConnectorConfig, setConnectorConfig,
setIndexingConnectorConfig, setIndexingConnectorConfig,
setConnectorName, setConnectorName,
} = useConnectorDialog(); } = useConnectorDialog();
// Fetch connectors using Electric SQL + PGlite for real-time updates const [pickerOpen, setPickerOpen] = useState(false);
// This provides instant updates when connectors change, without polling useEffect(() => {
const { const onOpen = () => setPickerOpen(true);
connectors: connectorsFromElectric = [], const onClose = () => setPickerOpen(false);
loading: connectorsLoading, window.addEventListener(PICKER_OPEN_EVENT, onOpen);
error: connectorsError, window.addEventListener(PICKER_CLOSE_EVENT, onClose);
refreshConnectors: refreshConnectorsElectric, return () => {
} = useConnectorsElectric(searchSpaceId); window.removeEventListener(PICKER_OPEN_EVENT, onOpen);
window.removeEventListener(PICKER_CLOSE_EVENT, onClose);
};
}, []);
// Fallback to API if Electric is not available or fails // Fetch connectors using Electric SQL + PGlite for real-time updates
// Use Electric data if: 1) we have data, or 2) still loading without error // This provides instant updates when connectors change, without polling
// Use API data if: Electric failed (has error) or finished loading with no data const {
const useElectricData = connectors: connectorsFromElectric = [],
connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError); loading: connectorsLoading,
const connectors = useElectricData ? connectorsFromElectric : allConnectors || []; error: connectorsError,
refreshConnectors: refreshConnectorsElectric,
} = useConnectorsElectric(searchSpaceId);
// Manual refresh function that works with both Electric and API // Fallback to API if Electric is not available or fails
const refreshConnectors = async () => { // Use Electric data if: 1) we have data, or 2) still loading without error
if (useElectricData) { // Use API data if: Electric failed (has error) or finished loading with no data
await refreshConnectorsElectric(); const useElectricData =
} else { connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError);
// Fallback: use allConnectors from useConnectorDialog (which uses connectorsAtom) const connectors = useElectricData ? connectorsFromElectric : allConnectors || [];
// The connectorsAtom will handle refetching if needed
}
};
// Track indexing state locally - clears automatically when Electric SQL detects last_indexed_at changed // Manual refresh function that works with both Electric and API
// Also clears when failed notifications are detected const refreshConnectors = async () => {
const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors( if (useElectricData) {
connectors as SearchSourceConnector[], await refreshConnectorsElectric();
inboxItems } else {
); // Fallback: use allConnectors from useConnectorDialog (which uses connectorsAtom)
// The connectorsAtom will handle refetching if needed
}
};
const isLoading = connectorsLoading || documentTypesLoading; // Track indexing state locally - clears automatically when Electric SQL detects last_indexed_at changed
// Also clears when failed notifications are detected
const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors(
connectors as SearchSourceConnector[],
inboxItems
);
// Get document types that have documents in the search space const isLoading = connectorsLoading || documentTypesLoading;
const activeDocumentTypes = documentTypeCounts
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
: [];
const hasConnectors = connectors.length > 0; // Get document types that have documents in the search space
const hasSources = hasConnectors || activeDocumentTypes.length > 0; const activeDocumentTypes = documentTypeCounts
const totalSourceCount = connectors.length + activeDocumentTypes.length; ? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
: [];
const activeConnectorsCount = connectors.length; const hasConnectors = connectors.length > 0;
const hasSources = hasConnectors || activeDocumentTypes.length > 0;
const totalSourceCount = connectors.length + activeDocumentTypes.length;
// Check which connectors are already connected const activeConnectorsCount = connectors.length;
// Using Electric SQL + PGlite for real-time connector updates
const connectedTypes = new Set<string>(
(connectors || []).map((c: SearchSourceConnector) => c.connector_type)
);
useImperativeHandle(ref, () => ({ // Check which connectors are already connected
open: () => handleOpenChange(true), // Using Electric SQL + PGlite for real-time connector updates
})); const connectedTypes = new Set<string>(
(connectors || []).map((c: SearchSourceConnector) => c.connector_type)
);
if (!searchSpaceId) return null; useImperativeHandle(ref, () => ({
open: () => handleOpenChange(true),
}));
return ( if (!searchSpaceId) return null;
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
{showTrigger && (
<TooltipIconButton
data-joyride="connector-icon"
tooltip={hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"}
side="bottom"
className={cn(
"size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",
"hover:bg-muted-foreground/15 dark:hover:bg-muted-foreground/30",
"outline-none focus:outline-none focus-visible:outline-none font-semibold text-xs",
"border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none"
)}
aria-label={
hasConnectors ? `View ${activeConnectorsCount} connectors` : "Add your first connector"
}
onClick={() => handleOpenChange(true)}
>
{isLoading ? (
<Spinner size="sm" />
) : (
<>
<Cable className="size-4 stroke-[1.5px]" />
{activeConnectorsCount > 0 && (
<span className="absolute -top-0.5 right-0 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm select-none">
{activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
</span>
)}
</>
)}
</TooltipIconButton>
)}
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 dark:ring-0 bg-muted dark:bg-muted text-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5 select-none"> return (
<DialogTitle className="sr-only">Manage Connectors</DialogTitle> <Dialog
{/* YouTube Crawler View - shown when adding YouTube videos */} open={isOpen}
{isYouTubeView && searchSpaceId ? ( onOpenChange={(open) => {
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} /> if (!open && pickerOpen) return;
) : viewingMCPList ? ( handleOpenChange(open);
<ConnectorAccountsListView }}
connectorType="MCP_CONNECTOR" modal={!pickerOpen}
connectorTitle="MCP Connectors" >
connectors={(allConnectors || []) as SearchSourceConnector[]} {showTrigger && (
indexingConnectorIds={indexingConnectorIds} <TooltipIconButton
onBack={handleBackFromMCPList} data-joyride="connector-icon"
onManage={handleStartEdit} tooltip={
onAddAccount={handleAddNewMCPFromList} hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"
addButtonText="Add New MCP Server"
/>
) : viewingAccountsType ? (
<ConnectorAccountsListView
connectorType={viewingAccountsType.connectorType}
connectorTitle={viewingAccountsType.connectorTitle}
connectors={(connectors || []) as SearchSourceConnector[]} // Using Electric SQL + PGlite for real-time connector updates (all connector types)
indexingConnectorIds={indexingConnectorIds}
onBack={handleBackFromAccountsList}
onManage={handleStartEdit}
onAddAccount={() => {
// Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS
const oauthConnector =
OAUTH_CONNECTORS.find(
(c) => c.connectorType === viewingAccountsType.connectorType
) ||
COMPOSIO_CONNECTORS.find(
(c) => c.connectorType === viewingAccountsType.connectorType
);
if (oauthConnector) {
handleConnectOAuth(oauthConnector);
}
}}
isConnecting={connectingId !== null}
/>
) : connectingConnectorType ? (
<ConnectorConnectView
connectorType={connectingConnectorType}
onSubmit={(formData) => handleSubmitConnectForm(formData, startIndexing)}
onBack={handleBackFromConnect}
isSubmitting={isCreatingConnector}
/>
) : editingConnector ? (
<ConnectorEditView
connector={{
...editingConnector,
config: connectorConfig || editingConnector.config,
name: editingConnector.name,
// Sync last_indexed_at with live data from Electric SQL for real-time updates
last_indexed_at:
(connectors as SearchSourceConnector[]).find((c) => c.id === editingConnector.id)
?.last_indexed_at ?? editingConnector.last_indexed_at,
}}
startDate={startDate}
endDate={endDate}
periodicEnabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
enableSummary={enableSummary}
isSaving={isSaving}
isDisconnecting={isDisconnecting}
isIndexing={indexingConnectorIds.has(editingConnector.id)}
searchSpaceId={searchSpaceId?.toString()}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled}
onFrequencyChange={setFrequencyMinutes}
onEnableSummaryChange={setEnableSummary}
onSave={() => {
startIndexing(editingConnector.id);
handleSaveConnector(() => refreshConnectors());
}}
onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())}
onBack={handleBackFromEdit}
onQuickIndex={
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR"
? () => {
startIndexing(editingConnector.id);
handleQuickIndexConnector(
editingConnector.id,
editingConnector.connector_type,
stopIndexing,
startDate,
endDate
);
}
: undefined
} }
onConfigChange={setConnectorConfig} side="bottom"
onNameChange={setConnectorName} className={cn(
/> "size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",
) : indexingConfig ? ( "hover:bg-muted-foreground/15 dark:hover:bg-muted-foreground/30",
<IndexingConfigurationView "outline-none focus:outline-none focus-visible:outline-none font-semibold text-xs",
config={indexingConfig} "border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none"
connector={ )}
indexingConnector aria-label={
? { hasConnectors
...indexingConnector, ? `View ${activeConnectorsCount} connectors`
config: indexingConnectorConfig || indexingConnector.config, : "Add your first connector"
}
: undefined
} }
startDate={startDate} onClick={() => handleOpenChange(true)}
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}
/>
) : (
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="flex-1 flex flex-col min-h-0"
> >
{/* Header */} {isLoading ? (
<ConnectorDialogHeader <Spinner size="sm" />
activeTab={activeTab} ) : (
totalSourceCount={activeConnectorsCount} <>
searchQuery={searchQuery} <Cable className="size-4 stroke-[1.5px]" />
onTabChange={handleTabChange} {activeConnectorsCount > 0 && (
onSearchChange={setSearchQuery} <span className="absolute -top-0.5 right-0 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm select-none">
isScrolled={isScrolled} {activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
</span>
)}
</>
)}
</TooltipIconButton>
)}
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 dark:ring-0 bg-muted dark:bg-muted text-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5 select-none">
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
{/* YouTube Crawler View - shown when adding YouTube videos */}
{isYouTubeView && searchSpaceId ? (
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />
) : viewingMCPList ? (
<ConnectorAccountsListView
connectorType="MCP_CONNECTOR"
connectorTitle="MCP Connectors"
connectors={(allConnectors || []) as SearchSourceConnector[]}
indexingConnectorIds={indexingConnectorIds}
onBack={handleBackFromMCPList}
onManage={handleStartEdit}
onAddAccount={handleAddNewMCPFromList}
addButtonText="Add New MCP Server"
/> />
) : viewingAccountsType ? (
<ConnectorAccountsListView
connectorType={viewingAccountsType.connectorType}
connectorTitle={viewingAccountsType.connectorTitle}
connectors={(connectors || []) as SearchSourceConnector[]} // Using Electric SQL + PGlite for real-time connector updates (all connector types)
indexingConnectorIds={indexingConnectorIds}
onBack={handleBackFromAccountsList}
onManage={handleStartEdit}
onAddAccount={() => {
// Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS
const oauthConnector =
OAUTH_CONNECTORS.find(
(c) => c.connectorType === viewingAccountsType.connectorType
) ||
COMPOSIO_CONNECTORS.find(
(c) => c.connectorType === viewingAccountsType.connectorType
);
if (oauthConnector) {
handleConnectOAuth(oauthConnector);
}
}}
isConnecting={connectingId !== null}
/>
) : connectingConnectorType ? (
<ConnectorConnectView
connectorType={connectingConnectorType}
onSubmit={(formData) => handleSubmitConnectForm(formData, startIndexing)}
onBack={handleBackFromConnect}
isSubmitting={isCreatingConnector}
/>
) : editingConnector ? (
<ConnectorEditView
connector={{
...editingConnector,
config: connectorConfig || editingConnector.config,
name: editingConnector.name,
// Sync last_indexed_at with live data from Electric SQL for real-time updates
last_indexed_at:
(connectors as SearchSourceConnector[]).find((c) => c.id === editingConnector.id)
?.last_indexed_at ?? editingConnector.last_indexed_at,
}}
startDate={startDate}
endDate={endDate}
periodicEnabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
enableSummary={enableSummary}
isSaving={isSaving}
isDisconnecting={isDisconnecting}
isIndexing={indexingConnectorIds.has(editingConnector.id)}
searchSpaceId={searchSpaceId?.toString()}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled}
onFrequencyChange={setFrequencyMinutes}
onEnableSummaryChange={setEnableSummary}
onSave={() => {
startIndexing(editingConnector.id);
handleSaveConnector(() => refreshConnectors());
}}
onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())}
onBack={handleBackFromEdit}
onQuickIndex={
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR"
? () => {
startIndexing(editingConnector.id);
handleQuickIndexConnector(
editingConnector.id,
editingConnector.connector_type,
stopIndexing,
startDate,
endDate
);
}
: undefined
}
onConfigChange={setConnectorConfig}
onNameChange={setConnectorName}
/>
) : indexingConfig ? (
<IndexingConfigurationView
config={indexingConfig}
connector={
indexingConnector
? {
...indexingConnector,
config: indexingConnectorConfig || indexingConnector.config,
}
: undefined
}
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}
/>
) : (
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="flex-1 flex flex-col min-h-0"
>
{/* Header */}
<ConnectorDialogHeader
activeTab={activeTab}
totalSourceCount={activeConnectorsCount}
searchQuery={searchQuery}
onTabChange={handleTabChange}
onSearchChange={setSearchQuery}
isScrolled={isScrolled}
/>
{/* Content */} {/* Content */}
<div className="flex-1 min-h-0 relative overflow-hidden"> <div className="flex-1 min-h-0 relative overflow-hidden">
<div className="h-full overflow-y-auto" onScroll={handleScroll}> <div className="h-full overflow-y-auto" onScroll={handleScroll}>
<div className="px-4 sm:px-12 py-4 sm:py-8 pb-12 sm:pb-16"> <div className="px-4 sm:px-12 py-4 sm:py-8 pb-12 sm:pb-16">
{/* LLM Configuration Warning */} {/* LLM Configuration Warning */}
{!llmConfigLoading && !hasDocumentSummaryLLM && ( {!llmConfigLoading && !hasDocumentSummaryLLM && (
<Alert variant="destructive" className="mb-6"> <Alert variant="destructive" className="mb-6">
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
<AlertTitle>LLM Configuration Required</AlertTitle> <AlertTitle>LLM Configuration Required</AlertTitle>
<AlertDescription className="mt-2"> <AlertDescription className="mt-2">
<p className="mb-3"> <p className="mb-3">
{isAutoMode && !hasGlobalConfigs {isAutoMode && !hasGlobalConfigs
? "Auto mode is selected but no global LLM configurations are available. Please configure a custom LLM in Settings to process and summarize documents from your connected sources." ? "Auto mode is selected but no global LLM configurations are available. Please configure a custom LLM in Settings to process and summarize documents from your connected sources."
: "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."} : "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."}
</p> </p>
<Button asChild size="sm" variant="outline"> <Button asChild size="sm" variant="outline">
<Link href={`/dashboard/${searchSpaceId}/settings?tab=models`}> <Link href={`/dashboard/${searchSpaceId}/settings?tab=models`}>
<Settings className="mr-2 h-4 w-4" /> <Settings className="mr-2 h-4 w-4" />
Go to Settings Go to Settings
</Link> </Link>
</Button> </Button>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
<TabsContent value="all" className="m-0"> <TabsContent value="all" className="m-0">
<AllConnectorsTab <AllConnectorsTab
searchQuery={searchQuery}
searchSpaceId={searchSpaceId}
connectedTypes={connectedTypes}
connectingId={connectingId}
allConnectors={connectors}
documentTypeCounts={documentTypeCounts}
indexingConnectorIds={indexingConnectorIds}
onConnectOAuth={hasDocumentSummaryLLM ? handleConnectOAuth : () => {}}
onConnectNonOAuth={hasDocumentSummaryLLM ? handleConnectNonOAuth : () => {}}
onCreateWebcrawler={
hasDocumentSummaryLLM ? handleCreateWebcrawler : () => {}
}
onCreateYouTubeCrawler={
hasDocumentSummaryLLM ? handleCreateYouTubeCrawler : () => {}
}
onManage={handleStartEdit}
onViewAccountsList={handleViewAccountsList}
/>
</TabsContent>
<ActiveConnectorsTab
searchQuery={searchQuery} searchQuery={searchQuery}
searchSpaceId={searchSpaceId} hasSources={hasSources}
connectedTypes={connectedTypes} totalSourceCount={totalSourceCount}
connectingId={connectingId} activeDocumentTypes={activeDocumentTypes}
allConnectors={connectors} connectors={connectors as SearchSourceConnector[]}
documentTypeCounts={documentTypeCounts}
indexingConnectorIds={indexingConnectorIds} indexingConnectorIds={indexingConnectorIds}
onConnectOAuth={hasDocumentSummaryLLM ? handleConnectOAuth : () => {}} onTabChange={handleTabChange}
onConnectNonOAuth={hasDocumentSummaryLLM ? handleConnectNonOAuth : () => {}}
onCreateWebcrawler={hasDocumentSummaryLLM ? handleCreateWebcrawler : () => {}}
onCreateYouTubeCrawler={
hasDocumentSummaryLLM ? handleCreateYouTubeCrawler : () => {}
}
onManage={handleStartEdit} onManage={handleStartEdit}
onViewAccountsList={handleViewAccountsList} onViewAccountsList={handleViewAccountsList}
/> />
</TabsContent> </div>
<ActiveConnectorsTab
searchQuery={searchQuery}
hasSources={hasSources}
totalSourceCount={totalSourceCount}
activeDocumentTypes={activeDocumentTypes}
connectors={connectors as SearchSourceConnector[]}
indexingConnectorIds={indexingConnectorIds}
onTabChange={handleTabChange}
onManage={handleStartEdit}
onViewAccountsList={handleViewAccountsList}
/>
</div> </div>
{/* Bottom fade shadow */}
<div className="absolute bottom-0 left-0 right-0 h-7 bg-linear-to-t from-muted via-muted/80 to-transparent pointer-events-none z-10" />
</div> </div>
{/* Bottom fade shadow */} </Tabs>
<div className="absolute bottom-0 left-0 right-0 h-7 bg-linear-to-t from-muted via-muted/80 to-transparent pointer-events-none z-10" /> )}
</div> </DialogContent>
</Tabs> </Dialog>
)} );
</DialogContent> }
</Dialog> );
);
});
ConnectorIndicator.displayName = "ConnectorIndicator"; ConnectorIndicator.displayName = "ConnectorIndicator";

View file

@ -6,12 +6,12 @@ import {
FileText, FileText,
FolderClosed, FolderClosed,
Image, Image,
Loader2,
Presentation, Presentation,
X, X,
} from "lucide-react"; } from "lucide-react";
import type { FC } from "react"; import type { FC } from "react";
import { useCallback, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ComposioDriveFolderTree } from "@/components/connectors/composio-drive-folder-tree";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@ -23,7 +23,6 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { type PickerResult, useGooglePicker } from "@/hooks/use-google-picker";
interface ComposioDriveConfigProps { interface ComposioDriveConfigProps {
connector: SearchSourceConnector; connector: SearchSourceConnector;
@ -31,7 +30,7 @@ interface ComposioDriveConfigProps {
onNameChange?: (name: string) => void; onNameChange?: (name: string) => void;
} }
interface SelectedItem { interface SelectedFolder {
id: string; id: string;
name: string; name: string;
} }
@ -94,18 +93,20 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({
}) => { }) => {
const isIndexable = connector.config?.is_indexable as boolean; const isIndexable = connector.config?.is_indexable as boolean;
const existingFolders = (connector.config?.selected_folders as SelectedItem[] | undefined) || []; const existingFolders =
const existingFiles = (connector.config?.selected_files as SelectedItem[] | undefined) || []; (connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
const existingFiles = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
const existingIndexingOptions = const existingIndexingOptions =
(connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS; (connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS;
const [selectedFolders, setSelectedFolders] = useState<SelectedItem[]>(existingFolders); const [selectedFolders, setSelectedFolders] = useState<SelectedFolder[]>(existingFolders);
const [selectedFiles, setSelectedFiles] = useState<SelectedItem[]>(existingFiles); const [selectedFiles, setSelectedFiles] = useState<SelectedFolder[]>(existingFiles);
const [showFolderSelector, setShowFolderSelector] = useState(false);
const [indexingOptions, setIndexingOptions] = useState<IndexingOptions>(existingIndexingOptions); const [indexingOptions, setIndexingOptions] = useState<IndexingOptions>(existingIndexingOptions);
useEffect(() => { useEffect(() => {
const folders = (connector.config?.selected_folders as SelectedItem[] | undefined) || []; const folders = (connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
const files = (connector.config?.selected_files as SelectedItem[] | undefined) || []; const files = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
const options = const options =
(connector.config?.indexing_options as IndexingOptions | undefined) || (connector.config?.indexing_options as IndexingOptions | undefined) ||
DEFAULT_INDEXING_OPTIONS; DEFAULT_INDEXING_OPTIONS;
@ -115,8 +116,8 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({
}, [connector.config]); }, [connector.config]);
const updateConfig = ( const updateConfig = (
folders: SelectedItem[], folders: SelectedFolder[],
files: SelectedItem[], files: SelectedFolder[],
options: IndexingOptions options: IndexingOptions
) => { ) => {
if (onConfigChange) { if (onConfigChange) {
@ -129,26 +130,15 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({
} }
}; };
const handlePicked = useCallback( const handleSelectFolders = (folders: SelectedFolder[]) => {
(result: PickerResult) => { setSelectedFolders(folders);
const folders = result.folders.map((f) => ({ id: f.id, name: f.name })); updateConfig(folders, selectedFiles, indexingOptions);
const files = result.files.map((f) => ({ id: f.id, name: f.name })); };
setSelectedFolders(folders);
setSelectedFiles(files);
updateConfig(folders, files, indexingOptions);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[indexingOptions, connector.config]
);
const { const handleSelectFiles = (files: SelectedFolder[]) => {
openPicker, setSelectedFiles(files);
loading: pickerLoading, updateConfig(selectedFolders, files, indexingOptions);
error: pickerError, };
} = useGooglePicker({
connectorId: connector.id,
onPicked: handlePicked,
});
const handleIndexingOptionChange = (key: keyof IndexingOptions, value: number | boolean) => { const handleIndexingOptionChange = (key: keyof IndexingOptions, value: number | boolean) => {
const newOptions = { ...indexingOptions, [key]: value }; const newOptions = { ...indexingOptions, [key]: value };
@ -157,13 +147,13 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({
}; };
const handleRemoveFolder = (folderId: string) => { const handleRemoveFolder = (folderId: string) => {
const newFolders = selectedFolders.filter((f) => f.id !== folderId); const newFolders = selectedFolders.filter((folder) => folder.id !== folderId);
setSelectedFolders(newFolders); setSelectedFolders(newFolders);
updateConfig(newFolders, selectedFiles, indexingOptions); updateConfig(newFolders, selectedFiles, indexingOptions);
}; };
const handleRemoveFile = (fileId: string) => { const handleRemoveFile = (fileId: string) => {
const newFiles = selectedFiles.filter((f) => f.id !== fileId); const newFiles = selectedFiles.filter((file) => file.id !== fileId);
setSelectedFiles(newFiles); setSelectedFiles(newFiles);
updateConfig(selectedFolders, newFiles, indexingOptions); updateConfig(selectedFolders, newFiles, indexingOptions);
}; };
@ -242,18 +232,35 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({
</div> </div>
)} )}
<Button {showFolderSelector ? (
type="button" <div className="space-y-2 sm:space-y-3">
variant="outline" <ComposioDriveFolderTree
onClick={openPicker} connectorId={connector.id}
disabled={pickerLoading} selectedFolders={selectedFolders}
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9" onSelectFolders={handleSelectFolders}
> selectedFiles={selectedFiles}
{pickerLoading && <Loader2 className="size-3.5 mr-1.5 animate-spin" />} onSelectFiles={handleSelectFiles}
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"} />
</Button> <Button
type="button"
{pickerError && <p className="text-xs text-destructive">{pickerError}</p>} variant="outline"
size="sm"
onClick={() => setShowFolderSelector(false)}
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
>
Done Selecting
</Button>
</div>
) : (
<Button
type="button"
variant="outline"
onClick={() => setShowFolderSelector(true)}
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
>
{totalSelected > 0 ? "Change Selection" : "Select Folders & Files"}
</Button>
)}
</div> </div>
{/* Indexing Options */} {/* Indexing Options */}

View file

@ -2,7 +2,6 @@ import { EnumConnectorName } from "@/contracts/enums/connector";
// OAuth Connectors (Quick Connect) // OAuth Connectors (Quick Connect)
export const OAUTH_CONNECTORS = [ export const OAUTH_CONNECTORS = [
// // Uncomment for managed Google Connections
// { // {
// id: "google-drive-connector", // id: "google-drive-connector",
// title: "Google Drive", // title: "Google Drive",
@ -249,19 +248,84 @@ export interface AutoIndexConfig {
} }
export const AUTO_INDEX_DEFAULTS: Record<string, AutoIndexConfig> = { export const AUTO_INDEX_DEFAULTS: Record<string, AutoIndexConfig> = {
[EnumConnectorName.GOOGLE_GMAIL_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of emails." }, [EnumConnectorName.GOOGLE_GMAIL_CONNECTOR]: {
[EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of emails." }, daysBack: 30,
[EnumConnectorName.SLACK_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of messages." }, daysForward: 0,
[EnumConnectorName.DISCORD_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of messages." }, frequencyMinutes: 1440,
[EnumConnectorName.TEAMS_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of messages." }, syncDescription: "Syncing your last 30 days of emails.",
[EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR]: { daysBack: 90, daysForward: 90, frequencyMinutes: 1440, syncDescription: "Syncing 90 days of past and upcoming events." }, },
[EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: { daysBack: 90, daysForward: 90, frequencyMinutes: 1440, syncDescription: "Syncing 90 days of past and upcoming events." }, [EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: {
[EnumConnectorName.LINEAR_CONNECTOR]: { daysBack: 90, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 90 days of issues." }, daysBack: 30,
[EnumConnectorName.JIRA_CONNECTOR]: { daysBack: 90, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 90 days of issues." }, daysForward: 0,
[EnumConnectorName.CLICKUP_CONNECTOR]: { daysBack: 90, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 90 days of tasks." }, frequencyMinutes: 1440,
[EnumConnectorName.NOTION_CONNECTOR]: { daysBack: 365, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your pages." }, syncDescription: "Syncing your last 30 days of emails.",
[EnumConnectorName.CONFLUENCE_CONNECTOR]: { daysBack: 365, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your documentation." }, },
[EnumConnectorName.AIRTABLE_CONNECTOR]: { daysBack: 365, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your bases." }, [EnumConnectorName.SLACK_CONNECTOR]: {
daysBack: 30,
daysForward: 0,
frequencyMinutes: 1440,
syncDescription: "Syncing your last 30 days of messages.",
},
[EnumConnectorName.DISCORD_CONNECTOR]: {
daysBack: 30,
daysForward: 0,
frequencyMinutes: 1440,
syncDescription: "Syncing your last 30 days of messages.",
},
[EnumConnectorName.TEAMS_CONNECTOR]: {
daysBack: 30,
daysForward: 0,
frequencyMinutes: 1440,
syncDescription: "Syncing your last 30 days of messages.",
},
[EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR]: {
daysBack: 90,
daysForward: 90,
frequencyMinutes: 1440,
syncDescription: "Syncing 90 days of past and upcoming events.",
},
[EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: {
daysBack: 90,
daysForward: 90,
frequencyMinutes: 1440,
syncDescription: "Syncing 90 days of past and upcoming events.",
},
[EnumConnectorName.LINEAR_CONNECTOR]: {
daysBack: 90,
daysForward: 0,
frequencyMinutes: 1440,
syncDescription: "Syncing your last 90 days of issues.",
},
[EnumConnectorName.JIRA_CONNECTOR]: {
daysBack: 90,
daysForward: 0,
frequencyMinutes: 1440,
syncDescription: "Syncing your last 90 days of issues.",
},
[EnumConnectorName.CLICKUP_CONNECTOR]: {
daysBack: 90,
daysForward: 0,
frequencyMinutes: 1440,
syncDescription: "Syncing your last 90 days of tasks.",
},
[EnumConnectorName.NOTION_CONNECTOR]: {
daysBack: 365,
daysForward: 0,
frequencyMinutes: 1440,
syncDescription: "Syncing your pages.",
},
[EnumConnectorName.CONFLUENCE_CONNECTOR]: {
daysBack: 365,
daysForward: 0,
frequencyMinutes: 1440,
syncDescription: "Syncing your documentation.",
},
[EnumConnectorName.AIRTABLE_CONNECTOR]: {
daysBack: 365,
daysForward: 0,
frequencyMinutes: 1440,
syncDescription: "Syncing your bases.",
},
}; };
export const AUTO_INDEX_CONNECTOR_TYPES = new Set<string>(Object.keys(AUTO_INDEX_DEFAULTS)); export const AUTO_INDEX_CONNECTOR_TYPES = new Set<string>(Object.keys(AUTO_INDEX_DEFAULTS));

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { useCallback, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { connectorsApiService } from "@/lib/apis/connectors-api.service"; import { connectorsApiService } from "@/lib/apis/connectors-api.service";
export interface PickerItem { export interface PickerItem {
@ -21,6 +21,8 @@ interface UseGooglePickerOptions {
const PICKER_SCRIPT_URL = "https://apis.google.com/js/api.js"; const PICKER_SCRIPT_URL = "https://apis.google.com/js/api.js";
const FOLDER_MIME = "application/vnd.google-apps.folder"; const FOLDER_MIME = "application/vnd.google-apps.folder";
export const PICKER_OPEN_EVENT = "google-picker-open";
export const PICKER_CLOSE_EVENT = "google-picker-close";
let scriptLoadPromise: Promise<void> | null = null; let scriptLoadPromise: Promise<void> | null = null;
let pickerApiPromise: Promise<void> | null = null; let pickerApiPromise: Promise<void> | null = null;
@ -68,6 +70,25 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption
const onPickedRef = useRef(onPicked); const onPickedRef = useRef(onPicked);
onPickedRef.current = onPicked; onPickedRef.current = onPicked;
const openingRef = useRef(false); const openingRef = useRef(false);
const pickerRef = useRef<google.picker.Picker | null>(null);
const closePicker = useCallback(() => {
if (!pickerRef.current) return;
window.dispatchEvent(new Event(PICKER_CLOSE_EVENT));
pickerRef.current.dispose();
pickerRef.current = null;
openingRef.current = false;
}, []);
useEffect(() => {
const onEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && pickerRef.current) {
closePicker();
}
};
window.addEventListener("keydown", onEscape);
return () => window.removeEventListener("keydown", onEscape);
}, [closePicker]);
const openPicker = useCallback(async () => { const openPicker = useCallback(async () => {
if (openingRef.current) return; if (openingRef.current) return;
@ -87,15 +108,18 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption
.setIncludeFolders(true) .setIncludeFolders(true)
.setSelectFolderEnabled(true); .setSelectFolderEnabled(true);
let pickerInstance: google.picker.Picker | null = null; const builder = new google.picker.PickerBuilder()
const picker = new google.picker.PickerBuilder()
.addView(docsView) .addView(docsView)
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED) .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
.setOAuthToken(access_token) .setOAuthToken(access_token)
.setDeveloperKey(picker_api_key)
.setOrigin(window.location.protocol + "//" + window.location.host) .setOrigin(window.location.protocol + "//" + window.location.host)
.setTitle("Select files and folders to index") .setTitle("Select files and folders to index");
if (picker_api_key) {
builder.setDeveloperKey(picker_api_key);
}
const picker = builder
.setCallback((data: google.picker.ResponseObject) => { .setCallback((data: google.picker.ResponseObject) => {
const action = data[google.picker.Response.ACTION]; const action = data[google.picker.Response.ACTION];
@ -128,16 +152,16 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption
action === google.picker.Action.CANCEL || action === google.picker.Action.CANCEL ||
action === google.picker.Action.ERROR action === google.picker.Action.ERROR
) { ) {
pickerInstance?.dispose(); closePicker();
pickerInstance = null;
openingRef.current = false;
} }
}) })
.build(); .build();
pickerInstance = picker; pickerRef.current = picker;
window.dispatchEvent(new Event(PICKER_OPEN_EVENT));
picker.setVisible(true); picker.setVisible(true);
} catch (err) { } catch (err) {
window.dispatchEvent(new Event(PICKER_CLOSE_EVENT));
openingRef.current = false; openingRef.current = false;
const msg = err instanceof Error ? err.message : "Failed to open Google Picker"; const msg = err instanceof Error ? err.message : "Failed to open Google Picker";
setError(msg); setError(msg);
@ -145,7 +169,7 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [connectorId]); }, [connectorId, closePicker]);
return { openPicker, loading, error }; return { openPicker, closePicker, loading, error };
} }

View file

@ -273,7 +273,7 @@ class ConnectorsApiService {
return baseApiService.get<{ return baseApiService.get<{
access_token: string; access_token: string;
client_id: string; client_id: string;
picker_api_key: string; picker_api_key: string | null;
}>(`/api/v1/connectors/${connectorId}/drive-picker-token`); }>(`/api/v1/connectors/${connectorId}/drive-picker-token`);
}; };