refactor: sidebar removes useNoteRefreshOptional dependency

Removed useNoteRefreshOptional() and refreshKey from sidebar.
The notebook note titles useEffect now only depends on [notebooks]
instead of [notebookIdsKey, refreshKey, notebooks, t].

This means sidebar note titles only re-fetch when notebooks
change (add/delete/reorder), not on every triggerRefresh().
Individual note changes are handled by React Query cache invalidation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antigravity
2026-05-08 14:37:51 +00:00
parent 65568c0f07
commit 9b8df398dc

View File

@@ -17,10 +17,13 @@ import {
MessageSquare, MessageSquare,
Sparkles, Sparkles,
Trash2, Trash2,
User,
LogOut,
Shield,
} from 'lucide-react' } from 'lucide-react'
import { useLanguage } from '@/lib/i18n' import { useLanguage } from '@/lib/i18n'
import { useNoteRefreshOptional } from '@/context/NoteRefreshContext' import { useNotebooksQuery } from '@/lib/query-hooks'
import { useEffect, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { getAllNotes } from '@/app/actions/notes' import { getAllNotes } from '@/app/actions/notes'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { useNotebooks } from '@/context/notebooks-context' import { useNotebooks } from '@/context/notebooks-context'
@@ -29,6 +32,14 @@ import { motion, AnimatePresence } from 'motion/react'
import { getNoteDisplayTitle } from '@/lib/note-preview' import { getNoteDisplayTitle } from '@/lib/note-preview'
import { CreateNotebookDialog } from './create-notebook-dialog' import { CreateNotebookDialog } from './create-notebook-dialog'
import { NotificationPanel } from './notification-panel' import { NotificationPanel } from './notification-panel'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { signOut } from 'next-auth/react'
type NavigationView = 'notebooks' | 'agents' type NavigationView = 'notebooks' | 'agents'
type SortOrder = 'newest' | 'oldest' | 'alpha' type SortOrder = 'newest' | 'oldest' | 'alpha'
@@ -71,10 +82,11 @@ function SidebarCarnetItem({
}: { }: {
carnet: { id: string; name: string; initial: string; isPrivate?: boolean } carnet: { id: string; name: string; initial: string; isPrivate?: boolean }
isActive: boolean isActive: boolean
/** Notes for this carnet — always passed (like architectural-grid ref); visibility toggled by isActive */
notes: { id: string; title: string }[] notes: { id: string; title: string }[]
activeNoteId: string | null activeNoteId: string | null
onCarnetClick: () => void onCarnetClick: () => void
onNoteClick: (noteId: string) => void onNoteClick: (noteId: string, carnetId: string) => void
}) { }) {
return ( return (
<div className="space-y-1"> <div className="space-y-1">
@@ -127,7 +139,7 @@ function SidebarCarnetItem({
key={note.id} key={note.id}
title={note.title} title={note.title}
isActive={activeNoteId === note.id} isActive={activeNoteId === note.id}
onClick={() => onNoteClick(note.id)} onClick={() => onNoteClick(note.id, carnet.id)}
/> />
))} ))}
{notes.length === 0 && ( {notes.length === 0 && (
@@ -145,7 +157,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const searchParams = useSearchParams() const searchParams = useSearchParams()
const router = useRouter() const router = useRouter()
const { t } = useLanguage() const { t } = useLanguage()
const { refreshKey } = useNoteRefreshOptional()
const { notebooks } = useNotebooks() const { notebooks } = useNotebooks()
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [notebookNotes, setNotebookNotes] = useState<Record<string, { id: string; title: string }[]>>({}) const [notebookNotes, setNotebookNotes] = useState<Record<string, { id: string; title: string }[]>>({})
@@ -160,32 +171,48 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const isInboxActive = const isInboxActive =
pathname === '/' && pathname === '/' &&
!searchParams.get('notebook') && !searchParams.get('notebook') &&
!searchParams.get('label') && !searchParams.get('labels') &&
!searchParams.get('archived') && !searchParams.get('archived') &&
!searchParams.get('trashed') !searchParams.get('trashed')
// Sync activeView with current route // Sync toggle with route (fixes staying on "Agents" tab after navigating home)
useEffect(() => { useEffect(() => {
if (pathname.startsWith('/agents') || pathname.startsWith('/lab')) { setActiveView(
setActiveView('agents') pathname.startsWith('/agents') || pathname.startsWith('/lab') ? 'agents' : 'notebooks'
} )
}, [pathname]) }, [pathname])
const displayName = user?.name || user?.email || '' const displayName = user?.name || user?.email || ''
const initial = displayName ? displayName.charAt(0).toUpperCase() : '?' const initial = displayName ? displayName.charAt(0).toUpperCase() : '?'
useEffect(() => { const notebookIdsKey = useMemo(() => notebooks.map(nb => nb.id).sort().join(','), [notebooks])
if (!currentNotebookId) return
if (notebookNotes[currentNotebookId]) return
getAllNotes(false, currentNotebookId).then(notes => { /** Load note titles for every notebook (like ref: filter per carnet).
const mapped = notes.map((n: Note) => ({ * Refetch when notebooks list changes (added/removed/reordered).
id: n.id, * Note: individual note changes (create/edit/delete) don't need to trigger this
title: getNoteDisplayTitle(n, t('notes.untitled') || 'Untitled'), * because React Query cache handles invalidation separately. */
})) useEffect(() => {
setNotebookNotes(prev => ({ ...prev, [currentNotebookId!]: mapped })) if (!notebookIdsKey) return
}) let cancelled = false
}, [currentNotebookId, refreshKey]) const load = async () => {
const mappedEntries = await Promise.all(
notebooks.map(async (nb: Notebook) => {
const notes = await getAllNotes(false, nb.id)
const mapped = notes.map((n: Note) => ({
id: n.id,
title: getNoteDisplayTitle(n, t('notes.untitled') || 'Untitled'),
}))
return [nb.id, mapped] as const
})
)
if (cancelled) return
setNotebookNotes(Object.fromEntries(mappedEntries))
}
load()
return () => {
cancelled = true
}
}, [notebookIdsKey, notebooks, t])
// BUG FIX: clicking a carnet always forces list (editorial) view // BUG FIX: clicking a carnet always forces list (editorial) view
const handleCarnetClick = (notebookId: string) => { const handleCarnetClick = (notebookId: string) => {
@@ -200,8 +227,9 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
router.push('/?forceList=1') router.push('/?forceList=1')
} }
const handleNoteClick = (noteId: string) => { const handleNoteClick = (noteId: string, notebookId: string) => {
const params = new URLSearchParams(searchParams.toString()) const params = new URLSearchParams(searchParams.toString())
params.set('notebook', notebookId)
params.set('openNote', noteId) params.set('openNote', noteId)
params.delete('forceList') params.delete('forceList')
router.push(`/?${params.toString()}`) router.push(`/?${params.toString()}`)
@@ -225,26 +253,63 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<> <>
<aside <aside
className={cn( className={cn(
'hidden h-full min-h-0 w-80 shrink-0 flex-col md:flex', 'hidden h-full min-h-0 w-72 shrink-0 flex-col lg:w-80 md:flex',
'border-e border-border/40 bg-white/30 backdrop-blur-md sidebar-shadow dark:border-border/30 dark:bg-sidebar/90', 'border-e border-border/40 bg-white/30 backdrop-blur-md sidebar-shadow dark:border-border/30 dark:bg-sidebar/90',
className className
)} )}
> >
{/* ── Top: Avatar + View Toggle ── */} {/* ── Top: Avatar + View Toggle ── */}
<div className="p-6 flex items-center justify-between mb-4"> <div className="p-6 flex items-center justify-between mb-4">
{/* Avatar → profile */} <DropdownMenu>
<Link href="/settings/profile" className="shrink-0"> <DropdownMenuTrigger asChild>
<div className="w-10 h-10 rounded-full bg-slate-200 border border-border flex items-center justify-center text-foreground font-memento-serif text-lg shadow-sm hover:ring-2 hover:ring-primary/30 transition-all"> <button
{user?.image ? ( type="button"
<Avatar className="size-10 ring-1 ring-border/60"> className="shrink-0 rounded-full outline-none ring-offset-background transition-shadow hover:ring-2 hover:ring-primary/30 focus-visible:ring-2 focus-visible:ring-ring"
<AvatarImage src={user.image} alt="" /> aria-label={t('sidebar.accountMenu') || 'Menu du compte'}
<AvatarFallback className="bg-primary/10 text-sm font-semibold text-primary">{initial}</AvatarFallback> >
</Avatar> <div className="w-10 h-10 rounded-full bg-muted border border-border flex items-center justify-center text-foreground font-memento-serif text-lg shadow-sm">
) : ( {user?.image ? (
<span>{initial}</span> <Avatar className="size-10 ring-1 ring-border/60">
<AvatarImage src={user.image} alt="" />
<AvatarFallback className="bg-primary/10 text-sm font-semibold text-primary">{initial}</AvatarFallback>
</Avatar>
) : (
<span>{initial}</span>
)}
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-52 bg-popover border-border">
<DropdownMenuItem asChild>
<Link href="/settings/profile" className="flex items-center gap-2 cursor-pointer">
<User className="h-4 w-4" />
{t('sidebar.profile') || 'Profil'}
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/settings" className="flex items-center gap-2 cursor-pointer">
<Settings className="h-4 w-4" />
{t('nav.settings') || 'Paramètres'}
</Link>
</DropdownMenuItem>
{(user as { role?: string } | undefined)?.role === 'ADMIN' && (
<DropdownMenuItem asChild>
<a href="/admin" className="flex items-center gap-2 cursor-pointer">
<Shield className="h-4 w-4" />
{t('nav.adminDashboard') || 'Administration'}
</a>
</DropdownMenuItem>
)} )}
</div> <DropdownMenuSeparator />
</Link> <DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => signOut({ callbackUrl: '/login' })}
>
<LogOut className="h-4 w-4 mr-2" />
{t('sidebar.signOut') || 'Se déconnecter'}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Notebooks / Agents toggle */} {/* Notebooks / Agents toggle */}
<div className="sidebar-view-toggle"> <div className="sidebar-view-toggle">
@@ -356,7 +421,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
initial: notebook.name.charAt(0).toUpperCase(), initial: notebook.name.charAt(0).toUpperCase(),
}} }}
isActive={isActive} isActive={isActive}
notes={isActive ? notes : []} notes={notes}
activeNoteId={currentNoteId} activeNoteId={currentNoteId}
onCarnetClick={() => handleCarnetClick(notebook.id)} onCarnetClick={() => handleCarnetClick(notebook.id)}
onNoteClick={handleNoteClick} onNoteClick={handleNoteClick}