feat: add file selection to Google Drive connector

- Add structured request body with folders and files arrays
- Support individual file indexing alongside folder indexing
- Remove deprecated folder_ids/folder_names query params
- Update UI to allow selecting both folders and files
This commit is contained in:
CREDO23 2025-12-31 14:15:07 +02:00
parent 476c764611
commit 9c78726b6b
12 changed files with 366 additions and 97 deletions

View file

@ -119,9 +119,10 @@ export default function ConnectorsPage() {
const [customFrequency, setCustomFrequency] = useState<string>("");
const [isSavingPeriodic, setIsSavingPeriodic] = useState(false);
// Google Drive folder selection state
// Google Drive folder and file selection state
const [driveFolderDialogOpen, setDriveFolderDialogOpen] = useState(false);
const [selectedFolders, setSelectedFolders] = useState<Array<{ id: string; name: string }>>([]);
const [selectedFiles, setSelectedFiles] = useState<Array<{ id: string; name: string }>>([]);
useEffect(() => {
if (error) {
@ -162,10 +163,10 @@ export default function ConnectorsPage() {
setDriveFolderDialogOpen(true);
};
// Handle Google Drive folder indexing
const handleIndexDriveFolder = async () => {
if (selectedConnectorForIndexing === null || selectedFolders.length === 0) {
toast.error("Please select at least one folder");
// Handle Google Drive folder and file indexing
const handleIndexGoogleDrive = async () => {
if (selectedConnectorForIndexing === null || (selectedFolders.length === 0 && selectedFiles.length === 0)) {
toast.error("Please select at least one folder or file");
return;
}
@ -174,15 +175,14 @@ export default function ConnectorsPage() {
try {
setIndexingConnectorId(selectedConnectorForIndexing);
const folderIds = selectedFolders.map((f) => f.id).join(",");
const folderNames = selectedFolders.map((f) => f.name).join(", ");
await indexConnector({
connector_id: selectedConnectorForIndexing,
body: {
folders: selectedFolders,
files: selectedFiles,
},
queryParams: {
search_space_id: searchSpaceId,
folder_ids: folderIds,
folder_names: folderNames,
},
});
toast.success(t("indexing_started"));
@ -190,10 +190,11 @@ export default function ConnectorsPage() {
console.error("Error indexing connector content:", error);
toast.error(error instanceof Error ? error.message : t("indexing_failed"));
} finally {
setIndexingConnectorId(null);
setSelectedConnectorForIndexing(null);
setSelectedFolders([]);
}
setIndexingConnectorId(null);
setSelectedConnectorForIndexing(null);
setSelectedFolders([]);
setSelectedFiles([]);
}
};
// Handle connector indexing with dates
@ -679,11 +680,11 @@ export default function ConnectorsPage() {
<Dialog open={driveFolderDialogOpen} onOpenChange={setDriveFolderDialogOpen}>
<DialogContent className="w-auto max-w-full">
<DialogHeader>
<DialogTitle>Select Google Drive Folders</DialogTitle>
<DialogTitle>Select Google Drive Folders & Files</DialogTitle>
<DialogDescription className="flex items-start gap-2 text-sm p-2 border mt-1 rounded ">
<Info className="h-4 w-4 shrink-0 text-blue-500" />
<span>
Select folders to index. Only files <strong>directly in each folder</strong> will be
Select folders and/or individual files to index. For folders, only files <strong>directly in each folder</strong> will be
processedsubfolders must be selected separately.
</span>
</DialogDescription>
@ -698,23 +699,43 @@ export default function ConnectorsPage() {
onSelectFolders={(folders) => {
setSelectedFolders(folders);
}}
selectedFiles={selectedFiles}
onSelectFiles={(files) => {
setSelectedFiles(files);
}}
/>
)}
</div>
{selectedFolders.length > 0 && (
{(selectedFolders.length > 0 || selectedFiles.length > 0) && (
<div className="p-3 bg-muted rounded-lg text-sm space-y-2">
<div>
<p className="font-medium mb-1">
Selected {selectedFolders.length} folder{selectedFolders.length > 1 ? "s" : ""}:
</p>
<div className="max-h-24 overflow-y-auto">
{selectedFolders.map((folder) => (
<p key={folder.id} className="text-sm text-muted-foreground truncate" title={folder.name}>
{folder.name}
</p>
))}
{selectedFolders.length > 0 && (
<div>
<p className="font-medium mb-1">
Selected {selectedFolders.length} folder{selectedFolders.length > 1 ? "s" : ""}:
</p>
<div className="max-h-24 overflow-y-auto">
{selectedFolders.map((folder) => (
<p key={folder.id} className="text-sm text-muted-foreground truncate" title={folder.name}>
📁 {folder.name}
</p>
))}
</div>
</div>
</div>
)}
{selectedFiles.length > 0 && (
<div>
<p className="font-medium mb-1">
Selected {selectedFiles.length} file{selectedFiles.length > 1 ? "s" : ""}:
</p>
<div className="max-h-24 overflow-y-auto">
{selectedFiles.map((file) => (
<p key={file.id} className="text-sm text-muted-foreground truncate" title={file.name}>
📄 {file.name}
</p>
))}
</div>
</div>
)}
</div>
)}
</div>
@ -725,11 +746,12 @@ export default function ConnectorsPage() {
setDriveFolderDialogOpen(false);
setSelectedConnectorForIndexing(null);
setSelectedFolders([]);
setSelectedFiles([]);
}}
>
{tCommon("cancel")}
</Button>
<Button onClick={handleIndexDriveFolder} disabled={selectedFolders.length === 0}>
<Button onClick={handleIndexGoogleDrive} disabled={selectedFolders.length === 0 && selectedFiles.length === 0}>
{t("start_indexing")}
</Button>
</DialogFooter>

View file

@ -47,6 +47,8 @@ interface GoogleDriveFolderTreeProps {
connectorId: number;
selectedFolders: SelectedFolder[];
onSelectFolders: (folders: SelectedFolder[]) => void;
selectedFiles?: SelectedFolder[];
onSelectFiles?: (files: SelectedFolder[]) => void;
}
// Helper to get appropriate icon for file type
@ -70,6 +72,8 @@ export function GoogleDriveFolderTree({
connectorId,
selectedFolders,
onSelectFolders,
selectedFiles = [],
onSelectFiles = () => {},
}: GoogleDriveFolderTreeProps) {
const [itemStates, setItemStates] = useState<Map<string, ItemTreeNode>>(new Map());
@ -83,6 +87,10 @@ export function GoogleDriveFolderTree({
return selectedFolders.some((f) => f.id === folderId);
};
const isFileSelected = (fileId: string): boolean => {
return selectedFiles.some((f) => f.id === fileId);
};
const toggleFolderSelection = (folderId: string, folderName: string) => {
if (isFolderSelected(folderId)) {
onSelectFolders(selectedFolders.filter((f) => f.id !== folderId));
@ -91,6 +99,14 @@ export function GoogleDriveFolderTree({
}
};
const toggleFileSelection = (fileId: string, fileName: string) => {
if (isFileSelected(fileId)) {
onSelectFiles(selectedFiles.filter((f) => f.id !== fileId));
} else {
onSelectFiles([...selectedFiles, { id: fileId, name: fileName }]);
}
};
/**
* Find an item by ID across all loaded items (root and nested).
*/
@ -201,8 +217,8 @@ export function GoogleDriveFolderTree({
const isExpanded = state?.isExpanded || false;
const isLoading = state?.isLoading || false;
const children = state?.children;
const isSelected = isFolderSelected(item.id);
const isFolder = item.isFolder;
const isSelected = isFolder ? isFolderSelected(item.id) : isFileSelected(item.id);
const childFolders = children?.filter((c) => c.isFolder) || [];
const childFiles = children?.filter((c) => !c.isFolder) || [];
@ -211,10 +227,8 @@ export function GoogleDriveFolderTree({
<div key={item.id} className="w-full" style={{ marginLeft: `${level * 1.25}rem` }}>
<div
className={cn(
"flex items-center gap-2 h-auto py-2 px-2 rounded-md",
isFolder && "hover:bg-accent cursor-pointer",
!isFolder && "cursor-default opacity-60",
isSelected && isFolder && "bg-accent/50"
"flex items-center group gap-2 h-auto py-2 px-2 rounded-md hover:bg-accent cursor-pointer",
isSelected && "bg-accent/50"
)}
>
{isFolder ? (
@ -237,16 +251,20 @@ export function GoogleDriveFolderTree({
<span className="w-4 h-4 shrink-0" />
)}
{isFolder && (
<Checkbox
checked={isSelected}
onCheckedChange={() => toggleFolderSelection(item.id, item.name)}
className="shrink-0"
onClick={(e) => e.stopPropagation()}
/>
)}
<Checkbox
checked={isSelected}
onCheckedChange={() => {
if (isFolder) {
toggleFolderSelection(item.id, item.name);
} else {
toggleFileSelection(item.id, item.name);
}
}}
className="shrink-0 z-20 group-hover:border-white group-hover:border"
onClick={(e) => e.stopPropagation()}
/>
<div className="shrink-0" style={{ marginLeft: isFolder ? "0" : "1.25rem" }}>
<div className="shrink-0">
{isFolder ? (
isExpanded ? (
<FolderOpen className="h-4 w-4 text-blue-500" />

View file

@ -126,6 +126,24 @@ export const deleteConnectorResponse = z.object({
message: z.literal("Search source connector deleted successfully"),
});
/**
* Google Drive index request body
*/
export const googleDriveIndexBody = z.object({
folders: z.array(
z.object({
id: z.string(),
name: z.string(),
})
),
files: z.array(
z.object({
id: z.string(),
name: z.string(),
})
),
});
/**
* Index connector
*/
@ -135,10 +153,8 @@ export const indexConnectorRequest = z.object({
search_space_id: z.number().or(z.string()),
start_date: z.string().optional(),
end_date: z.string().optional(),
// Google Drive only
folder_ids: z.string().optional(),
folder_names: z.string().optional(),
}),
body: googleDriveIndexBody.optional(),
});
export const indexConnectorResponse = z.object({

View file

@ -267,9 +267,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
connectorId: number,
searchSpaceId: string | number,
startDate?: string,
endDate?: string,
folderIds?: string,
folderNames?: string
endDate?: string
) => {
try {
// Build query parameters
@ -282,12 +280,6 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
if (endDate) {
params.append("end_date", endDate);
}
if (folderIds) {
params.append("folder_ids", folderIds);
}
if (folderNames) {
params.append("folder_names", folderNames);
}
const response = await authenticatedFetch(
`${

View file

@ -164,7 +164,7 @@ class ConnectorsApiService {
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { connector_id, queryParams } = parsedRequest.data;
const { connector_id, queryParams, body } = parsedRequest.data;
// Transform query params to be string values
const transformedQueryParams = Object.fromEntries(
@ -177,7 +177,10 @@ class ConnectorsApiService {
return baseApiService.post(
`/api/v1/search-source-connectors/${connector_id}/index?${queryString}`,
indexConnectorResponse
indexConnectorResponse,
{
body: body || {},
}
);
};