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