fix(ui): sélection groupée dans la boîte de réception
Permet de cocher plusieurs notes, les déplacer vers un carnet ou les envoyer à la corbeille, et rend la carte mentale plus lisible. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import React, { useState, useEffect, useCallback, useRef, useTransition, useMemo
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Note } from '@/lib/types'
|
||||
import { getAllNotes, searchNotes, enableNoteHistory, getNoteById, createNote, deleteNote, togglePin, toggleArchive, updateNote, updateFullOrderWithoutRevalidation } from '@/app/actions/notes'
|
||||
import { getAllNotes, searchNotes, enableNoteHistory, getNoteById, createNote, deleteNote, togglePin, toggleArchive, updateNote, updateFullOrderWithoutRevalidation, bulkTrashNotes, bulkMoveNotes } from '@/app/actions/notes'
|
||||
import { NotesListViews, type NotesLayoutMode, type NotesClassicLayoutMode, isClassicLayoutMode } from '@/components/notes-list-views'
|
||||
import {
|
||||
NOTES_LAYOUT_STORAGE_KEY,
|
||||
@@ -36,7 +36,8 @@ import { toast } from 'sonner'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import { isDashboardHomeRoute } from '@/lib/dashboard/home-route'
|
||||
import { ConfirmDeleteNoteDialog } from '@/components/confirm-delete-note-dialog'
|
||||
import { showNoteTrashedToast } from '@/lib/notes/trash-toast'
|
||||
import { showNoteTrashedToast, showNotesTrashedToast } from '@/lib/notes/trash-toast'
|
||||
import { InboxBulkBar } from '@/components/inbox-bulk-bar'
|
||||
|
||||
|
||||
type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual'
|
||||
@@ -163,6 +164,9 @@ export function HomeClient({
|
||||
const [showStudyPlanner, setShowStudyPlanner] = useState(false)
|
||||
const [showOrganizer, setShowOrganizer] = useState(false)
|
||||
const [notePendingDelete, setNotePendingDelete] = useState<Note | null>(null)
|
||||
const [selectedInboxIds, setSelectedInboxIds] = useState<Set<string>>(() => new Set())
|
||||
const [bulkBusy, setBulkBusy] = useState(false)
|
||||
const [bulkTrashOpen, setBulkTrashOpen] = useState(false)
|
||||
|
||||
const handleExportCSV = useCallback(() => {
|
||||
if (!searchParams.get('notebook')) return
|
||||
@@ -194,6 +198,10 @@ export function HomeClient({
|
||||
}, [searchParams])
|
||||
|
||||
const notebookFilter = searchParams.get('notebook')
|
||||
const isInboxView =
|
||||
!notebookFilter &&
|
||||
searchParams.get('shared') !== '1' &&
|
||||
searchParams.get('reminders') !== '1'
|
||||
const schemaHook = useNotebookSchema(notebookFilter)
|
||||
const structuredModeActive = Boolean(notebookFilter && schemaHook.schema)
|
||||
const wantsStructuredView = Boolean(
|
||||
@@ -602,6 +610,68 @@ export function HomeClient({
|
||||
[noteVisibleInCurrentView, patchNoteInList, removeNoteFromList, t]
|
||||
)
|
||||
|
||||
const handleToggleInboxSelect = useCallback((id: string) => {
|
||||
setSelectedInboxIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id)
|
||||
else next.add(id)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleBulkMoveInbox = useCallback(
|
||||
async (notebookId: string) => {
|
||||
const moved = notes.filter((n) => selectedInboxIds.has(n.id))
|
||||
const ids = moved.map((n) => n.id)
|
||||
if (ids.length === 0) return
|
||||
setBulkBusy(true)
|
||||
for (const note of moved) {
|
||||
removeNoteFromList(note.id)
|
||||
emitNoteChange({ type: 'updated', note: { ...note, notebookId } })
|
||||
}
|
||||
setSelectedInboxIds(new Set())
|
||||
try {
|
||||
await bulkMoveNotes(ids, notebookId, { skipRevalidation: true })
|
||||
toast.success(t('notes.bulkMovedToast', { count: ids.length }))
|
||||
refreshNotebooks()
|
||||
} catch {
|
||||
setNotes((prev) => [...moved, ...prev])
|
||||
toast.error(t('general.error'))
|
||||
} finally {
|
||||
setBulkBusy(false)
|
||||
}
|
||||
},
|
||||
[notes, selectedInboxIds, removeNoteFromList, refreshNotebooks, t],
|
||||
)
|
||||
|
||||
const confirmBulkTrashInbox = useCallback(async () => {
|
||||
const moved = notes.filter((n) => selectedInboxIds.has(n.id))
|
||||
const ids = moved.map((n) => n.id)
|
||||
setBulkTrashOpen(false)
|
||||
if (ids.length === 0) return
|
||||
setBulkBusy(true)
|
||||
for (const note of moved) {
|
||||
removeNoteFromList(note.id)
|
||||
emitNoteChange({ type: 'deleted', noteId: note.id, notebookId: note.notebookId })
|
||||
}
|
||||
setSelectedInboxIds(new Set())
|
||||
try {
|
||||
await bulkTrashNotes(ids, { skipRevalidation: true })
|
||||
showNotesTrashedToast(moved, t, () => {
|
||||
setNotes((prev) => [...moved, ...prev])
|
||||
})
|
||||
refreshNotebooks()
|
||||
} catch {
|
||||
setNotes((prev) => [...moved, ...prev])
|
||||
for (const note of moved) {
|
||||
emitNoteChange({ type: 'created', note })
|
||||
}
|
||||
toast.error(t('general.error'))
|
||||
} finally {
|
||||
setBulkBusy(false)
|
||||
}
|
||||
}, [notes, selectedInboxIds, removeNoteFromList, refreshNotebooks, t])
|
||||
|
||||
const handleNoteContentPatch = useCallback((noteId: string, patch: Partial<Note>) => {
|
||||
setNotes((prev) => {
|
||||
const next = prev.map((n) => (n.id === noteId ? { ...n, ...patch } : n))
|
||||
@@ -827,6 +897,27 @@ export function HomeClient({
|
||||
return sortedNotes.filter(n => n.isPinned)
|
||||
}, [sortedNotes])
|
||||
|
||||
const handleSelectAllInbox = useCallback(() => {
|
||||
setSelectedInboxIds(new Set(sortedNotes.map((n) => n.id)))
|
||||
}, [sortedNotes])
|
||||
|
||||
const handleDeselectAllInbox = useCallback(() => {
|
||||
setSelectedInboxIds(new Set())
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInboxView) {
|
||||
setSelectedInboxIds(new Set())
|
||||
return
|
||||
}
|
||||
setSelectedInboxIds((prev) => {
|
||||
if (prev.size === 0) return prev
|
||||
const valid = new Set(sortedNotes.map((n) => n.id))
|
||||
const next = new Set([...prev].filter((id) => valid.has(id)))
|
||||
return next.size === prev.size ? prev : next
|
||||
})
|
||||
}, [isInboxView, sortedNotes])
|
||||
|
||||
const sortLabels: Record<SortOrder, string> = {
|
||||
newest: t('sidebar.sortNewest'),
|
||||
oldest: t('sidebar.sortOldest'),
|
||||
@@ -928,7 +1019,9 @@ export function HomeClient({
|
||||
? t('sidebar.sharedWithMe')
|
||||
: searchParams.get('reminders') === '1'
|
||||
? t('sidebar.reminders')
|
||||
: t('notes.title')}
|
||||
: isInboxView
|
||||
? t('sidebar.inbox')
|
||||
: t('notes.title')}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1224,6 +1317,18 @@ export function HomeClient({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isInboxView && notes.length > 0 && (
|
||||
<InboxBulkBar
|
||||
totalCount={sortedNotes.length}
|
||||
selectedCount={selectedInboxIds.size}
|
||||
busy={bulkBusy}
|
||||
onSelectAll={handleSelectAllInbox}
|
||||
onDeselectAll={handleDeselectAllInbox}
|
||||
onMove={(notebookId) => { void handleBulkMoveInbox(notebookId) }}
|
||||
onTrash={() => setBulkTrashOpen(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{availableTags.length > 0 && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -1381,6 +1486,14 @@ export function HomeClient({
|
||||
onNoteIllustrationGenerated={handleNoteIllustrationGenerated}
|
||||
onNoteIllustrationDeleted={handleNoteIllustrationDeleted}
|
||||
onGridReorder={handleGridReorder}
|
||||
selection={isInboxView ? {
|
||||
selectedIds: selectedInboxIds,
|
||||
onToggle: handleToggleInboxSelect,
|
||||
onToggleAll: selectedInboxIds.size === sortedNotes.length
|
||||
? handleDeselectAllInbox
|
||||
: handleSelectAllInbox,
|
||||
allSelected: sortedNotes.length > 0 && selectedInboxIds.size === sortedNotes.length,
|
||||
} : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -1499,11 +1612,16 @@ export function HomeClient({
|
||||
)}
|
||||
|
||||
<ConfirmDeleteNoteDialog
|
||||
open={notePendingDelete != null}
|
||||
open={notePendingDelete != null || bulkTrashOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setNotePendingDelete(null)
|
||||
if (!open) {
|
||||
setNotePendingDelete(null)
|
||||
setBulkTrashOpen(false)
|
||||
}
|
||||
}}
|
||||
onConfirm={confirmDeleteNoteFromList}
|
||||
title={bulkTrashOpen ? t('notes.confirmBulkDeleteTitle') : undefined}
|
||||
description={bulkTrashOpen ? t('notes.confirmBulkDelete') : undefined}
|
||||
onConfirm={bulkTrashOpen ? confirmBulkTrashInbox : confirmDeleteNoteFromList}
|
||||
/>
|
||||
|
||||
{showNotebookSlides && currentNotebook && (
|
||||
|
||||
Reference in New Issue
Block a user