diff --git a/frontend/src/components/drive/DriveExplorer.jsx b/frontend/src/components/drive/DriveExplorer.jsx index 0ec36d4..3c4952f 100644 --- a/frontend/src/components/drive/DriveExplorer.jsx +++ b/frontend/src/components/drive/DriveExplorer.jsx @@ -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 }) { } }

{title}

+ {isTrash && ( +

+

+ )}

{loading ? 'Chargement…' : `${items.length} élément${items.length > 1 ? 's' : ''}`} {!canWrite ? ' · lecture seule' : ''} @@ -104,7 +110,7 @@ export default function DriveExplorer({ controller, user }) {

{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.'}

diff --git a/frontend/src/hooks/useDriveController.js b/frontend/src/hooks/useDriveController.js index 2bc5c82..a0cb459 100644 --- a/frontend/src/hooks/useDriveController.js +++ b/frontend/src/hooks/useDriveController.js @@ -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)