refactor(web): use React Query for Google Drive folder operations

- Fix errors in connectors-api.service (use .issues instead of .errors)
- Create useGoogleDriveFolders hook with proper React Query integration
- Add Google Drive folders cache keys with proper query invalidation
- Refactor GoogleDriveFolderTree to use React Query hook for root data
- Remove manual state management (isInitialized, setRootItems, loadRootItems)
- Remove unused state (driveFolders, isLoadingFolders) from manage page
- Simplify handleOpenDriveFolderDialog function
- Automatic loading, caching, error handling, and refetching via React Query
- Better performance with proper caching and state management
This commit is contained in:
CREDO23 2025-12-28 19:17:37 +02:00
parent c5c61a2c6b
commit 10c98745cd
6 changed files with 129 additions and 93 deletions

View file

@ -70,14 +70,8 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import { cn } from "@/lib/utils";
import { authenticatedFetch } from "@/lib/auth-utils";
import { GoogleDriveFolderTree } from "@/components/connectors/google-drive-folder-tree";
interface DriveFolder {
id: string;
name: string;
}
export default function ConnectorsPage() {
const t = useTranslations("connectors");
const tCommon = useTranslations("common");
@ -127,9 +121,7 @@ export default function ConnectorsPage() {
// Google Drive folder selection state
const [driveFolderDialogOpen, setDriveFolderDialogOpen] = useState(false);
const [driveFolders, setDriveFolders] = useState<DriveFolder[]>([]);
const [selectedFolders, setSelectedFolders] = useState<Array<{ id: string; name: string }>>([]);
const [isLoadingFolders, setIsLoadingFolders] = useState(false);
useEffect(() => {
if (error) {
@ -165,31 +157,9 @@ export default function ConnectorsPage() {
}
};
// Handle opening Google Drive folder selection dialog
const handleOpenDriveFolderDialog = async (connectorId: number) => {
const handleOpenDriveFolderDialog = (connectorId: number) => {
setSelectedConnectorForIndexing(connectorId);
setDriveFolderDialogOpen(true);
setIsLoadingFolders(true);
try {
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/connectors/${connectorId}/google-drive/folders`,
{ method: "GET" }
);
if (!response.ok) {
throw new Error("Failed to load folders");
}
const data = await response.json();
setDriveFolders(data.folders || []);
} catch (error) {
console.error("Error loading folders:", error);
toast.error("Failed to load Google Drive folders");
setDriveFolderDialogOpen(false);
} finally {
setIsLoadingFolders(false);
}
};
// Handle Google Drive folder indexing
@ -204,15 +174,17 @@ export default function ConnectorsPage() {
try {
setIndexingConnectorId(selectedConnectorForIndexing);
// Call indexConnector with folder_ids and folder_names as query params
await indexConnector(
selectedConnectorForIndexing,
searchSpaceId,
undefined,
undefined,
selectedFolders.map((f) => f.id).join(","),
selectedFolders.map((f) => f.name).join(", ")
);
const folderIds = selectedFolders.map((f) => f.id).join(",");
const folderNames = selectedFolders.map((f) => f.name).join(", ");
await indexConnector({
connector_id: selectedConnectorForIndexing,
queryParams: {
search_space_id: searchSpaceId,
folder_ids: folderIds,
folder_names: folderNames,
},
});
toast.success(t("indexing_started"));
} catch (error) {
console.error("Error indexing connector content:", error);
@ -221,7 +193,6 @@ export default function ConnectorsPage() {
setIndexingConnectorId(null);
setSelectedConnectorForIndexing(null);
setSelectedFolders([]);
setDriveFolders([]);
}
};
@ -747,14 +718,13 @@ export default function ConnectorsPage() {
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setDriveFolderDialogOpen(false);
setSelectedConnectorForIndexing(null);
setSelectedFolders([]);
setDriveFolders([]);
}}
>
{tCommon("cancel")}
onClick={() => {
setDriveFolderDialogOpen(false);
setSelectedConnectorForIndexing(null);
setSelectedFolders([]);
}}
>
{tCommon("cancel")}
</Button>
<Button onClick={handleIndexDriveFolder} disabled={selectedFolders.length === 0}>
{t("start_indexing")}

View file

@ -18,7 +18,8 @@ import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { authenticatedFetch } from "@/lib/auth-utils";
import { useGoogleDriveFolders } from "@/hooks/use-google-drive-folders";
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
interface DriveItem {
id: string;
@ -70,10 +71,13 @@ export function GoogleDriveFolderTree({
selectedFolders,
onSelectFolders,
}: GoogleDriveFolderTreeProps) {
const [rootItems, setRootItems] = useState<DriveItem[]>([]);
const [itemStates, setItemStates] = useState<Map<string, ItemTreeNode>>(new Map());
const [isLoadingRoot, setIsLoadingRoot] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const { data: rootData, isLoading: isLoadingRoot } = useGoogleDriveFolders({
connectorId,
});
const rootItems = rootData?.items || [];
const isFolderSelected = (folderId: string): boolean => {
return selectedFolders.some((f) => f.id === folderId);
@ -87,29 +91,6 @@ export function GoogleDriveFolderTree({
}
};
/**
* Load root-level folders and files from Google Drive.
*/
const loadRootItems = async () => {
if (isInitialized) return;
setIsLoadingRoot(true);
try {
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/connectors/${connectorId}/google-drive/folders`
);
if (!response.ok) throw new Error("Failed to load items");
const data = await response.json();
setRootItems(data.items || []);
setIsInitialized(true);
} catch (error) {
console.error("Error loading root items:", error);
} finally {
setIsLoadingRoot(false);
}
};
/**
* Find an item by ID across all loaded items (root and nested).
*/
@ -154,12 +135,10 @@ export function GoogleDriveFolderTree({
return newMap;
});
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/connectors/${connectorId}/google-drive/folders?parent_id=${folderId}`
);
if (!response.ok) throw new Error("Failed to load folder contents");
const data = await response.json();
const data = await connectorsApiService.listGoogleDriveFolders({
connector_id: connectorId,
parent_id: folderId,
});
const items = data.items || [];
setItemStates((prev) => {
@ -301,10 +280,6 @@ export function GoogleDriveFolderTree({
);
};
if (!isInitialized && !isLoadingRoot) {
loadRootItems();
}
return (
<div className="border rounded-md w-full overflow-hidden">
<ScrollArea className="h-[450px] w-full">

View file

@ -17,6 +17,7 @@ export const searchSourceConnectorTypeEnum = z.enum([
"CLICKUP_CONNECTOR",
"GOOGLE_CALENDAR_CONNECTOR",
"GOOGLE_GMAIL_CONNECTOR",
"GOOGLE_DRIVE_CONNECTOR",
"AIRTABLE_CONNECTOR",
"LUMA_CONNECTOR",
"ELASTICSEARCH_CONNECTOR",
@ -39,6 +40,19 @@ export const searchSourceConnector = z.object({
created_at: z.string(),
});
export const googleDriveItem = z.object({
id: z.string(),
name: z.string(),
mimeType: z.string(),
isFolder: z.boolean(),
parents: z.array(z.string()).optional(),
size: z.number().optional(),
iconLink: z.string().optional(),
webViewLink: z.string().optional(),
createdTime: z.string().optional(),
modifiedTime: z.string().optional(),
});
/**
* Get connectors
*/
@ -120,6 +134,9 @@ 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(),
}),
});
@ -140,6 +157,18 @@ export const listGitHubRepositoriesRequest = z.object({
export const listGitHubRepositoriesResponse = z.array(z.record(z.string(), z.any()));
/**
* List Google Drive folders
*/
export const listGoogleDriveFoldersRequest = z.object({
connector_id: z.number(),
parent_id: z.string().optional(),
});
export const listGoogleDriveFoldersResponse = z.object({
items: z.array(googleDriveItem),
});
// Inferred types
export type SearchSourceConnectorType = z.infer<typeof searchSourceConnectorTypeEnum>;
export type SearchSourceConnector = z.infer<typeof searchSourceConnector>;
@ -157,3 +186,6 @@ export type IndexConnectorRequest = z.infer<typeof indexConnectorRequest>;
export type IndexConnectorResponse = z.infer<typeof indexConnectorResponse>;
export type ListGitHubRepositoriesRequest = z.infer<typeof listGitHubRepositoriesRequest>;
export type ListGitHubRepositoriesResponse = z.infer<typeof listGitHubRepositoriesResponse>;
export type ListGoogleDriveFoldersRequest = z.infer<typeof listGoogleDriveFoldersRequest>;
export type ListGoogleDriveFoldersResponse = z.infer<typeof listGoogleDriveFoldersResponse>;
export type GoogleDriveItem = z.infer<typeof googleDriveItem>;

View file

@ -0,0 +1,29 @@
import { useQuery } from "@tanstack/react-query";
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
interface UseGoogleDriveFoldersOptions {
connectorId: number;
parentId?: string;
enabled?: boolean;
}
export function useGoogleDriveFolders({
connectorId,
parentId,
enabled = true,
}: UseGoogleDriveFoldersOptions) {
return useQuery({
queryKey: cacheKeys.connectors.googleDrive.folders(connectorId, parentId),
queryFn: async () => {
return connectorsApiService.listGoogleDriveFolders({
connector_id: connectorId,
parent_id: parentId,
});
},
enabled: enabled && !!connectorId,
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 2,
});
}

View file

@ -17,6 +17,9 @@ import {
type ListGitHubRepositoriesRequest,
listGitHubRepositoriesRequest,
listGitHubRepositoriesResponse,
type ListGoogleDriveFoldersRequest,
listGoogleDriveFoldersRequest,
listGoogleDriveFoldersResponse,
type UpdateConnectorRequest,
updateConnectorRequest,
updateConnectorResponse,
@ -34,7 +37,7 @@ class ConnectorsApiService {
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
@ -66,7 +69,7 @@ class ConnectorsApiService {
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
@ -85,7 +88,7 @@ class ConnectorsApiService {
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
@ -118,7 +121,7 @@ class ConnectorsApiService {
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
@ -138,7 +141,7 @@ class ConnectorsApiService {
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
@ -157,7 +160,7 @@ class ConnectorsApiService {
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
@ -187,7 +190,7 @@ class ConnectorsApiService {
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
@ -195,6 +198,29 @@ class ConnectorsApiService {
body: parsedRequest.data,
});
};
/**
* List Google Drive folders and files
*/
listGoogleDriveFolders = async (request: ListGoogleDriveFoldersRequest) => {
const parsedRequest = listGoogleDriveFoldersRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { connector_id, parent_id } = parsedRequest.data;
const queryParams = parent_id ? `?parent_id=${encodeURIComponent(parent_id)}` : "";
return baseApiService.get(
`/api/v1/connectors/${connector_id}/google-drive/folders${queryParams}`,
listGoogleDriveFoldersResponse
);
};
}
export const connectorsApiService = new ConnectorsApiService();

View file

@ -67,5 +67,9 @@ export const cacheKeys = {
["connectors", ...(queries ? Object.values(queries) : [])] as const,
byId: (connectorId: string) => ["connector", connectorId] as const,
index: () => ["connector", "index"] as const,
googleDrive: {
folders: (connectorId: number, parentId?: string) =>
["connectors", "google-drive", connectorId, "folders", parentId] as const,
},
},
};