feat(cloud-integration): add external URL handling for mounted cloud files
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
This commit is contained in:
parent
4d31ac294b
commit
918ce6e348
@ -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<void>, ok: string, err: string) => {
|
||||
onClose?.()
|
||||
@ -169,8 +172,12 @@ export function DriveFileMenuActions({
|
||||
}> = [
|
||||
{
|
||||
key: "open",
|
||||
label: "Ouvrir",
|
||||
icon: <ExternalLink className="h-4 w-4" aria-hidden />,
|
||||
label: mountSuiteBrand?.label ?? "Ouvrir",
|
||||
icon: mountSuiteBrand ? (
|
||||
<Icon icon={mountSuiteBrand.icon} className="h-4 w-4" aria-hidden />
|
||||
) : (
|
||||
<ExternalLink className="h-4 w-4" aria-hidden />
|
||||
),
|
||||
visible: !hideOpen && !multi && Boolean(single),
|
||||
onSelect: () => runMenuAction(() => window.setTimeout(() => onOpen(), 0)),
|
||||
},
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
90
lib/drive/cloud-native-open.ts
Normal file
90
lib/drive/cloud-native-open.ts
Normal file
@ -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<string | null> {
|
||||
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<boolean> {
|
||||
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"
|
||||
}
|
||||
@ -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<str
|
||||
return driveFolderHref(view, file.path)
|
||||
}
|
||||
|
||||
if (shouldOpenMountFileExternally(file) && file.external_url) {
|
||||
return file.external_url
|
||||
}
|
||||
|
||||
if (shouldOpenInRichTextEditor(file)) {
|
||||
let fileId = file.file_id
|
||||
if (!fileId) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Icon } from "@iconify/react"
|
||||
import type { DriveFileInfo } from "@/lib/api/types"
|
||||
import { classifyDriveFile, type DriveMimeCategory } from "@/lib/drive/drive-filters"
|
||||
import { mountCloudSuiteIcon } from "@/lib/drive/cloud-native-open"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type DriveIconSize = "sm" | "md" | "lg"
|
||||
@ -127,5 +128,17 @@ export function DriveFileTypeIcon({
|
||||
/>
|
||||
)
|
||||
}
|
||||
const suiteIcon = mountCloudSuiteIcon(file)
|
||||
if (suiteIcon) {
|
||||
return (
|
||||
<span data-drive-type-icon className="inline-flex shrink-0">
|
||||
<Icon
|
||||
icon={suiteIcon}
|
||||
className={cn(SIZE_CLASS[size], "shrink-0", className)}
|
||||
aria-hidden
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <DriveMimeCategoryIcon category={category} size={size} className={className} />
|
||||
}
|
||||
|
||||
@ -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 =
|
||||
|
||||
Loading…
Reference in New Issue
Block a user