feat: add multi-select upload and unified selection bar on images page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 12:03:20 +02:00
parent 3ec0b9fa3c
commit 99de0da823
2 changed files with 97 additions and 27 deletions
+86 -26
View File
@@ -11,15 +11,16 @@ import { useImages } from '@/hooks/useImages'
import type { ImageItem } from '@/types/images' import type { ImageItem } from '@/types/images'
function ImagesPageInner() { function ImagesPageInner() {
const { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, removeItem, assignProduct, reprocess, detour } = useImages() const { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, uploadSelected, removeItem, assignProduct, reprocess, detour } = useImages()
const [lightboxItem, setLightboxItem] = useState<ImageItem | null>(null) const [lightboxItem, setLightboxItem] = useState<ImageItem | null>(null)
const [mockupItem, setMockupItem] = useState<ImageItem | null>(null) const [mockupItem, setMockupItem] = useState<ImageItem | null>(null)
const [selected, setSelected] = useState<Set<string>>(new Set())
const [uploadingSelected, setUploadingSelected] = useState(false)
const openMockup = useCallback((id: string) => { const openMockup = useCallback((id: string) => {
const item = items.find(i => i.id === id) const item = items.find(i => i.id === id)
if (item) setMockupItem(item) if (item) setMockupItem(item)
}, [items]) }, [items])
const [selected, setSelected] = useState<Set<string>>(new Set())
const toggleSelect = useCallback((id: string, value: boolean) => { const toggleSelect = useCallback((id: string, value: boolean) => {
setSelected(prev => { setSelected(prev => {
@@ -30,23 +31,51 @@ function ImagesPageInner() {
}) })
}, []) }, [])
const selectAll = useCallback(() => { const clearSelection = useCallback(() => setSelected(new Set()), [])
const detourable = items.filter(i =>
i.status === 'converted' || i.status === 'not_found' || i.status === 'error' const selectAllDetourable = useCallback(() => {
) const ids = items
setSelected(new Set(detourable.map(i => i.id) as string[])) .filter(i => i.status === 'converted' || i.status === 'not_found' || i.status === 'error')
.map(i => i.id) as string[]
setSelected(new Set(ids))
}, [items]) }, [items])
const clearSelection = useCallback(() => setSelected(new Set()), []) const selectAllUploadable = useCallback(() => {
const ids = items
.filter(i => (i.status === 'ready' || i.status === 'converted') && !!i.shopifyProductId)
.map(i => i.id) as string[]
setSelected(new Set(ids))
}, [items])
const detourSelected = useCallback(() => { const detourSelected = useCallback(() => {
Array.from(selected).forEach(id => detour(id)) Array.from(selected).forEach(id => detour(id))
setSelected(new Set()) setSelected(new Set())
}, [selected, detour]) }, [selected, detour])
const handleUploadSelected = useCallback(async () => {
setUploadingSelected(true)
await uploadSelected(selected)
setUploadingSelected(false)
setSelected(new Set())
}, [selected, uploadSelected])
const selectedDetourable = Array.from(selected).filter(id => {
const item = items.find(i => i.id === id)
return item && (item.status === 'converted' || item.status === 'not_found' || item.status === 'error')
})
const selectedUploadable = Array.from(selected).filter(id => {
const item = items.find(i => i.id === id)
return item && (item.status === 'ready' || item.status === 'converted') && !!item.shopifyProductId
})
const detourable = items.filter(i => const detourable = items.filter(i =>
i.status === 'converted' || i.status === 'not_found' || i.status === 'error' i.status === 'converted' || i.status === 'not_found' || i.status === 'error'
) )
const uploadable = items.filter(i =>
(i.status === 'ready' || i.status === 'converted') && !!i.shopifyProductId
)
const showSelectionBar = detourable.length > 0 || uploadable.length > 0
return ( return (
<> <>
@@ -66,26 +95,57 @@ function ImagesPageInner() {
<GlobalActions items={items} onUploadAll={uploadAll} /> <GlobalActions items={items} onUploadAll={uploadAll} />
)} )}
{/* Barre de sélection multiple pour détourage par lot */} {/* Barre de sélection multiple */}
{detourable.length > 0 && ( {showSelectionBar && (
<div className="flex items-center gap-3 px-3 py-2 bg-slate-800 rounded-lg border border-slate-700"> <div className="flex items-center gap-2 px-3 py-2 bg-slate-800 rounded-lg border border-slate-700 flex-wrap">
<span className="text-xs text-slate-400">{detourable.length} image{detourable.length > 1 ? 's' : ''} sans détourage</span> {/* Compteurs et raccourcis de sélection */}
<button onClick={selectAll} className="text-xs text-indigo-400 hover:text-indigo-300"> <div className="flex items-center gap-2 text-xs text-slate-400">
Tout sélectionner {detourable.length > 0 && (
</button> <button onClick={selectAllDetourable} className="hover:text-purple-300 transition-colors">
Sélectionner à détourer ({detourable.length})
</button>
)}
{detourable.length > 0 && uploadable.length > 0 && (
<span className="text-slate-600">·</span>
)}
{uploadable.length > 0 && (
<button onClick={selectAllUploadable} className="hover:text-indigo-300 transition-colors">
Sélectionner à uploader ({uploadable.length})
</button>
)}
</div>
{/* Actions sur la sélection */}
{selected.size > 0 && ( {selected.size > 0 && (
<> <>
<span className="text-xs text-slate-500">·</span> <span className="text-slate-600 text-xs">|</span>
<span className="text-xs text-white font-medium">{selected.size} sélectionnée{selected.size > 1 ? 's' : ''}</span> <span className="text-xs text-white font-medium">
<button {selected.size} sélectionnée{selected.size > 1 ? 's' : ''}
onClick={detourSelected} </span>
className="ml-auto px-3 py-1 text-xs bg-purple-700 hover:bg-purple-600 text-white font-semibold rounded" <div className="ml-auto flex items-center gap-2">
> {selectedDetourable.length > 0 && (
Détourer la sélection <button
</button> onClick={detourSelected}
<button onClick={clearSelection} className="text-xs text-slate-500 hover:text-slate-300"> className="px-3 py-1 text-xs bg-purple-700 hover:bg-purple-600 text-white font-semibold rounded"
>
</button> Détourer ({selectedDetourable.length})
</button>
)}
{selectedUploadable.length > 0 && (
<button
onClick={handleUploadSelected}
disabled={uploadingSelected}
className="px-3 py-1 text-xs bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded disabled:opacity-50"
>
{uploadingSelected
? `⬆️ Upload en cours…`
: `⬆️ Uploader (${selectedUploadable.length})`}
</button>
)}
<button onClick={clearSelection} className="text-xs text-slate-500 hover:text-slate-300">
</button>
</div>
</> </>
)} )}
</div> </div>
+11 -1
View File
@@ -353,6 +353,16 @@ export function useImages() {
} }
}, [items, uploadOne]) }, [items, uploadOne])
const uploadSelected = useCallback(async (ids: Set<string>) => {
const toUpload = items.filter(i =>
ids.has(i.id) && (i.status === 'ready' || i.status === 'converted') && !!i.shopifyProductId
)
for (const item of toUpload) {
await uploadOne(item.id)
await new Promise(r => setTimeout(r, 500))
}
}, [items, uploadOne])
const removeItem = useCallback((id: string) => { const removeItem = useCallback((id: string) => {
dispatch({ type: 'REMOVE', id }) dispatch({ type: 'REMOVE', id })
}, []) }, [])
@@ -366,5 +376,5 @@ export function useImages() {
[items, update], [items, update],
) )
return { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, removeItem, assignProduct, reprocess, detour } return { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, uploadSelected, removeItem, assignProduct, reprocess, detour }
} }