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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user