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,
Sparkles,
Trash2,
User,
LogOut,
Shield,
} from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { useNoteRefreshOptional } from '@/context/NoteRefreshContext'
import { useEffect, useState } from 'react'
import { useNotebooksQuery } from '@/lib/query-hooks'
import { useEffect, useMemo, useState } from 'react'
import { getAllNotes } from '@/app/actions/notes'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { useNotebooks } from '@/context/notebooks-context'
@@ -29,6 +32,14 @@ import { motion, AnimatePresence } from 'motion/react'
import { getNoteDisplayTitle } from '@/lib/note-preview'
import { CreateNotebookDialog } from './create-notebook-dialog'
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 SortOrder = 'newest' | 'oldest' | 'alpha'
@@ -71,10 +82,11 @@ function SidebarCarnetItem({
}: {
carnet: { id: string; name: string; initial: string; isPrivate?: boolean }
isActive: boolean
/** Notes for this carnet — always passed (like architectural-grid ref); visibility toggled by isActive */
notes: { id: string; title: string }[]
activeNoteId: string | null
onCarnetClick: () => void
onNoteClick: (noteId: string) => void
onNoteClick: (noteId: string, carnetId: string) => void
}) {
return (
<div className="space-y-1">
@@ -127,7 +139,7 @@ function SidebarCarnetItem({
key={note.id}
title={note.title}
isActive={activeNoteId === note.id}
onClick={() => onNoteClick(note.id)}
onClick={() => onNoteClick(note.id, carnet.id)}
/>
))}
{notes.length === 0 && (
@@ -145,7 +157,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const searchParams = useSearchParams()
const router = useRouter()
const { t } = useLanguage()
const { refreshKey } = useNoteRefreshOptional()
const { notebooks } = useNotebooks()
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
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 =
pathname === '/' &&
!searchParams.get('notebook') &&
!searchParams.get('label') &&
!searchParams.get('labels') &&
!searchParams.get('archived') &&
!searchParams.get('trashed')
// Sync activeView with current route
// Sync toggle with route (fixes staying on "Agents" tab after navigating home)
useEffect(() => {
if (pathname.startsWith('/agents') || pathname.startsWith('/lab')) {
setActiveView('agents')
}
setActiveView(
pathname.startsWith('/agents') || pathname.startsWith('/lab') ? 'agents' : 'notebooks'
)
}, [pathname])
const displayName = user?.name || user?.email || ''
const initial = displayName ? displayName.charAt(0).toUpperCase() : '?'
useEffect(() => {
if (!currentNotebookId) return
if (notebookNotes[currentNotebookId]) return
const notebookIdsKey = useMemo(() => notebooks.map(nb => nb.id).sort().join(','), [notebooks])
getAllNotes(false, currentNotebookId).then(notes => {
const mapped = notes.map((n: Note) => ({
id: n.id,
title: getNoteDisplayTitle(n, t('notes.untitled') || 'Untitled'),
}))
setNotebookNotes(prev => ({ ...prev, [currentNotebookId!]: mapped }))
})
}, [currentNotebookId, refreshKey])
/** Load note titles for every notebook (like ref: filter per carnet).
* Refetch when notebooks list changes (added/removed/reordered).
* Note: individual note changes (create/edit/delete) don't need to trigger this
* because React Query cache handles invalidation separately. */
useEffect(() => {
if (!notebookIdsKey) return
let cancelled = false
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
const handleCarnetClick = (notebookId: string) => {
@@ -200,8 +227,9 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
router.push('/?forceList=1')
}
const handleNoteClick = (noteId: string) => {
const handleNoteClick = (noteId: string, notebookId: string) => {
const params = new URLSearchParams(searchParams.toString())
params.set('notebook', notebookId)
params.set('openNote', noteId)
params.delete('forceList')
router.push(`/?${params.toString()}`)
@@ -225,26 +253,63 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<>
<aside
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',
className
)}
>
{/* ── Top: Avatar + View Toggle ── */}
<div className="p-6 flex items-center justify-between mb-4">
{/* Avatar → profile */}
<Link href="/settings/profile" className="shrink-0">
<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">
{user?.image ? (
<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>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
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"
aria-label={t('sidebar.accountMenu') || 'Menu du compte'}
>
<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 ? (
<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>
</Link>
<DropdownMenuSeparator />
<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 */}
<div className="sidebar-view-toggle">
@@ -356,7 +421,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
initial: notebook.name.charAt(0).toUpperCase(),
}}
isActive={isActive}
notes={isActive ? notes : []}
notes={notes}
activeNoteId={currentNoteId}
onCarnetClick={() => handleCarnetClick(notebook.id)}
onNoteClick={handleNoteClick}