This commit is contained in:
Louis Mazin 2026-10-14 18:26:03 +02:00
parent f1f7eed9c6
commit a9f9f70f84
2 changed files with 66 additions and 23 deletions

View File

@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
ChevronRight, FolderOpen, Grid2X2, List as ListIcon,
ChevronRight, Clock3, FolderOpen, Grid2X2, List as ListIcon,
LoaderCircle, Search, Upload,
} from 'lucide-react'
import FileList from './FileList'
@ -62,6 +62,12 @@ export default function DriveExplorer({ controller, user }) {
</>}
</div>}
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{isTrash && (
<p className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
<Clock3 className="size-3.5 shrink-0" aria-hidden="true" />
Les éléments de la corbeille sont supprimés définitivement après 5 jours.
</p>
)}
<p className="mt-1 text-sm text-muted-foreground">
{loading ? 'Chargement…' : `${items.length} élément${items.length > 1 ? 's' : ''}`}
{!canWrite ? ' · lecture seule' : ''}
@ -104,7 +110,7 @@ export default function DriveExplorer({ controller, user }) {
</h3>
<p className="mt-2 max-w-sm text-sm leading-relaxed text-muted-foreground">
{isTrash
? 'Les fichiers supprimés apparaîtront ici avant leur suppression définitive.'
? 'Les fichiers et dossiers supprimés apparaîtront ici.'
: search ? 'Essaie un autre nom de fichier.'
: view === 'shared' ? 'Les éléments que d’autres utilisateurs partagent avec toi apparaîtront ici.' : 'Importe tes premiers fichiers ou crée un dossier pour commencer à organiser ton espace.'}
</p>

View File

@ -19,8 +19,8 @@ export function useDriveController(user) {
const [query, setQuery] = useState('')
const [search, setSearch] = useState('')
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [note, setNote] = useState('')
const [errorNotice, setErrorNotice] = useState(null)
const [noteNotice, setNoteNotice] = useState(null)
const [layout, setLayout] = useState('grid')
const [sort, setSort] = useState('name')
const [dark, setDark] = useState(localStorage.getItem('drive_theme') === 'dark')
@ -42,6 +42,36 @@ export function useDriveController(user) {
const requestSequence = useRef(0)
const statsSequence = useRef(0)
// Feedback belongs to the exact location where it was produced, not to the
// entire Drive page. Leaving that location hides and discards its messages.
const feedbackScope = JSON.stringify([view, activeSpaceId, parent, search])
const feedbackScopeRef = useRef(feedbackScope)
feedbackScopeRef.current = feedbackScope
const noticeSequence = useRef(0)
const setNote = useCallback((text, scope = feedbackScopeRef.current) => {
setNoteNotice(text ? { text, scope, id: ++noticeSequence.current } : null)
}, [])
const setError = useCallback((text, scope = feedbackScopeRef.current) => {
setErrorNotice(text ? { text, scope } : null)
}, [])
const note = noteNotice?.scope === feedbackScope ? noteNotice.text : ''
const error = errorNotice?.scope === feedbackScope ? errorNotice.text : ''
useEffect(() => {
setNoteNotice(current => current?.scope === feedbackScope ? current : null)
setErrorNotice(current => current?.scope === feedbackScope ? current : null)
}, [feedbackScope])
useEffect(() => {
if (!noteNotice) return
const { id } = noteNotice
const timeout = setTimeout(() => {
// A former timer must never dismiss a newer notification.
setNoteNotice(current => current?.id === id ? null : current)
}, 4500)
return () => clearTimeout(timeout)
}, [noteNotice])
useEffect(() => {
document.documentElement.classList.toggle('dark', dark)
localStorage.setItem('drive_theme', dark ? 'dark' : 'light')
@ -65,6 +95,9 @@ export function useDriveController(user) {
useEffect(() => { loadSpaces() }, [loadSpaces])
const switchSpace = id => {
// A dialog may display a success message immediately after this action,
// before React has rendered the destination. Scope it to the new space.
feedbackScopeRef.current = JSON.stringify(['files', id, null, ''])
// Clicking the current space must not erase its statistics. No dependency
// changes in that case, so the stats effect would not fetch them again.
if (id !== activeSpaceId) {
@ -81,6 +114,7 @@ export function useDriveController(user) {
// Silent background synchronization: other users can rename, move or delete
// shared items. Ignore responses from an outdated folder/space request.
const refresh = useCallback(async (silent = false) => {
const sourceScope = JSON.stringify([view, activeSpaceId, parent, search])
const sequence = ++requestSequence.current
if (!silent) setLoading(true)
try {
@ -92,18 +126,18 @@ export function useDriveController(user) {
if (view === 'files' || view === 'shared') delete params.parent
}
const data = await drive.list(params)
if (sequence !== requestSequence.current) return
if (sequence !== requestSequence.current || feedbackScopeRef.current !== sourceScope) return
setItems(data.items)
if(view==='shared')setSharedFolderAccess({can_write:data.can_write,space_id:data.space_id,parentId:parent})
setTrail(data.breadcrumb || [])
if (data.limited && !silent) setNote('La liste affiche les 500 premiers résultats. Utilise la recherche pour affiner.')
if (data.limited && !silent) setNote('La liste affiche les 500 premiers résultats. Utilise la recherche pour affiner.', sourceScope)
if (!silent) setError('')
} catch (exception) {
if (sequence !== requestSequence.current) return
if (sequence !== requestSequence.current || feedbackScopeRef.current !== sourceScope) return
if (silent && parent && [403, 404].includes(exception.response?.status)) {
setParent(null)
setNote('Ce dossier a été déplacé, supprimé ou n’est plus accessible.')
} else if (!silent) setError(message(exception))
setNote('Ce dossier a été déplacé, supprimé ou n’est plus accessible.', JSON.stringify([view, activeSpaceId, null, search]))
} else if (!silent) setError(message(exception), sourceScope)
} finally {
if (sequence === requestSequence.current) setLoading(false)
}
@ -138,13 +172,8 @@ export function useDriveController(user) {
}, [])
useEffect(() => { refreshPending() }, [refreshPending])
useEffect(() => {
if (!note) return
const timeout = setTimeout(() => setNote(''), 4500)
return () => clearTimeout(timeout)
}, [note])
const section = id => {
feedbackScopeRef.current = JSON.stringify([id, activeSpaceId, null, ''])
setView(id)
setParent(null)
setQuery('')
@ -153,6 +182,7 @@ export function useDriveController(user) {
}
const openFolder = item => {
feedbackScopeRef.current = JSON.stringify([view === 'shared' ? 'shared' : 'files', activeSpaceId, item.id, ''])
setParent(item.id)
setView(view === 'shared' ? 'shared' : 'files')
setQuery('')
@ -177,6 +207,7 @@ export function useDriveController(user) {
}
const download = async item => {
const sourceScope = feedbackScopeRef.current
try {
const url = await drive.link(item.id, false)
const anchor = document.createElement('a')
@ -186,7 +217,7 @@ export function useDriveController(user) {
anchor.click()
anchor.remove()
} catch (exception) {
setError(message(exception))
setError(message(exception), sourceScope)
}
}
@ -222,25 +253,29 @@ export function useDriveController(user) {
}, [refresh, reloadStats, refreshPending])
const cancelPendingUpload = async id => {
const sourceScope = feedbackScopeRef.current
try {
await drive.cancelUpload(id)
setNote('Import interrompu annulé, espace réservé libéré.')
if (feedbackScopeRef.current === sourceScope) setNote('Import interrompu annulé, espace réservé libéré.', sourceScope)
await Promise.all([refreshPending(), reloadStats()])
} catch (exception) { setError(message(exception)) }
} catch (exception) { setError(message(exception), sourceScope) }
}
const run = async operation => {
const sourceScope = feedbackScopeRef.current
setBusy(true)
setError('')
try {
await operation()
setModal(null)
setAction(null)
setNote('Modifications enregistrées.')
refresh()
if (feedbackScopeRef.current === sourceScope) {
setNote('Modifications enregistrées.', sourceScope)
refresh()
}
reloadStats()
} catch (exception) {
setError(message(exception))
setError(message(exception), sourceScope)
} finally {
setBusy(false)
}
@ -259,21 +294,23 @@ export function useDriveController(user) {
})
const goRoot = () => {
feedbackScopeRef.current = JSON.stringify([view === 'shared' ? 'shared' : 'files', activeSpaceId, null, ''])
setView(view === 'shared' ? 'shared' : 'files')
setParent(null)
setQuery('')
}
const moveBrowse = useCallback(async destination => {
const sourceScope = feedbackScopeRef.current
setMoveFolder(destination)
try {
const data = await drive.list({ view: 'files', space: activeSpaceId, ...(destination ? { parent: destination } : {}) })
setMoveTrail(data.breadcrumb)
setMoveItems(data.items.filter(item => item.is_folder))
} catch (exception) {
setError(message(exception))
setError(message(exception), sourceScope)
}
}, [activeSpaceId])
}, [activeSpaceId, setError])
useEffect(() => {
if (modal === 'move') moveBrowse(null)