mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-28 21:49:40 +02:00
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:
parent
476c764611
commit
9c78726b6b
12 changed files with 366 additions and 97 deletions
|
|
@ -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
|
||||
processed—subfolders 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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
`${
|
||||
|
|
|
|||
|
|
@ -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 || {},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue