mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
add Google Picker hook and API types
This commit is contained in:
parent
1e2c54eea6
commit
a42a5a936c
4 changed files with 180 additions and 0 deletions
151
surfsense_web/hooks/use-google-picker.ts
Normal file
151
surfsense_web/hooks/use-google-picker.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
|
||||
|
||||
export interface PickerItem {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export interface PickerResult {
|
||||
folders: PickerItem[];
|
||||
files: PickerItem[];
|
||||
}
|
||||
|
||||
interface UseGooglePickerOptions {
|
||||
connectorId: number;
|
||||
onPicked: (result: PickerResult) => void;
|
||||
}
|
||||
|
||||
const PICKER_SCRIPT_URL = "https://apis.google.com/js/api.js";
|
||||
const FOLDER_MIME = "application/vnd.google-apps.folder";
|
||||
|
||||
let scriptLoadPromise: Promise<void> | null = null;
|
||||
let pickerApiPromise: Promise<void> | null = null;
|
||||
|
||||
function loadPickerScript(): Promise<void> {
|
||||
if (scriptLoadPromise) return scriptLoadPromise;
|
||||
if (typeof window !== "undefined" && window.gapi) {
|
||||
scriptLoadPromise = Promise.resolve();
|
||||
return scriptLoadPromise;
|
||||
}
|
||||
|
||||
scriptLoadPromise = new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = PICKER_SCRIPT_URL;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => {
|
||||
scriptLoadPromise = null;
|
||||
reject(new Error("Failed to load Google Picker script"));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
return scriptLoadPromise;
|
||||
}
|
||||
|
||||
function loadPickerApi(): Promise<void> {
|
||||
if (pickerApiPromise) return pickerApiPromise;
|
||||
|
||||
pickerApiPromise = new Promise<void>((resolve, reject) => {
|
||||
gapi.load("picker", {
|
||||
callback: () => resolve(),
|
||||
onerror: () => {
|
||||
pickerApiPromise = null;
|
||||
reject(new Error("Failed to load Google Picker API"));
|
||||
},
|
||||
});
|
||||
});
|
||||
return pickerApiPromise;
|
||||
}
|
||||
|
||||
export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOptions) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const onPickedRef = useRef(onPicked);
|
||||
onPickedRef.current = onPicked;
|
||||
const openingRef = useRef(false);
|
||||
|
||||
const openPicker = useCallback(async () => {
|
||||
if (openingRef.current) return;
|
||||
openingRef.current = true;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const [tokenData] = await Promise.all([
|
||||
connectorsApiService.getDrivePickerToken(connectorId),
|
||||
loadPickerScript().then(() => loadPickerApi()),
|
||||
]);
|
||||
|
||||
const { access_token, picker_api_key } = tokenData;
|
||||
|
||||
const docsView = new google.picker.DocsView(google.picker.ViewId.DOCS)
|
||||
.setIncludeFolders(true)
|
||||
.setSelectFolderEnabled(true);
|
||||
|
||||
let pickerInstance: google.picker.Picker | null = null;
|
||||
|
||||
const picker = 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")
|
||||
.setCallback((data: google.picker.ResponseObject) => {
|
||||
const action = data[google.picker.Response.ACTION];
|
||||
|
||||
if (action === google.picker.Action.PICKED) {
|
||||
const docs = data[google.picker.Response.DOCUMENTS];
|
||||
if (docs) {
|
||||
const folders: PickerItem[] = [];
|
||||
const files: PickerItem[] = [];
|
||||
|
||||
for (const doc of docs) {
|
||||
const mimeType = doc[google.picker.Document.MIME_TYPE] ?? "";
|
||||
const item: PickerItem = {
|
||||
id: doc[google.picker.Document.ID],
|
||||
name: doc[google.picker.Document.NAME] ?? "Untitled",
|
||||
mimeType,
|
||||
};
|
||||
if (mimeType === FOLDER_MIME) {
|
||||
folders.push(item);
|
||||
} else {
|
||||
files.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
onPickedRef.current({ folders, files });
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
action === google.picker.Action.PICKED ||
|
||||
action === google.picker.Action.CANCEL ||
|
||||
action === google.picker.Action.ERROR
|
||||
) {
|
||||
pickerInstance?.dispose();
|
||||
pickerInstance = null;
|
||||
openingRef.current = false;
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
pickerInstance = picker;
|
||||
picker.setVisible(true);
|
||||
} catch (err) {
|
||||
openingRef.current = false;
|
||||
const msg = err instanceof Error ? err.message : "Failed to open Google Picker";
|
||||
setError(msg);
|
||||
console.error("Google Picker error:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [connectorId]);
|
||||
|
||||
return { openPicker, loading, error };
|
||||
}
|
||||
|
|
@ -266,6 +266,17 @@ class ConnectorsApiService {
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Google Picker token (access_token + client_id + picker_api_key) for a Drive connector
|
||||
*/
|
||||
getDrivePickerToken = async (connectorId: number) => {
|
||||
return baseApiService.get<{
|
||||
access_token: string;
|
||||
client_id: string;
|
||||
picker_api_key: string;
|
||||
}>(`/api/v1/connectors/${connectorId}/drive-picker-token`);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// MCP Connector Methods
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -144,6 +144,8 @@
|
|||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/gapi": "^0.0.47",
|
||||
"@types/google.picker": "^0.0.52",
|
||||
"@types/node": "^20.19.9",
|
||||
"@types/pg": "^8.15.5",
|
||||
"@types/react": "^19.1.8",
|
||||
|
|
|
|||
16
surfsense_web/pnpm-lock.yaml
generated
16
surfsense_web/pnpm-lock.yaml
generated
|
|
@ -372,6 +372,12 @@ importers:
|
|||
'@types/canvas-confetti':
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0
|
||||
'@types/gapi':
|
||||
specifier: ^0.0.47
|
||||
version: 0.0.47
|
||||
'@types/google.picker':
|
||||
specifier: ^0.0.52
|
||||
version: 0.0.52
|
||||
'@types/node':
|
||||
specifier: ^20.19.9
|
||||
version: 20.19.33
|
||||
|
|
@ -3807,6 +3813,12 @@ packages:
|
|||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/gapi@0.0.47':
|
||||
resolution: {integrity: sha512-/ZsLuq6BffMgbKMtZyDZ8vwQvTyKhKQ1G2K6VyWCgtHHhfSSXbk4+4JwImZiTjWNXfI2q1ZStAwFFHSkNoTkHA==}
|
||||
|
||||
'@types/google.picker@0.0.52':
|
||||
resolution: {integrity: sha512-k0HyW8HxJePomM2r0JWq9nE9XG6qY93lVpoVnaV4WjQggDHrGwDKq3G8CGpcBWhQlJBTxX9jDIrI7RQnqjM63w==}
|
||||
|
||||
'@types/hast@2.3.10':
|
||||
resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
|
||||
|
||||
|
|
@ -10486,6 +10498,10 @@ snapshots:
|
|||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/gapi@0.0.47': {}
|
||||
|
||||
'@types/google.picker@0.0.52': {}
|
||||
|
||||
'@types/hast@2.3.10':
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue