From 3bda6c167952e28018623b9262712dc0ec906f76 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 10 Mar 2026 23:06:33 +0200 Subject: [PATCH] revert Composio Drive to folder tree, harden Picker for native Drive --- .../assistant-ui/connector-popup.tsx | 755 +++++++++--------- .../components/composio-drive-config.tsx | 97 +-- .../constants/connector-constants.ts | 92 ++- surfsense_web/hooks/use-google-picker.ts | 48 +- .../lib/apis/connectors-api.service.ts | 2 +- 5 files changed, 558 insertions(+), 436 deletions(-) diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 489513bff..cf1235c65 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -4,8 +4,9 @@ import { useAtomValue } from "jotai"; import { AlertTriangle, Cable, Settings } from "lucide-react"; import Link from "next/link"; 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 { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom"; import { globalNewLLMConfigsAtom, llmPreferencesAtom, @@ -19,8 +20,8 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; -import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom"; 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 { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header"; import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view"; @@ -47,400 +48,426 @@ interface ConnectorIndicatorProps { export const ConnectorIndicator = forwardRef( ({ showTrigger = true }, ref) => { - const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); - const searchParams = useSearchParams(); - const { data: currentUser } = useAtomValue(currentUserAtom); - const { data: preferences = {}, isFetching: preferencesLoading } = - useAtomValue(llmPreferencesAtom); - const { data: globalConfigs = [], isFetching: globalConfigsLoading } = - useAtomValue(globalNewLLMConfigsAtom); + const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); + const searchParams = useSearchParams(); + const { data: currentUser } = useAtomValue(currentUserAtom); + const { data: preferences = {}, isFetching: preferencesLoading } = + useAtomValue(llmPreferencesAtom); + const { data: globalConfigs = [], isFetching: globalConfigsLoading } = + useAtomValue(globalNewLLMConfigsAtom); - // Check if document summary LLM is properly configured - // - 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 null/undefined, it's not configured - const docSummaryLlmId = preferences.document_summary_llm_id; - const isAutoMode = docSummaryLlmId === 0; - const hasGlobalConfigs = globalConfigs.length > 0; + // Check if document summary LLM is properly configured + // - 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 null/undefined, it's not configured + const docSummaryLlmId = preferences.document_summary_llm_id; + const isAutoMode = docSummaryLlmId === 0; + const hasGlobalConfigs = globalConfigs.length > 0; - const hasDocumentSummaryLLM = - docSummaryLlmId !== null && - docSummaryLlmId !== undefined && - // If it's Auto mode, we need global configs to actually be available - (!isAutoMode || hasGlobalConfigs); + const hasDocumentSummaryLLM = + docSummaryLlmId !== null && + docSummaryLlmId !== undefined && + // If it's Auto mode, we need global configs to actually be available + (!isAutoMode || hasGlobalConfigs); - const llmConfigLoading = preferencesLoading || globalConfigsLoading; + const llmConfigLoading = preferencesLoading || globalConfigsLoading; - // Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min) - const { data: documentTypeCounts, isFetching: documentTypesLoading } = - useAtomValue(documentTypeCountsAtom); + // Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min) + const { data: documentTypeCounts, isFetching: documentTypesLoading } = + useAtomValue(documentTypeCountsAtom); - // Read status inbox items from shared atom (populated by LayoutDataProvider) - // instead of creating a duplicate useInbox("status") hook. - const statusInboxItems = useAtomValue(statusInboxItemsAtom); - const inboxItems = useMemo( - () => statusInboxItems.filter((item) => item.type === "connector_indexing"), - [statusInboxItems] - ); + // Read status inbox items from shared atom (populated by LayoutDataProvider) + // instead of creating a duplicate useInbox("status") hook. + const statusInboxItems = useAtomValue(statusInboxItemsAtom); + const inboxItems = useMemo( + () => statusInboxItems.filter((item) => item.type === "connector_indexing"), + [statusInboxItems] + ); - // Check if YouTube view is active - const isYouTubeView = searchParams.get("view") === "youtube"; + // Check if YouTube view is active + const isYouTubeView = searchParams.get("view") === "youtube"; - // Use the custom hook for dialog state management - const { - isOpen, - activeTab, - connectingId, - isScrolled, - searchQuery, - indexingConfig, - indexingConnector, - indexingConnectorConfig, - editingConnector, - connectingConnectorType, - isCreatingConnector, - startDate, - endDate, - isStartingIndexing, - isSaving, - isDisconnecting, - periodicEnabled, - frequencyMinutes, - enableSummary, - allConnectors, - viewingAccountsType, - viewingMCPList, - setSearchQuery, - setStartDate, - setEndDate, - setPeriodicEnabled, - setFrequencyMinutes, - setEnableSummary, - handleOpenChange, - handleTabChange, - handleScroll, - handleConnectOAuth, - handleConnectNonOAuth, - handleCreateWebcrawler, - handleCreateYouTubeCrawler, - handleSubmitConnectForm, - handleStartIndexing, - handleSkipIndexing, - handleStartEdit, - handleSaveConnector, - handleDisconnectConnector, - handleBackFromEdit, - handleBackFromConnect, - handleBackFromYouTube, - handleViewAccountsList, - handleBackFromAccountsList, - handleBackFromMCPList, - handleAddNewMCPFromList, - handleQuickIndexConnector, - connectorConfig, - setConnectorConfig, - setIndexingConnectorConfig, - setConnectorName, - } = useConnectorDialog(); + // Use the custom hook for dialog state management + const { + isOpen, + activeTab, + connectingId, + isScrolled, + searchQuery, + indexingConfig, + indexingConnector, + indexingConnectorConfig, + editingConnector, + connectingConnectorType, + isCreatingConnector, + startDate, + endDate, + isStartingIndexing, + isSaving, + isDisconnecting, + periodicEnabled, + frequencyMinutes, + enableSummary, + allConnectors, + viewingAccountsType, + viewingMCPList, + setSearchQuery, + setStartDate, + setEndDate, + setPeriodicEnabled, + setFrequencyMinutes, + setEnableSummary, + handleOpenChange, + handleTabChange, + handleScroll, + handleConnectOAuth, + handleConnectNonOAuth, + handleCreateWebcrawler, + handleCreateYouTubeCrawler, + handleSubmitConnectForm, + handleStartIndexing, + handleSkipIndexing, + handleStartEdit, + handleSaveConnector, + handleDisconnectConnector, + handleBackFromEdit, + handleBackFromConnect, + handleBackFromYouTube, + handleViewAccountsList, + handleBackFromAccountsList, + handleBackFromMCPList, + handleAddNewMCPFromList, + handleQuickIndexConnector, + connectorConfig, + setConnectorConfig, + setIndexingConnectorConfig, + setConnectorName, + } = useConnectorDialog(); - // Fetch connectors using Electric SQL + PGlite for real-time updates - // This provides instant updates when connectors change, without polling - const { - connectors: connectorsFromElectric = [], - loading: connectorsLoading, - error: connectorsError, - refreshConnectors: refreshConnectorsElectric, - } = useConnectorsElectric(searchSpaceId); + const [pickerOpen, setPickerOpen] = useState(false); + useEffect(() => { + const onOpen = () => setPickerOpen(true); + const onClose = () => setPickerOpen(false); + window.addEventListener(PICKER_OPEN_EVENT, onOpen); + window.addEventListener(PICKER_CLOSE_EVENT, onClose); + return () => { + window.removeEventListener(PICKER_OPEN_EVENT, onOpen); + window.removeEventListener(PICKER_CLOSE_EVENT, onClose); + }; + }, []); - // Fallback to API if Electric is not available or fails - // Use Electric data if: 1) we have data, or 2) still loading without error - // Use API data if: Electric failed (has error) or finished loading with no data - const useElectricData = - connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError); - const connectors = useElectricData ? connectorsFromElectric : allConnectors || []; + // Fetch connectors using Electric SQL + PGlite for real-time updates + // This provides instant updates when connectors change, without polling + const { + connectors: connectorsFromElectric = [], + loading: connectorsLoading, + error: connectorsError, + refreshConnectors: refreshConnectorsElectric, + } = useConnectorsElectric(searchSpaceId); - // Manual refresh function that works with both Electric and API - const refreshConnectors = async () => { - if (useElectricData) { - await refreshConnectorsElectric(); - } else { - // Fallback: use allConnectors from useConnectorDialog (which uses connectorsAtom) - // The connectorsAtom will handle refetching if needed - } - }; + // Fallback to API if Electric is not available or fails + // Use Electric data if: 1) we have data, or 2) still loading without error + // Use API data if: Electric failed (has error) or finished loading with no data + const useElectricData = + connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError); + const connectors = useElectricData ? connectorsFromElectric : allConnectors || []; - // 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 - ); + // Manual refresh function that works with both Electric and API + const refreshConnectors = async () => { + if (useElectricData) { + await refreshConnectorsElectric(); + } 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 activeDocumentTypes = documentTypeCounts - ? Object.entries(documentTypeCounts).filter(([, count]) => count > 0) - : []; + const isLoading = connectorsLoading || documentTypesLoading; - const hasConnectors = connectors.length > 0; - const hasSources = hasConnectors || activeDocumentTypes.length > 0; - const totalSourceCount = connectors.length + activeDocumentTypes.length; + // Get document types that have documents in the search space + const activeDocumentTypes = documentTypeCounts + ? 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 - // Using Electric SQL + PGlite for real-time connector updates - const connectedTypes = new Set( - (connectors || []).map((c: SearchSourceConnector) => c.connector_type) - ); + const activeConnectorsCount = connectors.length; - useImperativeHandle(ref, () => ({ - open: () => handleOpenChange(true), - })); + // Check which connectors are already connected + // Using Electric SQL + PGlite for real-time connector updates + const connectedTypes = new Set( + (connectors || []).map((c: SearchSourceConnector) => c.connector_type) + ); - if (!searchSpaceId) return null; + useImperativeHandle(ref, () => ({ + open: () => handleOpenChange(true), + })); - return ( - - {showTrigger && ( - handleOpenChange(true)} - > - {isLoading ? ( - - ) : ( - <> - - {activeConnectorsCount > 0 && ( - - {activeConnectorsCount > 99 ? "99+" : activeConnectorsCount} - - )} - - )} - - )} + if (!searchSpaceId) return null; - - Manage Connectors - {/* YouTube Crawler View - shown when adding YouTube videos */} - {isYouTubeView && searchSpaceId ? ( - - ) : viewingMCPList ? ( - - ) : viewingAccountsType ? ( - { - // 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 ? ( - handleSubmitConnectForm(formData, startIndexing)} - onBack={handleBackFromConnect} - isSubmitting={isCreatingConnector} - /> - ) : editingConnector ? ( - 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 + return ( + { + if (!open && pickerOpen) return; + handleOpenChange(open); + }} + modal={!pickerOpen} + > + {showTrigger && ( + - ) : indexingConfig ? ( - { - if (indexingConfig.connectorId) { - startIndexing(indexingConfig.connectorId); - } - handleStartIndexing(() => refreshConnectors()); - }} - onSkip={handleSkipIndexing} - /> - ) : ( - handleOpenChange(true)} > - {/* Header */} - + ) : ( + <> + + {activeConnectorsCount > 0 && ( + + {activeConnectorsCount > 99 ? "99+" : activeConnectorsCount} + + )} + + )} + + )} + + + Manage Connectors + {/* YouTube Crawler View - shown when adding YouTube videos */} + {isYouTubeView && searchSpaceId ? ( + + ) : viewingMCPList ? ( + + ) : viewingAccountsType ? ( + { + // 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 ? ( + handleSubmitConnectForm(formData, startIndexing)} + onBack={handleBackFromConnect} + isSubmitting={isCreatingConnector} + /> + ) : editingConnector ? ( + 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 ? ( + { + if (indexingConfig.connectorId) { + startIndexing(indexingConfig.connectorId); + } + handleStartIndexing(() => refreshConnectors()); + }} + onSkip={handleSkipIndexing} + /> + ) : ( + + {/* Header */} + - {/* Content */} -
-
-
- {/* LLM Configuration Warning */} - {!llmConfigLoading && !hasDocumentSummaryLLM && ( - - - LLM Configuration Required - -

- {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." - : "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."} -

- -
-
- )} + {/* Content */} +
+
+
+ {/* LLM Configuration Warning */} + {!llmConfigLoading && !hasDocumentSummaryLLM && ( + + + LLM Configuration Required + +

+ {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." + : "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."} +

+ +
+
+ )} - - + {}} + onConnectNonOAuth={hasDocumentSummaryLLM ? handleConnectNonOAuth : () => {}} + onCreateWebcrawler={ + hasDocumentSummaryLLM ? handleCreateWebcrawler : () => {} + } + onCreateYouTubeCrawler={ + hasDocumentSummaryLLM ? handleCreateYouTubeCrawler : () => {} + } + onManage={handleStartEdit} + onViewAccountsList={handleViewAccountsList} + /> + + + {}} - onConnectNonOAuth={hasDocumentSummaryLLM ? handleConnectNonOAuth : () => {}} - onCreateWebcrawler={hasDocumentSummaryLLM ? handleCreateWebcrawler : () => {}} - onCreateYouTubeCrawler={ - hasDocumentSummaryLLM ? handleCreateYouTubeCrawler : () => {} - } + onTabChange={handleTabChange} onManage={handleStartEdit} onViewAccountsList={handleViewAccountsList} /> - - - +
+ {/* Bottom fade shadow */} +
- {/* Bottom fade shadow */} -
-
- - )} - -
- ); -}); + + )} +
+
+ ); + } +); ConnectorIndicator.displayName = "ConnectorIndicator"; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/composio-drive-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/composio-drive-config.tsx index c8aea8721..66ea22e92 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/composio-drive-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/composio-drive-config.tsx @@ -6,12 +6,12 @@ import { FileText, FolderClosed, Image, - Loader2, Presentation, X, } from "lucide-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 { Label } from "@/components/ui/label"; import { @@ -23,7 +23,6 @@ import { } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; -import { type PickerResult, useGooglePicker } from "@/hooks/use-google-picker"; interface ComposioDriveConfigProps { connector: SearchSourceConnector; @@ -31,7 +30,7 @@ interface ComposioDriveConfigProps { onNameChange?: (name: string) => void; } -interface SelectedItem { +interface SelectedFolder { id: string; name: string; } @@ -94,18 +93,20 @@ export const ComposioDriveConfig: FC = ({ }) => { const isIndexable = connector.config?.is_indexable as boolean; - const existingFolders = (connector.config?.selected_folders as SelectedItem[] | undefined) || []; - const existingFiles = (connector.config?.selected_files as SelectedItem[] | undefined) || []; + const existingFolders = + (connector.config?.selected_folders as SelectedFolder[] | undefined) || []; + const existingFiles = (connector.config?.selected_files as SelectedFolder[] | undefined) || []; const existingIndexingOptions = (connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS; - const [selectedFolders, setSelectedFolders] = useState(existingFolders); - const [selectedFiles, setSelectedFiles] = useState(existingFiles); + const [selectedFolders, setSelectedFolders] = useState(existingFolders); + const [selectedFiles, setSelectedFiles] = useState(existingFiles); + const [showFolderSelector, setShowFolderSelector] = useState(false); const [indexingOptions, setIndexingOptions] = useState(existingIndexingOptions); useEffect(() => { - const folders = (connector.config?.selected_folders as SelectedItem[] | undefined) || []; - const files = (connector.config?.selected_files as SelectedItem[] | undefined) || []; + const folders = (connector.config?.selected_folders as SelectedFolder[] | undefined) || []; + const files = (connector.config?.selected_files as SelectedFolder[] | undefined) || []; const options = (connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS; @@ -115,8 +116,8 @@ export const ComposioDriveConfig: FC = ({ }, [connector.config]); const updateConfig = ( - folders: SelectedItem[], - files: SelectedItem[], + folders: SelectedFolder[], + files: SelectedFolder[], options: IndexingOptions ) => { if (onConfigChange) { @@ -129,26 +130,15 @@ export const ComposioDriveConfig: FC = ({ } }; - const handlePicked = useCallback( - (result: PickerResult) => { - const folders = result.folders.map((f) => ({ id: f.id, name: f.name })); - 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 handleSelectFolders = (folders: SelectedFolder[]) => { + setSelectedFolders(folders); + updateConfig(folders, selectedFiles, indexingOptions); + }; - const { - openPicker, - loading: pickerLoading, - error: pickerError, - } = useGooglePicker({ - connectorId: connector.id, - onPicked: handlePicked, - }); + const handleSelectFiles = (files: SelectedFolder[]) => { + setSelectedFiles(files); + updateConfig(selectedFolders, files, indexingOptions); + }; const handleIndexingOptionChange = (key: keyof IndexingOptions, value: number | boolean) => { const newOptions = { ...indexingOptions, [key]: value }; @@ -157,13 +147,13 @@ export const ComposioDriveConfig: FC = ({ }; const handleRemoveFolder = (folderId: string) => { - const newFolders = selectedFolders.filter((f) => f.id !== folderId); + const newFolders = selectedFolders.filter((folder) => folder.id !== folderId); setSelectedFolders(newFolders); updateConfig(newFolders, selectedFiles, indexingOptions); }; const handleRemoveFile = (fileId: string) => { - const newFiles = selectedFiles.filter((f) => f.id !== fileId); + const newFiles = selectedFiles.filter((file) => file.id !== fileId); setSelectedFiles(newFiles); updateConfig(selectedFolders, newFiles, indexingOptions); }; @@ -242,18 +232,35 @@ export const ComposioDriveConfig: FC = ({ )} - - - {pickerError &&

{pickerError}

} + {showFolderSelector ? ( +
+ + +
+ ) : ( + + )} {/* Indexing Options */} diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index 137a9480e..9889708d7 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -2,7 +2,6 @@ import { EnumConnectorName } from "@/contracts/enums/connector"; // OAuth Connectors (Quick Connect) export const OAUTH_CONNECTORS = [ - // // Uncomment for managed Google Connections // { // id: "google-drive-connector", // title: "Google Drive", @@ -249,19 +248,84 @@ export interface AutoIndexConfig { } export const AUTO_INDEX_DEFAULTS: Record = { - [EnumConnectorName.GOOGLE_GMAIL_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of emails." }, - [EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of emails." }, - [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." }, + [EnumConnectorName.GOOGLE_GMAIL_CONNECTOR]: { + daysBack: 30, + daysForward: 0, + frequencyMinutes: 1440, + syncDescription: "Syncing your last 30 days of emails.", + }, + [EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: { + daysBack: 30, + daysForward: 0, + frequencyMinutes: 1440, + syncDescription: "Syncing your last 30 days of emails.", + }, + [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(Object.keys(AUTO_INDEX_DEFAULTS)); diff --git a/surfsense_web/hooks/use-google-picker.ts b/surfsense_web/hooks/use-google-picker.ts index 45e696235..6aa019b0f 100644 --- a/surfsense_web/hooks/use-google-picker.ts +++ b/surfsense_web/hooks/use-google-picker.ts @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { connectorsApiService } from "@/lib/apis/connectors-api.service"; export interface PickerItem { @@ -21,6 +21,8 @@ interface UseGooglePickerOptions { const PICKER_SCRIPT_URL = "https://apis.google.com/js/api.js"; 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 | null = null; let pickerApiPromise: Promise | null = null; @@ -68,6 +70,25 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption const onPickedRef = useRef(onPicked); onPickedRef.current = onPicked; const openingRef = useRef(false); + const pickerRef = useRef(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 () => { if (openingRef.current) return; @@ -87,15 +108,18 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption .setIncludeFolders(true) .setSelectFolderEnabled(true); - let pickerInstance: google.picker.Picker | null = null; - - const picker = new google.picker.PickerBuilder() + const builder = new google.picker.PickerBuilder() .addView(docsView) .enableFeature(google.picker.Feature.MULTISELECT_ENABLED) .setOAuthToken(access_token) - .setDeveloperKey(picker_api_key) .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) => { 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.ERROR ) { - pickerInstance?.dispose(); - pickerInstance = null; - openingRef.current = false; + closePicker(); } }) .build(); - pickerInstance = picker; + pickerRef.current = picker; + window.dispatchEvent(new Event(PICKER_OPEN_EVENT)); picker.setVisible(true); } catch (err) { + window.dispatchEvent(new Event(PICKER_CLOSE_EVENT)); openingRef.current = false; const msg = err instanceof Error ? err.message : "Failed to open Google Picker"; setError(msg); @@ -145,7 +169,7 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption } finally { setLoading(false); } - }, [connectorId]); + }, [connectorId, closePicker]); - return { openPicker, loading, error }; + return { openPicker, closePicker, loading, error }; } diff --git a/surfsense_web/lib/apis/connectors-api.service.ts b/surfsense_web/lib/apis/connectors-api.service.ts index ba607ccc1..fafe1a8fa 100644 --- a/surfsense_web/lib/apis/connectors-api.service.ts +++ b/surfsense_web/lib/apis/connectors-api.service.ts @@ -273,7 +273,7 @@ class ConnectorsApiService { return baseApiService.get<{ access_token: string; client_id: string; - picker_api_key: string; + picker_api_key: string | null; }>(`/api/v1/connectors/${connectorId}/drive-picker-token`); };