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,
|
Trash2,
|
||||||
Undo2,
|
Undo2,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
import { Icon } from "@iconify/react"
|
||||||
import {
|
import {
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
} from "@/components/ui/context-menu"
|
} from "@/components/ui/context-menu"
|
||||||
@ -28,6 +29,7 @@ import {
|
|||||||
} from "@/lib/drive/drive-menu-guard"
|
} from "@/lib/drive/drive-menu-guard"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { driveTrashItemKey } from "@/lib/drive/drive-trash"
|
import { driveTrashItemKey } from "@/lib/drive/drive-trash"
|
||||||
|
import { mountCloudSuiteBrand } from "@/lib/drive/cloud-native-open"
|
||||||
|
|
||||||
export const DRIVE_MENU_ITEM_CLASS =
|
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]"
|
"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) {
|
}: DriveFileMenuActionsProps) {
|
||||||
const single = targets.length === 1 ? targets[0]! : null
|
const single = targets.length === 1 ? targets[0]! : null
|
||||||
const multi = targets.length > 1
|
const multi = targets.length > 1
|
||||||
|
const mountSuiteBrand = single ? mountCloudSuiteBrand(single) : null
|
||||||
|
|
||||||
const runAsync = async (fn: () => Promise<void>, ok: string, err: string) => {
|
const runAsync = async (fn: () => Promise<void>, ok: string, err: string) => {
|
||||||
onClose?.()
|
onClose?.()
|
||||||
@ -169,8 +172,12 @@ export function DriveFileMenuActions({
|
|||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
key: "open",
|
key: "open",
|
||||||
label: "Ouvrir",
|
label: mountSuiteBrand?.label ?? "Ouvrir",
|
||||||
icon: <ExternalLink className="h-4 w-4" aria-hidden />,
|
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),
|
visible: !hideOpen && !multi && Boolean(single),
|
||||||
onSelect: () => runMenuAction(() => window.setTimeout(() => onOpen(), 0)),
|
onSelect: () => runMenuAction(() => window.setTimeout(() => onOpen(), 0)),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
driveServerThumbnail,
|
driveServerThumbnail,
|
||||||
isOfficeFormat,
|
isOfficeFormat,
|
||||||
} from "@/lib/drive/drive-preview"
|
} from "@/lib/drive/drive-preview"
|
||||||
|
import { shouldOpenMountFileExternally } from "@/lib/drive/cloud-native-open"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function useInView(rootMargin = "200px") {
|
function useInView(rootMargin = "200px") {
|
||||||
@ -112,7 +113,9 @@ export function FileThumbnail({
|
|||||||
const retriedRef = useRef(false)
|
const retriedRef = useRef(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const previewKind = drivePreviewKind(file)
|
const previewKind = drivePreviewKind(file)
|
||||||
|
const forceSuiteIcon = shouldOpenMountFileExternally(file)
|
||||||
const canPreview =
|
const canPreview =
|
||||||
|
!forceSuiteIcon &&
|
||||||
file.type === "file" &&
|
file.type === "file" &&
|
||||||
previewKind !== "audio" &&
|
previewKind !== "audio" &&
|
||||||
(driveServerThumbnail(file) || previewKind !== null)
|
(driveServerThumbnail(file) || previewKind !== null)
|
||||||
|
|||||||
@ -52,6 +52,11 @@ export function driveMountPreviewApiPath(
|
|||||||
return `/drive/mounts/${encodeURIComponent(mountId)}/preview/${encoded}?w=${width}&h=${height}`
|
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 {
|
export interface DrivePathRef {
|
||||||
root?: "personal" | "org" | "mount"
|
root?: "personal" | "org" | "mount"
|
||||||
root_id?: string
|
root_id?: string
|
||||||
|
|||||||
@ -392,6 +392,12 @@ export interface DriveFileInfo {
|
|||||||
source?: string
|
source?: string
|
||||||
root_kind?: DriveRootKind
|
root_kind?: DriveRootKind
|
||||||
root_id?: string
|
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
|
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 { apiClient } from "@/lib/api/client"
|
||||||
import type { DriveFileInfo } from "@/lib/api/types"
|
import type { DriveFileInfo } from "@/lib/api/types"
|
||||||
import { driveFolderHref } from "@/lib/drive/drive-sidebar-tree"
|
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 { shouldOpenInOnlyOffice, shouldOpenInRichTextEditor } from "@/lib/drive/drive-preview"
|
||||||
import { buildDriveDocsEditHref, buildDriveEditHref } from "@/lib/drive/drive-url"
|
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)
|
return driveFolderHref(view, file.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldOpenMountFileExternally(file) && file.external_url) {
|
||||||
|
return file.external_url
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldOpenInRichTextEditor(file)) {
|
if (shouldOpenInRichTextEditor(file)) {
|
||||||
let fileId = file.file_id
|
let fileId = file.file_id
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Icon } from "@iconify/react"
|
import { Icon } from "@iconify/react"
|
||||||
import type { DriveFileInfo } from "@/lib/api/types"
|
import type { DriveFileInfo } from "@/lib/api/types"
|
||||||
import { classifyDriveFile, type DriveMimeCategory } from "@/lib/drive/drive-filters"
|
import { classifyDriveFile, type DriveMimeCategory } from "@/lib/drive/drive-filters"
|
||||||
|
import { mountCloudSuiteIcon } from "@/lib/drive/cloud-native-open"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export type DriveIconSize = "sm" | "md" | "lg"
|
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} />
|
return <DriveMimeCategoryIcon category={category} size={size} className={className} />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { downloadDriveFile } from "@/lib/api/drive-download"
|
import { downloadDriveFile } from "@/lib/api/drive-download"
|
||||||
import type { DriveFileInfo } from "@/lib/api/types"
|
import type { DriveFileInfo } from "@/lib/api/types"
|
||||||
|
import {
|
||||||
|
openMountFileExternally,
|
||||||
|
shouldOpenMountFileExternally,
|
||||||
|
} from "@/lib/drive/cloud-native-open"
|
||||||
import {
|
import {
|
||||||
drivePreviewKind,
|
drivePreviewKind,
|
||||||
isPreviewNavigable,
|
isPreviewNavigable,
|
||||||
@ -50,6 +54,15 @@ export function openDriveItem(file: DriveFileInfo, options: OpenDriveItemOptions
|
|||||||
return
|
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)) {
|
if (drivePreviewKind(file)) {
|
||||||
const ext = file.name.split(".").pop()?.toLowerCase() ?? ""
|
const ext = file.name.split(".").pop()?.toLowerCase() ?? ""
|
||||||
const richTextPreview =
|
const richTextPreview =
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user