From 3375059e645779730bba02e8f018c88b32e57bfd Mon Sep 17 00:00:00 2001 From: Serge Ivo Date: Sat, 11 Jul 2026 13:48:01 +1000 Subject: [PATCH] Add Zoho WorkDrive connector MVP --- store/console/src/tabs/KnowledgeTab.tsx | 142 ++++++++++++ store/console/src/tabs/SettingsTab.tsx | 66 ++++-- workers/api/src/index.ts | 2 + workers/api/src/lib/workdrive.test.ts | 173 ++++++++++++++ workers/api/src/lib/workdrive.ts | 290 ++++++++++++++++++++++++ workers/api/src/routes/drive.ts | 35 +-- workers/api/src/routes/workdrive.ts | 237 +++++++++++++++++++ workers/api/src/types.ts | 7 + workers/api/wrangler.toml | 6 + 9 files changed, 930 insertions(+), 28 deletions(-) create mode 100644 workers/api/src/lib/workdrive.test.ts create mode 100644 workers/api/src/lib/workdrive.ts create mode 100644 workers/api/src/routes/workdrive.ts diff --git a/store/console/src/tabs/KnowledgeTab.tsx b/store/console/src/tabs/KnowledgeTab.tsx index a86c29210..55d8df470 100644 --- a/store/console/src/tabs/KnowledgeTab.tsx +++ b/store/console/src/tabs/KnowledgeTab.tsx @@ -17,6 +17,16 @@ interface DriveFile { webViewLink?: string; } +interface WorkDriveFile { + id: string; + name: string; + type: string; + isFolder?: boolean; + mimeType?: string; + extension?: string; + permalink?: string; +} + interface Props { instanceId: string; isApply: boolean; @@ -50,6 +60,14 @@ export default function KnowledgeTab({ instanceId, isApply }: Props) { const [driveLoading, setDriveLoading] = useState(false); const [driveMsg, setDriveMsg] = useState(""); const [importingDriveId, setImportingDriveId] = useState(null); + const [showWorkdrive, setShowWorkdrive] = useState(false); + const [workdriveStatus, setWorkdriveStatus] = useState<{ connected: boolean; configured: boolean; account?: string | null } | null>(null); + const [workdriveRef, setWorkdriveRef] = useState(""); + const [workdriveFiles, setWorkdriveFiles] = useState([]); + const [workdriveNextOffset, setWorkdriveNextOffset] = useState(null); + const [workdriveLoading, setWorkdriveLoading] = useState(false); + const [workdriveMsg, setWorkdriveMsg] = useState(""); + const [importingWorkdriveId, setImportingWorkdriveId] = useState(null); const loadDocs = useCallback(async () => { try { @@ -87,6 +105,10 @@ export default function KnowledgeTab({ instanceId, isApply }: Props) { const s = await api<{ connected: boolean; configured: boolean; email?: string | null }>("/v1/drive/status"); setDriveStatus(s); } catch {} + try { + const s = await api<{ connected: boolean; configured: boolean; account?: string | null }>("/v1/workdrive/status"); + setWorkdriveStatus(s); + } catch {} })(); }, [subTab]); @@ -171,6 +193,52 @@ export default function KnowledgeTab({ instanceId, isApply }: Props) { setImportingDriveId(null); }; + const listWorkdriveFolder = async (ref = workdriveRef, offset = 0, append = false) => { + const folderRef = ref.trim(); + if (!folderRef) return; + setWorkdriveLoading(true); + setWorkdriveMsg(""); + try { + const params = new URLSearchParams({ folder: folderRef, offset: String(offset), limit: "50" }); + const d = await api<{ files?: WorkDriveFile[]; nextOffset?: number | null }>(`/v1/workdrive/folder?${params}`); + const files = d.files || []; + setWorkdriveRef(folderRef); + setWorkdriveFiles((prev) => (append ? [...prev, ...files] : files)); + setWorkdriveNextOffset(typeof d.nextOffset === "number" ? d.nextOffset : null); + if (!append && !files.length) setWorkdriveMsg("No files or folders found."); + } catch (e) { + setWorkdriveMsg(e instanceof Error ? e.message : "WorkDrive folder lookup failed"); + } + setWorkdriveLoading(false); + }; + + const openWorkdriveFolder = (file: WorkDriveFile) => { + setWorkdriveFiles([]); + setWorkdriveNextOffset(null); + listWorkdriveFolder(file.id); + }; + + const importWorkdriveFile = async (file?: WorkDriveFile) => { + const ref = file?.id || workdriveRef.trim(); + if (!ref) return; + setImportingWorkdriveId(file?.id || "__direct__"); + setWorkdriveMsg(""); + try { + await api(`/v1/workdrive/instances/${instanceId}/import`, { + method: "POST", + body: JSON.stringify(file ? { resourceId: file.id } : { url: ref }), + }); + setWorkdriveMsg(`Imported ${file?.name || "WorkDrive file"}.`); + setShowWorkdrive(false); + setWorkdriveFiles([]); + setWorkdriveNextOffset(null); + loadDocs(); + } catch (e) { + setWorkdriveMsg(e instanceof Error ? e.message : "WorkDrive import failed"); + } + setImportingWorkdriveId(null); + }; + const deleteDoc = async (docId: string) => { if (!confirm("Delete this document?")) return; try { @@ -237,6 +305,17 @@ export default function KnowledgeTab({ instanceId, isApply }: Props) { file.mimeType === "application/vnd.google-apps.presentation" ); + const supportedWorkdriveFile = (file: WorkDriveFile) => { + const type = (file.mimeType || "").toLowerCase(); + const ext = (file.extension || file.name.split(".").pop() || "").toLowerCase(); + return type.startsWith("text/") || type.includes("json") || type.includes("xml") || ["txt", "md", "csv", "json", "xml", "html", "htm", "yaml", "yml", "tsv"].includes(ext); + }; + + const isWorkdriveFolder = (file: WorkDriveFile) => { + const type = file.type.toLowerCase(); + return file.isFolder === true || type.includes("folder"); + }; + const subTabs: { id: KbSubTab; label: string }[] = [ { id: "docs", label: "Documents" }, { id: "memory", label: "Memory" }, @@ -333,6 +412,7 @@ export default function KnowledgeTab({ instanceId, isApply }: Props) { +