fix(ui): sélection groupée dans la boîte de réception
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m17s
CI / Deploy production (on server) (push) Successful in 24s

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:
Antigravity
2026-09-01 19:40:10 +00:00
parent 4fc5e3ce04
commit a7b16870a4
15 changed files with 509 additions and 37 deletions

View File

@@ -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 && (