ok
This commit is contained in:
parent
f1f7eed9c6
commit
a9f9f70f84
@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import {
|
import {
|
||||||
ChevronRight, FolderOpen, Grid2X2, List as ListIcon,
|
ChevronRight, Clock3, FolderOpen, Grid2X2, List as ListIcon,
|
||||||
LoaderCircle, Search, Upload,
|
LoaderCircle, Search, Upload,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
@ -62,6 +62,12 @@ export default function DriveExplorer({ controller, user }) {
|
|||||||
</>}
|
</>}
|
||||||
</div>}
|
</div>}
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
<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">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
{loading ? 'Chargement…' : `${items.length} élément${items.length > 1 ? 's' : ''}`}
|
{loading ? 'Chargement…' : `${items.length} élément${items.length > 1 ? 's' : ''}`}
|
||||||
{!canWrite ? ' · lecture seule' : ''}
|
{!canWrite ? ' · lecture seule' : ''}
|
||||||
@ -104,7 +110,7 @@ export default function DriveExplorer({ controller, user }) {
|
|||||||
</h3>
|
</h3>
|
||||||
<p className="mt-2 max-w-sm text-sm leading-relaxed text-muted-foreground">
|
<p className="mt-2 max-w-sm text-sm leading-relaxed text-muted-foreground">
|
||||||
{isTrash
|
{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.'
|
: 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.'}
|
: 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>
|
</p>
|
||||||
|
|||||||
@ -19,8 +19,8 @@ export function useDriveController(user) {
|
|||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [errorNotice, setErrorNotice] = useState(null)
|
||||||
const [note, setNote] = useState('')
|
const [noteNotice, setNoteNotice] = useState(null)
|
||||||
const [layout, setLayout] = useState('grid')
|
const [layout, setLayout] = useState('grid')
|
||||||
const [sort, setSort] = useState('name')
|
const [sort, setSort] = useState('name')
|
||||||
const [dark, setDark] = useState(localStorage.getItem('drive_theme') === 'dark')
|
const [dark, setDark] = useState(localStorage.getItem('drive_theme') === 'dark')
|
||||||
@ -42,6 +42,36 @@ export function useDriveController(user) {
|
|||||||
const requestSequence = useRef(0)
|
const requestSequence = useRef(0)
|
||||||
const statsSequence = 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(() => {
|
useEffect(() => {
|
||||||
document.documentElement.classList.toggle('dark', dark)
|
document.documentElement.classList.toggle('dark', dark)
|
||||||
localStorage.setItem('drive_theme', dark ? 'dark' : 'light')
|
localStorage.setItem('drive_theme', dark ? 'dark' : 'light')
|
||||||
@ -65,6 +95,9 @@ export function useDriveController(user) {
|
|||||||
useEffect(() => { loadSpaces() }, [loadSpaces])
|
useEffect(() => { loadSpaces() }, [loadSpaces])
|
||||||
|
|
||||||
const switchSpace = id => {
|
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
|
// Clicking the current space must not erase its statistics. No dependency
|
||||||
// changes in that case, so the stats effect would not fetch them again.
|
// changes in that case, so the stats effect would not fetch them again.
|
||||||
if (id !== activeSpaceId) {
|
if (id !== activeSpaceId) {
|
||||||
@ -81,6 +114,7 @@ export function useDriveController(user) {
|
|||||||
// Silent background synchronization: other users can rename, move or delete
|
// Silent background synchronization: other users can rename, move or delete
|
||||||
// shared items. Ignore responses from an outdated folder/space request.
|
// shared items. Ignore responses from an outdated folder/space request.
|
||||||
const refresh = useCallback(async (silent = false) => {
|
const refresh = useCallback(async (silent = false) => {
|
||||||
|
const sourceScope = JSON.stringify([view, activeSpaceId, parent, search])
|
||||||
const sequence = ++requestSequence.current
|
const sequence = ++requestSequence.current
|
||||||
if (!silent) setLoading(true)
|
if (!silent) setLoading(true)
|
||||||
try {
|
try {
|
||||||
@ -92,18 +126,18 @@ export function useDriveController(user) {
|
|||||||
if (view === 'files' || view === 'shared') delete params.parent
|
if (view === 'files' || view === 'shared') delete params.parent
|
||||||
}
|
}
|
||||||
const data = await drive.list(params)
|
const data = await drive.list(params)
|
||||||
if (sequence !== requestSequence.current) return
|
if (sequence !== requestSequence.current || feedbackScopeRef.current !== sourceScope) return
|
||||||
setItems(data.items)
|
setItems(data.items)
|
||||||
if(view==='shared')setSharedFolderAccess({can_write:data.can_write,space_id:data.space_id,parentId:parent})
|
if(view==='shared')setSharedFolderAccess({can_write:data.can_write,space_id:data.space_id,parentId:parent})
|
||||||
setTrail(data.breadcrumb || [])
|
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('')
|
if (!silent) setError('')
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
if (sequence !== requestSequence.current) return
|
if (sequence !== requestSequence.current || feedbackScopeRef.current !== sourceScope) return
|
||||||
if (silent && parent && [403, 404].includes(exception.response?.status)) {
|
if (silent && parent && [403, 404].includes(exception.response?.status)) {
|
||||||
setParent(null)
|
setParent(null)
|
||||||
setNote('Ce dossier a été déplacé, supprimé ou n’est plus accessible.')
|
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))
|
} else if (!silent) setError(message(exception), sourceScope)
|
||||||
} finally {
|
} finally {
|
||||||
if (sequence === requestSequence.current) setLoading(false)
|
if (sequence === requestSequence.current) setLoading(false)
|
||||||
}
|
}
|
||||||
@ -138,13 +172,8 @@ export function useDriveController(user) {
|
|||||||
}, [])
|
}, [])
|
||||||
useEffect(() => { refreshPending() }, [refreshPending])
|
useEffect(() => { refreshPending() }, [refreshPending])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!note) return
|
|
||||||
const timeout = setTimeout(() => setNote(''), 4500)
|
|
||||||
return () => clearTimeout(timeout)
|
|
||||||
}, [note])
|
|
||||||
|
|
||||||
const section = id => {
|
const section = id => {
|
||||||
|
feedbackScopeRef.current = JSON.stringify([id, activeSpaceId, null, ''])
|
||||||
setView(id)
|
setView(id)
|
||||||
setParent(null)
|
setParent(null)
|
||||||
setQuery('')
|
setQuery('')
|
||||||
@ -153,6 +182,7 @@ export function useDriveController(user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openFolder = item => {
|
const openFolder = item => {
|
||||||
|
feedbackScopeRef.current = JSON.stringify([view === 'shared' ? 'shared' : 'files', activeSpaceId, item.id, ''])
|
||||||
setParent(item.id)
|
setParent(item.id)
|
||||||
setView(view === 'shared' ? 'shared' : 'files')
|
setView(view === 'shared' ? 'shared' : 'files')
|
||||||
setQuery('')
|
setQuery('')
|
||||||
@ -177,6 +207,7 @@ export function useDriveController(user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const download = async item => {
|
const download = async item => {
|
||||||
|
const sourceScope = feedbackScopeRef.current
|
||||||
try {
|
try {
|
||||||
const url = await drive.link(item.id, false)
|
const url = await drive.link(item.id, false)
|
||||||
const anchor = document.createElement('a')
|
const anchor = document.createElement('a')
|
||||||
@ -186,7 +217,7 @@ export function useDriveController(user) {
|
|||||||
anchor.click()
|
anchor.click()
|
||||||
anchor.remove()
|
anchor.remove()
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
setError(message(exception))
|
setError(message(exception), sourceScope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,25 +253,29 @@ export function useDriveController(user) {
|
|||||||
}, [refresh, reloadStats, refreshPending])
|
}, [refresh, reloadStats, refreshPending])
|
||||||
|
|
||||||
const cancelPendingUpload = async id => {
|
const cancelPendingUpload = async id => {
|
||||||
|
const sourceScope = feedbackScopeRef.current
|
||||||
try {
|
try {
|
||||||
await drive.cancelUpload(id)
|
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()])
|
await Promise.all([refreshPending(), reloadStats()])
|
||||||
} catch (exception) { setError(message(exception)) }
|
} catch (exception) { setError(message(exception), sourceScope) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const run = async operation => {
|
const run = async operation => {
|
||||||
|
const sourceScope = feedbackScopeRef.current
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
await operation()
|
await operation()
|
||||||
setModal(null)
|
setModal(null)
|
||||||
setAction(null)
|
setAction(null)
|
||||||
setNote('Modifications enregistrées.')
|
if (feedbackScopeRef.current === sourceScope) {
|
||||||
refresh()
|
setNote('Modifications enregistrées.', sourceScope)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
reloadStats()
|
reloadStats()
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
setError(message(exception))
|
setError(message(exception), sourceScope)
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false)
|
setBusy(false)
|
||||||
}
|
}
|
||||||
@ -259,21 +294,23 @@ export function useDriveController(user) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const goRoot = () => {
|
const goRoot = () => {
|
||||||
|
feedbackScopeRef.current = JSON.stringify([view === 'shared' ? 'shared' : 'files', activeSpaceId, null, ''])
|
||||||
setView(view === 'shared' ? 'shared' : 'files')
|
setView(view === 'shared' ? 'shared' : 'files')
|
||||||
setParent(null)
|
setParent(null)
|
||||||
setQuery('')
|
setQuery('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveBrowse = useCallback(async destination => {
|
const moveBrowse = useCallback(async destination => {
|
||||||
|
const sourceScope = feedbackScopeRef.current
|
||||||
setMoveFolder(destination)
|
setMoveFolder(destination)
|
||||||
try {
|
try {
|
||||||
const data = await drive.list({ view: 'files', space: activeSpaceId, ...(destination ? { parent: destination } : {}) })
|
const data = await drive.list({ view: 'files', space: activeSpaceId, ...(destination ? { parent: destination } : {}) })
|
||||||
setMoveTrail(data.breadcrumb)
|
setMoveTrail(data.breadcrumb)
|
||||||
setMoveItems(data.items.filter(item => item.is_folder))
|
setMoveItems(data.items.filter(item => item.is_folder))
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
setError(message(exception))
|
setError(message(exception), sourceScope)
|
||||||
}
|
}
|
||||||
}, [activeSpaceId])
|
}, [activeSpaceId, setError])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modal === 'move') moveBrowse(null)
|
if (modal === 'move') moveBrowse(null)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user