diff --git a/package.json b/package.json index 182d3466..990fda0d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:acting-as": "node --experimental-strip-types scripts/test-acting-as.mjs", "test:repeating-scope": "node --experimental-strip-types scripts/test-repeating-scope.mjs", "test:join-class": "node --experimental-strip-types scripts/test-join-class.mjs", + "test:drive-status": "node scripts/test-drive-status.mjs", "deploy": "node deploy.js", "postbuild": "cross-env OS_TYPE=$(uname -s) npm-run-all --parallel copy-files echo-message", "copy-files": "npm run copy-win || npm run copy-nix", diff --git a/scripts/test-drive-status.mjs b/scripts/test-drive-status.mjs new file mode 100644 index 00000000..e5b9e8fa --- /dev/null +++ b/scripts/test-drive-status.mjs @@ -0,0 +1,40 @@ +/** + * Unit tests for Drive account status parsing (mirrors workspace.ts helper). + * Run: node scripts/test-drive-status.mjs + */ +function parseDriveConnected(payload) { + if (!payload || typeof payload !== "object") return false; + const row = payload; + if (row.connected === true) return true; + if (row.drive_connected === true) return true; + const status = typeof row.status === "string" ? row.status.toLowerCase() : ""; + return status === "connected" || status === "ok"; +} + +let failed = 0; + +function assert(cond, msg) { + if (!cond) { + failed++; + console.error("FAIL:", msg); + } else { + console.log("ok:", msg); + } +} + +console.log("--- drive status parse tests ---\n"); + +assert(parseDriveConnected({ connected: true }), "connected: true"); +assert(parseDriveConnected({ drive_connected: true }), "drive_connected: true"); +assert(parseDriveConnected({ status: "connected" }), 'status: "connected"'); +assert(parseDriveConnected({ status: "ok" }), 'status: "ok"'); +assert(!parseDriveConnected({ connected: false }), "connected: false"); +assert(!parseDriveConnected({ status: "disconnected" }), 'status: "disconnected"'); +assert(!parseDriveConnected(null), "null"); +assert(!parseDriveConnected(undefined), "undefined"); + +if (failed) { + console.error(`\n${failed} test(s) failed`); + process.exit(1); +} +console.log("\nAll drive status tests passed."); diff --git a/src/common/workspace.ts b/src/common/workspace.ts index eb6ff783..26819ca0 100644 --- a/src/common/workspace.ts +++ b/src/common/workspace.ts @@ -360,10 +360,51 @@ export async function deleteWorkspaceFile(workspaceId: string, fileId: string): } } +/** sessionStorage key for post-OAuth return path (web platform). */ +export const DRIVE_OAUTH_RETURN_KEY = "mv_drive_oauth_return"; + +/** Account-level Drive connection (not per-workspace `drive_connected`). */ +export function parseDriveConnected(payload: unknown): boolean { + if (!payload || typeof payload !== "object") return false; + const row = payload as Record; + if (row.connected === true) return true; + if (row.drive_connected === true) return true; + const status = typeof row.status === "string" ? row.status.toLowerCase() : ""; + return status === "connected" || status === "ok"; +} + +/** GET /api/v1/me/drive/status — whether the signed-in account has Drive linked. */ +export async function fetchDriveStatus(): Promise { + try { + const payload = await apiFetch("/api/v1/me/drive/status"); + return parseDriveConnected(payload); + } catch (err) { + if (isMissingEndpoint(err)) return false; + throw err; + } +} + +export function storeDriveOAuthReturnPath(path?: string): void { + const target = (path || `${window.location.pathname}${window.location.search}`).trim(); + if (!target) return; + sessionStorage.setItem(DRIVE_OAUTH_RETURN_KEY, target); +} + +export function consumeDriveOAuthReturnPath(): string | null { + const path = sessionStorage.getItem(DRIVE_OAUTH_RETURN_KEY); + sessionStorage.removeItem(DRIVE_OAUTH_RETURN_KEY); + return path?.trim() || null; +} + +export function dispatchDriveStatusChanged(): void { + window.dispatchEvent(new CustomEvent("drive-status-changed")); +} + /** Start Google Drive OAuth — returns redirect URL. */ -export async function startDriveOAuth(): Promise { +export async function startDriveOAuth(platform: "web" | "brief" = "web"): Promise { const payload = await apiFetch<{ url?: string; state?: string }>("/api/v1/me/drive/oauth/start", { method: "POST", + body: { platform }, }); const url = payload?.url?.trim(); if (!url) { diff --git a/src/components/Portal/TaskWorkspace.vue b/src/components/Portal/TaskWorkspace.vue index a381699d..b446fdee 100644 --- a/src/components/Portal/TaskWorkspace.vue +++ b/src/components/Portal/TaskWorkspace.vue @@ -5,6 +5,23 @@
Loading workspace…
+ +