mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-05 22:02:39 +02:00
feat: implement OneDrive picker integration and enhance connector functionality with new API endpoints and UI updates
This commit is contained in:
parent
e2dd6e61a9
commit
ea218b7be6
5 changed files with 457 additions and 212 deletions
252
surfsense_web/hooks/use-onedrive-picker.ts
Normal file
252
surfsense_web/hooks/use-onedrive-picker.ts
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
|
||||
export interface OneDrivePickerItem {
|
||||
id: string;
|
||||
name: string;
|
||||
isFolder: boolean;
|
||||
driveId?: string;
|
||||
}
|
||||
|
||||
export interface OneDrivePickerResult {
|
||||
folders: OneDrivePickerItem[];
|
||||
files: OneDrivePickerItem[];
|
||||
}
|
||||
|
||||
interface UseOneDrivePickerOptions {
|
||||
connectorId: number;
|
||||
onPicked: (result: OneDrivePickerResult) => void;
|
||||
}
|
||||
|
||||
export const ONEDRIVE_PICKER_OPEN_EVENT = "onedrive-picker-open";
|
||||
export const ONEDRIVE_PICKER_CLOSE_EVENT = "onedrive-picker-close";
|
||||
|
||||
async function fetchPickerToken(
|
||||
connectorId: number,
|
||||
resource?: string,
|
||||
): Promise<{ access_token: string; base_url: string }> {
|
||||
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
|
||||
const params = new URLSearchParams();
|
||||
if (resource) params.set("resource", resource);
|
||||
const qs = params.toString();
|
||||
const url = `${backendUrl}/api/v1/connectors/${connectorId}/onedrive/picker-token${qs ? `?${qs}` : ""}`;
|
||||
const response = await authenticatedFetch(url);
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.detail || `Failed to get picker token (${response.status})`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function useOneDrivePicker({ connectorId, onPicked }: UseOneDrivePickerOptions) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const onPickedRef = useRef(onPicked);
|
||||
onPickedRef.current = onPicked;
|
||||
const openingRef = useRef(false);
|
||||
const winRef = useRef<Window | null>(null);
|
||||
const portRef = useRef<MessagePort | null>(null);
|
||||
const messageHandlerRef = useRef<((e: MessageEvent) => void) | null>(null);
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const closePicker = useCallback(() => {
|
||||
window.dispatchEvent(new Event(ONEDRIVE_PICKER_CLOSE_EVENT));
|
||||
if (pollRef.current) {
|
||||
clearInterval(pollRef.current);
|
||||
pollRef.current = null;
|
||||
}
|
||||
if (messageHandlerRef.current) {
|
||||
window.removeEventListener("message", messageHandlerRef.current);
|
||||
messageHandlerRef.current = null;
|
||||
}
|
||||
if (winRef.current && !winRef.current.closed) {
|
||||
winRef.current.close();
|
||||
}
|
||||
winRef.current = null;
|
||||
portRef.current = null;
|
||||
openingRef.current = false;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && winRef.current) {
|
||||
closePicker();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onEscape);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onEscape);
|
||||
closePicker();
|
||||
};
|
||||
}, [closePicker]);
|
||||
|
||||
const openPicker = useCallback(async () => {
|
||||
if (openingRef.current) return;
|
||||
openingRef.current = true;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const { access_token, base_url } = await fetchPickerToken(connectorId);
|
||||
|
||||
const win = window.open("", "OneDrivePicker", "width=1080,height=680");
|
||||
if (!win) {
|
||||
throw new Error("Popup blocked. Please allow popups for this site.");
|
||||
}
|
||||
winRef.current = win;
|
||||
|
||||
const channelId = crypto.randomUUID();
|
||||
|
||||
const pickerConfig = {
|
||||
sdk: "8.0",
|
||||
entry: { oneDrive: { files: {} } },
|
||||
authentication: {},
|
||||
messaging: {
|
||||
origin: window.location.origin,
|
||||
channelId,
|
||||
},
|
||||
selection: { mode: "multiple" },
|
||||
typesAndSources: {
|
||||
mode: "all" as const,
|
||||
pivots: { oneDrive: true, recent: true },
|
||||
},
|
||||
};
|
||||
|
||||
const qs = new URLSearchParams({
|
||||
filePicker: JSON.stringify(pickerConfig),
|
||||
locale: navigator.language || "en-us",
|
||||
});
|
||||
const pickerUrl = `${base_url}/_layouts/15/FilePicker.aspx?${qs}`;
|
||||
|
||||
const form = win.document.createElement("form");
|
||||
form.setAttribute("action", pickerUrl);
|
||||
form.setAttribute("method", "POST");
|
||||
const input = win.document.createElement("input");
|
||||
input.setAttribute("type", "hidden");
|
||||
input.setAttribute("name", "access_token");
|
||||
input.setAttribute("value", access_token);
|
||||
form.appendChild(input);
|
||||
win.document.body.append(form);
|
||||
form.submit();
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.source !== win) return;
|
||||
const msg = event.data;
|
||||
if (msg?.type !== "initialize" || msg.channelId !== channelId) return;
|
||||
|
||||
const port = event.ports[0];
|
||||
portRef.current = port;
|
||||
|
||||
port.addEventListener("message", async (portEvent: MessageEvent) => {
|
||||
const payload = portEvent.data;
|
||||
if (payload.type !== "command") return;
|
||||
|
||||
port.postMessage({ type: "acknowledge", id: payload.id });
|
||||
|
||||
const cmd = payload.data;
|
||||
switch (cmd.command) {
|
||||
case "authenticate": {
|
||||
try {
|
||||
const result = await fetchPickerToken(connectorId, cmd.resource);
|
||||
port.postMessage({
|
||||
type: "result",
|
||||
id: payload.id,
|
||||
data: { result: "token", token: result.access_token },
|
||||
});
|
||||
} catch (err) {
|
||||
port.postMessage({
|
||||
type: "result",
|
||||
id: payload.id,
|
||||
data: {
|
||||
result: "error",
|
||||
error: {
|
||||
code: "unableToObtainToken",
|
||||
message: err instanceof Error ? err.message : "Token error",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "pick": {
|
||||
const items: Record<string, unknown>[] = cmd.items || [];
|
||||
const folders: OneDrivePickerItem[] = [];
|
||||
const files: OneDrivePickerItem[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const isFolder =
|
||||
item.folder != null ||
|
||||
(typeof item["@odata.type"] === "string" &&
|
||||
(item["@odata.type"] as string).includes("folder"));
|
||||
const parentRef = item.parentReference as
|
||||
| { driveId?: string }
|
||||
| undefined;
|
||||
const pickerItem: OneDrivePickerItem = {
|
||||
id: item.id as string,
|
||||
name: (item.name as string) || "Untitled",
|
||||
isFolder,
|
||||
driveId: parentRef?.driveId,
|
||||
};
|
||||
if (isFolder) {
|
||||
folders.push(pickerItem);
|
||||
} else {
|
||||
files.push(pickerItem);
|
||||
}
|
||||
}
|
||||
|
||||
onPickedRef.current({ folders, files });
|
||||
port.postMessage({
|
||||
type: "result",
|
||||
id: payload.id,
|
||||
data: { result: "success" },
|
||||
});
|
||||
closePicker();
|
||||
break;
|
||||
}
|
||||
case "close": {
|
||||
closePicker();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
port.postMessage({
|
||||
type: "result",
|
||||
id: payload.id,
|
||||
data: {
|
||||
result: "error",
|
||||
error: { code: "unsupportedCommand", message: cmd.command },
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
port.start();
|
||||
port.postMessage({ type: "activate" });
|
||||
};
|
||||
|
||||
messageHandlerRef.current = handleMessage;
|
||||
window.addEventListener("message", handleMessage);
|
||||
|
||||
pollRef.current = setInterval(() => {
|
||||
if (win.closed) {
|
||||
closePicker();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
window.dispatchEvent(new Event(ONEDRIVE_PICKER_OPEN_EVENT));
|
||||
} catch (err) {
|
||||
openingRef.current = false;
|
||||
const msg = err instanceof Error ? err.message : "Failed to open OneDrive Picker";
|
||||
setError(msg);
|
||||
console.error("OneDrive Picker error:", err);
|
||||
window.dispatchEvent(new Event(ONEDRIVE_PICKER_CLOSE_EVENT));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [connectorId, closePicker]);
|
||||
|
||||
return { openPicker, closePicker, loading, error };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue