ultisuite-client/lib/api/hooks/use-drive-queries.ts
R3D347HR4Y d6d18f911b
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Lots of stuff and mobile app
2026-06-17 00:13:28 +02:00

553 lines
18 KiB
TypeScript

"use client"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { apiClient } from "@/lib/api/client"
import { driveFilesListApiPath, driveFilterCorpusApiPath } from "@/lib/api/drive-download"
import {
driveMountFilesApiPath,
driveOrgFilesApiPath,
type DrivePathRef,
withMoveCopyRefs,
withPathRefBody,
} from "@/lib/api/drive-roots"
import { useAuthReady } from "@/lib/api/use-auth-ready"
import { getApiBaseUrl } from "@/lib/runtime-config"
import { useDemoDrive, useIsDemoDrive } from "@/lib/demo/demo-drive-context"
import { DEMO_DRIVE_QUERY_ROOT } from "@/lib/demo/demo-drive-bootstrap"
import { useDemoDriveStore } from "@/lib/demo/demo-drive-store"
import type {
DriveFileInfo,
DriveListResponse,
DriveMount,
DriveOrgFolder,
DriveQuota,
DriveShare,
ShareRecipientLookup,
} from "@/lib/api/types"
import type { DriveShareMode } from "@/lib/drive/drive-share-types"
function filesKey(path: string, page: number, q: string) {
return ["drive", "files", path, page, q] as const
}
function demoFilesKey(path: string, page: number, q: string, version: number) {
return [...DEMO_DRIVE_QUERY_ROOT, "files", path, page, q, version] as const
}
function filterCorpusKey(path: string) {
return ["drive", "filter-corpus", path] as const
}
export function useDriveFilterCorpus(path: string, enabled = true) {
const { ready, authenticated } = useAuthReady()
const apiPath = driveFilterCorpusApiPath(path)
return useQuery({
queryKey: filterCorpusKey(path),
enabled: ready && authenticated && enabled,
staleTime: 60_000,
queryFn: () => apiClient.get<DriveListResponse>(apiPath),
})
}
export function useDriveList(path: string, page = 1, q = "", enabled = true) {
const { ready, authenticated } = useAuthReady()
const isDemoDrive = useIsDemoDrive()
const demoVersion = useDemoDriveStore((s) => s.version)
const apiPath = driveFilesListApiPath(path)
return useQuery({
queryKey: isDemoDrive
? demoFilesKey(path, page, q, demoVersion)
: filesKey(path, page, q),
enabled: ready && authenticated && enabled,
queryFn: () => {
if (isDemoDrive) {
return useDemoDriveStore.getState().listFiles(path, page)
}
return apiClient.get<DriveListResponse>(
`${apiPath}?page=${page}&page_size=50${q ? `&q=${encodeURIComponent(q)}` : ""}`
)
},
initialData: isDemoDrive
? () => useDemoDriveStore.getState().listFiles(path, page)
: undefined,
})
}
export function useDriveFileById(fileId: string, enabled = true) {
const { ready, authenticated } = useAuthReady()
return useQuery({
queryKey: ["drive", "file", fileId],
enabled: ready && authenticated && enabled && Boolean(fileId),
staleTime: 30_000,
queryFn: () => apiClient.get<DriveFileInfo>(`/drive/files/id/${encodeURIComponent(fileId)}`),
})
}
export function useDriveSharedWithMe(page = 1, q = "", enabled = true) {
const { ready, authenticated } = useAuthReady()
const isDemoDrive = useIsDemoDrive()
const demoVersion = useDemoDriveStore((s) => s.version)
return useQuery({
queryKey: isDemoDrive
? [...DEMO_DRIVE_QUERY_ROOT, "shared", page, q, demoVersion]
: ["drive", "shared", page, q],
enabled: ready && authenticated && enabled,
queryFn: () => {
if (isDemoDrive) return useDemoDriveStore.getState().listShared()
return apiClient.get<DriveListResponse>(
`/drive/shared?page=${page}&page_size=50${q ? `&q=${encodeURIComponent(q)}` : ""}`
)
},
initialData: isDemoDrive
? () => useDemoDriveStore.getState().listShared()
: undefined,
})
}
export function useDriveTrash(page = 1, q = "") {
const { ready, authenticated } = useAuthReady()
const isDemoDrive = useIsDemoDrive()
const demoVersion = useDemoDriveStore((s) => s.version)
return useQuery({
queryKey: isDemoDrive
? [...DEMO_DRIVE_QUERY_ROOT, "trash", page, q, demoVersion]
: ["drive", "trash", page, q],
enabled: ready && authenticated,
queryFn: () => {
if (isDemoDrive) return useDemoDriveStore.getState().listTrash()
return apiClient.get<DriveListResponse>(
`/drive/trash?page=${page}&page_size=50${q ? `&q=${encodeURIComponent(q)}` : ""}`
)
},
initialData: isDemoDrive
? () => useDemoDriveStore.getState().listTrash()
: undefined,
})
}
export function useDriveRecent() {
const { ready, authenticated } = useAuthReady()
const isDemoDrive = useIsDemoDrive()
const demoVersion = useDemoDriveStore((s) => s.version)
return useQuery({
queryKey: isDemoDrive
? [...DEMO_DRIVE_QUERY_ROOT, "recent", demoVersion]
: ["drive", "recent"],
enabled: ready && authenticated,
retry: 1,
queryFn: () => {
if (isDemoDrive) return useDemoDriveStore.getState().listRecent()
return apiClient.get<DriveListResponse>("/drive/recent?page_size=50")
},
initialData: isDemoDrive
? () => useDemoDriveStore.getState().listRecent()
: undefined,
})
}
export function useDriveStarred(path = "/") {
const { ready, authenticated } = useAuthReady()
const isDemoDrive = useIsDemoDrive()
const demoVersion = useDemoDriveStore((s) => s.version)
const suffix = path === "/" ? "" : path
return useQuery({
queryKey: isDemoDrive
? [...DEMO_DRIVE_QUERY_ROOT, "starred", path, demoVersion]
: ["drive", "starred", path],
enabled: ready && authenticated,
queryFn: () => {
if (isDemoDrive) return useDemoDriveStore.getState().listStarred(path)
return apiClient.get<DriveListResponse>(`/drive/starred${suffix}?page_size=50`)
},
initialData: isDemoDrive
? () => useDemoDriveStore.getState().listStarred(path)
: undefined,
})
}
function searchKey(q: string, scope: string, path: string, page: number, suggest: boolean) {
return ["drive", "search", q, scope, path, page, suggest] as const
}
export function useDriveSearch(
q: string,
scope: string,
path: string,
page = 1,
enabled = true
) {
const { ready, authenticated } = useAuthReady()
const isDemoDrive = useIsDemoDrive()
const demoVersion = useDemoDriveStore((s) => s.version)
const trimmed = q.trim()
return useQuery({
queryKey: isDemoDrive
? [...DEMO_DRIVE_QUERY_ROOT, "search", trimmed, scope, path, page, false, demoVersion]
: searchKey(trimmed, scope, path, page, false),
enabled: ready && authenticated && enabled && trimmed.length > 0,
queryFn: () => {
if (isDemoDrive) {
return useDemoDriveStore.getState().search(trimmed, scope, path, page)
}
const params = new URLSearchParams({
q: trimmed,
scope,
page: String(page),
page_size: "50",
})
if (scope === "folder" && path !== "/") {
params.set("path", path)
}
return apiClient.get<DriveListResponse>(`/drive/search?${params.toString()}`)
},
initialData:
isDemoDrive && trimmed.length > 0
? () => useDemoDriveStore.getState().search(trimmed, scope, path, page)
: undefined,
})
}
export function useDriveSearchSuggestions(
q: string,
scope: string,
path: string,
enabled = true
) {
const { ready, authenticated } = useAuthReady()
const isDemoDrive = useIsDemoDrive()
const demoVersion = useDemoDriveStore((s) => s.version)
const trimmed = q.trim()
return useQuery({
queryKey: isDemoDrive
? [...DEMO_DRIVE_QUERY_ROOT, "search", trimmed, scope, path, 1, true, demoVersion]
: searchKey(trimmed, scope, path, 1, true),
enabled: ready && authenticated && enabled && trimmed.length >= 2,
staleTime: 30_000,
queryFn: () => {
if (isDemoDrive) {
const res = useDemoDriveStore.getState().search(trimmed, scope, path, 1)
return { ...res, files: res.files.slice(0, 8) }
}
const params = new URLSearchParams({
q: trimmed,
scope,
suggest: "1",
page_size: "8",
})
if (scope === "folder" && path !== "/") {
params.set("path", path)
}
return apiClient.get<DriveListResponse>(`/drive/search?${params.toString()}`)
},
initialData:
isDemoDrive && trimmed.length >= 2
? () => {
const res = useDemoDriveStore.getState().search(trimmed, scope, path, 1)
return { ...res, files: res.files.slice(0, 8) }
}
: undefined,
})
}
export function useDriveQuota() {
const { ready, authenticated } = useAuthReady()
return useQuery({
queryKey: ["drive", "quota"],
enabled: ready && authenticated,
retry: 1,
queryFn: () => apiClient.get<DriveQuota>("/drive/quota"),
})
}
export function useDriveShares(filePath: string, enabled: boolean, pathRef?: DrivePathRef) {
const { ready, authenticated } = useAuthReady()
const rootQs =
pathRef?.root && pathRef.root !== "personal"
? `&root=${encodeURIComponent(pathRef.root)}&root_id=${encodeURIComponent(pathRef.root_id ?? "")}`
: ""
return useQuery({
queryKey: ["drive", "shares", filePath, pathRef?.root, pathRef?.root_id],
enabled: ready && authenticated && enabled && Boolean(filePath),
queryFn: () =>
apiClient.get<{ shares: DriveShare[] }>(
`/drive/shares?path=${encodeURIComponent(filePath)}${rootQs}`
),
})
}
export function useDriveOrgFolders(enabled = true) {
const { ready, authenticated } = useAuthReady()
return useQuery({
queryKey: ["drive", "org-folders"],
enabled: ready && authenticated && enabled,
staleTime: 60_000,
queryFn: async () => {
const res = await apiClient.get<{ folders: DriveOrgFolder[] }>("/drive/org-folders")
return res.folders ?? []
},
})
}
export function useDriveOrgList(folderId: string, path: string, page = 1, enabled = true) {
const { ready, authenticated } = useAuthReady()
const apiPath = driveOrgFilesApiPath(folderId, path)
return useQuery({
queryKey: ["drive", "org", folderId, path, page],
enabled: ready && authenticated && enabled && Boolean(folderId),
queryFn: () =>
apiClient.get<DriveListResponse>(`${apiPath}?page=${page}&page_size=50`),
})
}
export function useDriveMounts(orgSlugs: string[] = [], enabled = true) {
const { ready, authenticated } = useAuthReady()
const slugParam = orgSlugs.filter(Boolean).join(",")
return useQuery({
queryKey: ["drive", "mounts", slugParam],
enabled: ready && authenticated && enabled,
staleTime: 30_000,
queryFn: async () => {
const qs = slugParam ? `?org_slugs=${encodeURIComponent(slugParam)}` : ""
const res = await apiClient.get<{ mounts: DriveMount[] }>(`/drive/mounts${qs}`)
return res.mounts ?? []
},
})
}
export function useDriveMountList(mountId: string, path: string, page = 1, enabled = true) {
const { ready, authenticated } = useAuthReady()
const apiPath = driveMountFilesApiPath(mountId, path)
return useQuery({
queryKey: ["drive", "mount", mountId, path, page],
enabled: ready && authenticated && enabled && Boolean(mountId),
queryFn: () =>
apiClient.get<DriveListResponse>(`${apiPath}?page=${page}&page_size=50`),
})
}
export function useDriveMutations(pathRef?: DrivePathRef) {
const qc = useQueryClient()
const demoDrive = useDemoDrive()
const isDemoDrive = demoDrive?.enabled ?? false
const invalidate = () => {
if (isDemoDrive) {
useDemoDriveStore.getState().bump()
void qc.invalidateQueries({ queryKey: DEMO_DRIVE_QUERY_ROOT })
return
}
void qc.invalidateQueries({ queryKey: ["drive"] })
}
const ref = pathRef ?? { path: "/" }
const rootPrefix =
ref.root === "org" && ref.root_id
? `/drive/org-folders/${encodeURIComponent(ref.root_id)}`
: ref.root === "mount" && ref.root_id
? `/drive/mounts/${encodeURIComponent(ref.root_id)}`
: "/drive"
const createFolder = useMutation({
mutationFn: async (path: string) => {
if (isDemoDrive) {
useDemoDriveStore.getState().createFolder(path)
return
}
if (ref.root === "org" || ref.root === "mount") {
return apiClient.post<void>(`${rootPrefix}/folders${path}`, {})
}
return apiClient.post<void>(`/drive/folders${path}`, {})
},
onSuccess: invalidate,
})
const deleteFile = useMutation({
mutationFn: async (path: string) => {
if (isDemoDrive) {
useDemoDriveStore.getState().deleteFile(path)
demoDrive?.notify("Élément placé dans la corbeille")
return
}
if (ref.root === "org" || ref.root === "mount") {
return apiClient.delete(`${rootPrefix}/files${path}`)
}
return apiClient.delete(`/drive/files${path}`)
},
onSuccess: invalidate,
})
const rename = useMutation({
mutationFn: async (body: { path: string; new_name: string }) => {
if (isDemoDrive) {
useDemoDriveStore.getState().rename(body.path, body.new_name)
return
}
return apiClient.post<void>(
"/drive/rename",
withPathRefBody(body, { ...ref, path: body.path })
)
},
onSuccess: invalidate,
})
const move = useMutation({
mutationFn: (body: { source: string; destination: string; sourceRef?: DrivePathRef; destRef?: DrivePathRef }) =>
apiClient.post<void>(
"/drive/move",
withMoveCopyRefs(
body.sourceRef ?? { ...ref, path: body.source },
body.destRef ?? { ...ref, path: body.destination }
)
),
onSuccess: invalidate,
})
const copy = useMutation({
mutationFn: (body: { source: string; destination: string; sourceRef?: DrivePathRef; destRef?: DrivePathRef }) =>
apiClient.post<void>(
"/drive/copy",
withMoveCopyRefs(
body.sourceRef ?? { ...ref, path: body.source },
body.destRef ?? { ...ref, path: body.destination }
)
),
onSuccess: invalidate,
})
const favorite = useMutation({
mutationFn: async (body: { path: string; favorite: boolean }) => {
if (isDemoDrive) {
useDemoDriveStore.getState().setFavorite(body.path, body.favorite)
return
}
return apiClient.post<void>(
"/drive/favorite",
withPathRefBody(body, { ...ref, path: body.path })
)
},
onSuccess: invalidate,
})
const restore = useMutation({
mutationFn: async (name: string) => {
if (isDemoDrive) {
useDemoDriveStore.getState().restoreTrash(name)
return
}
return apiClient.post<void>("/drive/trash/restore", { name })
},
onSuccess: invalidate,
})
const deleteTrash = useMutation({
mutationFn: async (name: string) => {
if (isDemoDrive) {
useDemoDriveStore.getState().deleteTrash(name)
return
}
return apiClient.post<void>("/drive/trash/delete", { name })
},
onSuccess: invalidate,
})
const emptyTrash = useMutation({
mutationFn: async () => {
if (isDemoDrive) {
useDemoDriveStore.getState().emptyTrash()
return
}
return apiClient.delete("/drive/trash")
},
onSuccess: invalidate,
})
const createShare = useMutation({
mutationFn: (body: {
path: string
mode?: DriveShareMode
role?: string
permissions?: number
share_type?: number
share_with?: string
note?: string
send_mail?: boolean
}) =>
apiClient.post<DriveShare>("/drive/shares", {
...withPathRefBody({ path: body.path }, { ...ref, path: body.path }),
mode: body.mode ?? "public",
...(body.mode === "contact"
? {
share_with: body.share_with,
note: body.note,
send_mail: body.send_mail ?? true,
}
: body.mode === "internal"
? { share_type: 3 }
: { share_type: body.share_type ?? 3 }),
...(body.permissions != null && body.permissions > 0
? { permissions: body.permissions }
: { role: body.role ?? "viewer" }),
}),
onSuccess: (_data, variables) => {
qc.invalidateQueries({ queryKey: ["drive"] })
if (variables.path) {
qc.invalidateQueries({ queryKey: ["drive", "shares", variables.path] })
}
},
})
const deleteShare = useMutation({
mutationFn: (shareId: string) => apiClient.delete(`/drive/shares/${shareId}`),
onSuccess: invalidate,
})
const lookupShareRecipient = useMutation({
mutationFn: (email: string) =>
apiClient.get<ShareRecipientLookup>(
`/drive/shares/recipients/lookup?email=${encodeURIComponent(email)}`
),
})
const createFile = useMutation({
mutationFn: (body: { parent_path: string; name: string; kind: string }) =>
apiClient.post<{ path: string; file_id?: number }>("/drive/files/new", body),
onSuccess: invalidate,
})
return { createFolder, deleteFile, rename, move, copy, favorite, restore, deleteTrash, emptyTrash, createShare, deleteShare, lookupShareRecipient, createFile, invalidate }
}
export function useDriveMountMutations() {
const qc = useQueryClient()
const invalidate = () => qc.invalidateQueries({ queryKey: ["drive", "mounts"] })
const createMount = useMutation({
mutationFn: (body: {
scope: "user" | "org"
org_slug?: string
display_name: string
backend_type: string
webdav?: { host: string; root: string; user: string; password: string; secure?: boolean }
oauth_backend?: string
oauth_auth?: string
}) => apiClient.post<DriveMount>("/drive/mounts", body),
onSuccess: invalidate,
})
const deleteMount = useMutation({
mutationFn: (mountId: string) => apiClient.delete(`/drive/mounts/${encodeURIComponent(mountId)}`),
onSuccess: invalidate,
})
const fetchOAuthURL = async (mountId: string, redirectUri: string) =>
apiClient.get<{ oauth_url: string }>(
`/drive/mounts/${encodeURIComponent(mountId)}/oauth-url?redirect_uri=${encodeURIComponent(redirectUri)}`
)
const completeOAuth = useMutation({
mutationFn: ({ mountId, code, redirectUri }: { mountId: string; code: string; redirectUri: string }) =>
apiClient.post<{ status: string }>(`/drive/mounts/${encodeURIComponent(mountId)}/oauth/complete`, {
code,
redirect_uri: redirectUri,
}),
onSuccess: invalidate,
})
return { createMount, deleteMount, fetchOAuthURL, completeOAuth, invalidate }
}
/** @deprecated Use openDriveFileInNewTab / downloadDriveFile — API requires Authorization. */
export function fileDownloadUrl(path: string): string {
const base = getApiBaseUrl()
return `${base}/drive/download${path.startsWith("/") ? path : `/${path}`}`
}
export type { DriveFileInfo }