From 918ce6e348788d569278a6c303c33b4dbc0e3973 Mon Sep 17 00:00:00 2001 From: R3D347HR4Y Date: Sat, 13 Jun 2026 13:44:37 +0200 Subject: [PATCH] feat(cloud-integration): add external URL handling for mounted cloud files - Introduced `driveMountExternalURLApiPath` function to generate API paths for external file URLs. - Enhanced `DriveFileInfo` interface with new properties for cloud mount providers and external URLs. - Implemented functions to determine if a mounted file should open externally and to resolve external URLs. - Updated existing components to utilize the new external URL functionality for improved user experience when opening cloud documents. --- components/drive/drive-file-menu-actions.tsx | 11 ++- components/drive/file-thumbnail.tsx | 3 + lib/api/drive-roots.ts | 5 ++ lib/api/types.ts | 6 ++ lib/drive/cloud-native-open.ts | 90 ++++++++++++++++++++ lib/drive/docs-link-href.ts | 5 ++ lib/drive/drive-file-icon.tsx | 13 +++ lib/drive/drive-open-item.ts | 13 +++ 8 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 lib/drive/cloud-native-open.ts diff --git a/components/drive/drive-file-menu-actions.tsx b/components/drive/drive-file-menu-actions.tsx index 6811f73..9d72cd6 100644 --- a/components/drive/drive-file-menu-actions.tsx +++ b/components/drive/drive-file-menu-actions.tsx @@ -14,6 +14,7 @@ import { Trash2, Undo2, } from "lucide-react" +import { Icon } from "@iconify/react" import { ContextMenuItem, } from "@/components/ui/context-menu" @@ -28,6 +29,7 @@ import { } from "@/lib/drive/drive-menu-guard" import { cn } from "@/lib/utils" import { driveTrashItemKey } from "@/lib/drive/drive-trash" +import { mountCloudSuiteBrand } from "@/lib/drive/cloud-native-open" export const DRIVE_MENU_ITEM_CLASS = "gap-3 py-2 text-[#3c4043] focus:text-[#3c4043] dark:text-[#e8eaed] dark:focus:text-[#e8eaed] [&_svg]:text-[#3c4043] dark:[&_svg]:text-[#e8eaed]" @@ -96,6 +98,7 @@ export function DriveFileMenuActions({ }: DriveFileMenuActionsProps) { const single = targets.length === 1 ? targets[0]! : null const multi = targets.length > 1 + const mountSuiteBrand = single ? mountCloudSuiteBrand(single) : null const runAsync = async (fn: () => Promise, ok: string, err: string) => { onClose?.() @@ -169,8 +172,12 @@ export function DriveFileMenuActions({ }> = [ { key: "open", - label: "Ouvrir", - icon: , + label: mountSuiteBrand?.label ?? "Ouvrir", + icon: mountSuiteBrand ? ( + + ) : ( + + ), visible: !hideOpen && !multi && Boolean(single), onSelect: () => runMenuAction(() => window.setTimeout(() => onOpen(), 0)), }, diff --git a/components/drive/file-thumbnail.tsx b/components/drive/file-thumbnail.tsx index 4c58e95..9f90f0b 100644 --- a/components/drive/file-thumbnail.tsx +++ b/components/drive/file-thumbnail.tsx @@ -15,6 +15,7 @@ import { driveServerThumbnail, isOfficeFormat, } from "@/lib/drive/drive-preview" +import { shouldOpenMountFileExternally } from "@/lib/drive/cloud-native-open" import { cn } from "@/lib/utils" function useInView(rootMargin = "200px") { @@ -112,7 +113,9 @@ export function FileThumbnail({ const retriedRef = useRef(false) const queryClient = useQueryClient() const previewKind = drivePreviewKind(file) + const forceSuiteIcon = shouldOpenMountFileExternally(file) const canPreview = + !forceSuiteIcon && file.type === "file" && previewKind !== "audio" && (driveServerThumbnail(file) || previewKind !== null) diff --git a/lib/api/drive-roots.ts b/lib/api/drive-roots.ts index b2c3da5..17dc472 100644 --- a/lib/api/drive-roots.ts +++ b/lib/api/drive-roots.ts @@ -52,6 +52,11 @@ export function driveMountPreviewApiPath( return `/drive/mounts/${encodeURIComponent(mountId)}/preview/${encoded}?w=${width}&h=${height}` } +export function driveMountExternalURLApiPath(mountId: string, filePath: string): string { + const encoded = encodeFolderPath(filePath) + return `/drive/mounts/${encodeURIComponent(mountId)}/files/external-url/${encoded}` +} + export interface DrivePathRef { root?: "personal" | "org" | "mount" root_id?: string diff --git a/lib/api/types.ts b/lib/api/types.ts index 1e28d89..e6b6fb2 100644 --- a/lib/api/types.ts +++ b/lib/api/types.ts @@ -392,6 +392,12 @@ export interface DriveFileInfo { source?: string root_kind?: DriveRootKind root_id?: string + /** Cloud mount provider (google, microsoft) for external mounts. */ + mount_backend?: string + /** Provider web editor URL (Google Docs, Office Online, …). */ + external_url?: string + /** Default open uses external_url instead of Ultidocs / OnlyOffice. */ + open_externally?: boolean capabilities?: DriveFileCapabilities } diff --git a/lib/drive/cloud-native-open.ts b/lib/drive/cloud-native-open.ts new file mode 100644 index 0000000..a7146ef --- /dev/null +++ b/lib/drive/cloud-native-open.ts @@ -0,0 +1,90 @@ +import { apiClient } from "@/lib/api/client" +import { driveMountExternalURLApiPath } from "@/lib/api/drive-roots" +import type { DriveFileInfo } from "@/lib/api/types" + +/** Mounted cloud file that should open in Google / Microsoft web apps by default. */ +export function shouldOpenMountFileExternally(file: DriveFileInfo): boolean { + return ( + file.root_kind === "mount" && + file.type !== "directory" && + Boolean(file.open_externally) + ) +} + +export async function resolveMountExternalURL( + mountId: string, + filePath: string +): Promise { + const path = filePath.startsWith("/") ? filePath : `/${filePath}` + const data = await apiClient.get<{ external_url?: string }>( + driveMountExternalURLApiPath(mountId, path) + ) + const url = data.external_url?.trim() + return url || null +} + +/** Open mount cloud document in provider web app (new tab). Returns false if blocked or unresolved. */ +export async function openMountFileExternally(file: DriveFileInfo): Promise { + let url = file.external_url?.trim() + if (!url && file.root_id) { + url = (await resolveMountExternalURL(file.root_id, file.path)) ?? undefined + } + if (!url) return false + const opened = window.open(url, "_blank", "noopener,noreferrer") + return Boolean(opened) +} + +/** Branding for mount cloud-native files (label + Iconify icon). */ +export type MountCloudSuiteBrand = { + label: string + icon: string +} + +export function mountCloudSuiteBrand(file: DriveFileInfo): MountCloudSuiteBrand | null { + if (!shouldOpenMountFileExternally(file)) return null + const label = mountExternalOpenLabel(file) + const icon = mountCloudSuiteIcon(file) + if (!label || !icon) return null + return { label, icon } +} + +/** Iconify id for native suite file type on external mounts. */ +export function mountCloudSuiteIcon(file: DriveFileInfo): string | null { + if (!shouldOpenMountFileExternally(file)) return null + const backend = file.mount_backend + if (backend === "google") { + const mime = (file.mime_type ?? "").toLowerCase() + if (mime.includes("spreadsheet")) return "logos:google-sheets" + if (mime.includes("presentation")) return "logos:google-slides" + if (mime.includes("document")) return "logos:google-docs" + return "logos:google-drive" + } + if (backend === "microsoft") { + const ext = file.name.split(".").pop()?.toLowerCase() ?? "" + if (ext === "xls" || ext === "xlsx") return "vscode-icons:file-type-excel" + if (ext === "ppt" || ext === "pptx") return "vscode-icons:file-type-powerpoint" + if (ext === "doc" || ext === "docx") return "vscode-icons:file-type-word" + return "logos:microsoft-onedrive" + } + return null +} + +export function mountExternalOpenLabel(file: DriveFileInfo): string | null { + if (!shouldOpenMountFileExternally(file)) return null + const backend = file.mount_backend + if (backend === "google") { + const mime = (file.mime_type ?? "").toLowerCase() + if (mime.includes("spreadsheet")) return "Ouvrir dans Google Sheets" + if (mime.includes("presentation")) return "Ouvrir dans Google Slides" + if (mime.includes("document")) return "Ouvrir dans Google Docs" + return "Ouvrir dans Google Drive" + } + if (backend === "microsoft") { + const ext = file.name.split(".").pop()?.toLowerCase() ?? "" + if (ext === "xls" || ext === "xlsx") return "Ouvrir dans Excel Online" + if (ext === "ppt" || ext === "pptx") return "Ouvrir dans PowerPoint Online" + if (ext === "doc" || ext === "docx") return "Ouvrir dans Word Online" + return "Ouvrir dans Microsoft 365" + } + return "Ouvrir dans l’application cloud" +} diff --git a/lib/drive/docs-link-href.ts b/lib/drive/docs-link-href.ts index d24e55a..f6decfd 100644 --- a/lib/drive/docs-link-href.ts +++ b/lib/drive/docs-link-href.ts @@ -1,6 +1,7 @@ import { apiClient } from "@/lib/api/client" import type { DriveFileInfo } from "@/lib/api/types" import { driveFolderHref } from "@/lib/drive/drive-sidebar-tree" +import { shouldOpenMountFileExternally } from "@/lib/drive/cloud-native-open" import { shouldOpenInOnlyOffice, shouldOpenInRichTextEditor } from "@/lib/drive/drive-preview" import { buildDriveDocsEditHref, buildDriveEditHref } from "@/lib/drive/drive-url" @@ -19,6 +20,10 @@ export async function resolveDriveItemLinkHref(file: DriveFileInfo): Promise ) } + const suiteIcon = mountCloudSuiteIcon(file) + if (suiteIcon) { + return ( + + + + ) + } return } diff --git a/lib/drive/drive-open-item.ts b/lib/drive/drive-open-item.ts index 449758d..d9ab4a5 100644 --- a/lib/drive/drive-open-item.ts +++ b/lib/drive/drive-open-item.ts @@ -1,5 +1,9 @@ import { downloadDriveFile } from "@/lib/api/drive-download" import type { DriveFileInfo } from "@/lib/api/types" +import { + openMountFileExternally, + shouldOpenMountFileExternally, +} from "@/lib/drive/cloud-native-open" import { drivePreviewKind, isPreviewNavigable, @@ -50,6 +54,15 @@ export function openDriveItem(file: DriveFileInfo, options: OpenDriveItemOptions return } + if (shouldOpenMountFileExternally(file)) { + void openMountFileExternally(file).then((opened) => { + if (!opened) { + window.alert("Impossible d’ouvrir le document dans l’application cloud.") + } + }) + return + } + if (drivePreviewKind(file)) { const ext = file.name.split(".").pop()?.toLowerCase() ?? "" const richTextPreview =