mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-12 22:42:13 +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
|
// MCP Connector Methods
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,8 @@
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
|
"@types/gapi": "^0.0.47",
|
||||||
|
"@types/google.picker": "^0.0.52",
|
||||||
"@types/node": "^20.19.9",
|
"@types/node": "^20.19.9",
|
||||||
"@types/pg": "^8.15.5",
|
"@types/pg": "^8.15.5",
|
||||||
"@types/react": "^19.1.8",
|
"@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':
|
'@types/canvas-confetti':
|
||||||
specifier: ^1.9.0
|
specifier: ^1.9.0
|
||||||
version: 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':
|
'@types/node':
|
||||||
specifier: ^20.19.9
|
specifier: ^20.19.9
|
||||||
version: 20.19.33
|
version: 20.19.33
|
||||||
|
|
@ -3807,6 +3813,12 @@ packages:
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
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':
|
'@types/hast@2.3.10':
|
||||||
resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
|
resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
|
||||||
|
|
||||||
|
|
@ -10486,6 +10498,10 @@ snapshots:
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/gapi@0.0.47': {}
|
||||||
|
|
||||||
|
'@types/google.picker@0.0.52': {}
|
||||||
|
|
||||||
'@types/hast@2.3.10':
|
'@types/hast@2.3.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 2.0.11
|
'@types/unist': 2.0.11
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue