Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced turbopack alias for canvas in next.config.mjs. - Updated package.json scripts for development and branding tasks. - Added new dependencies for Tiptap extensions. - Implemented new demo layouts for agenda, contacts, drive, and mail applications. - Enhanced globals.css for improved theming and splash screen animations. - Added OAuth callback handling for drive mounts. - Updated layout components to integrate new demo shells and improve structure.
552 lines
18 KiB
TypeScript
552 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 { 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 = process.env.NEXT_PUBLIC_API_URL ?? "/api/v1"
|
|
return `${base}/drive/download${path.startsWith("/") ? path : `/${path}`}`
|
|
}
|
|
|
|
export type { DriveFileInfo }
|