fix(sidebar): eliminate full page reloads and fix notebook actions visibility

- Fix createNotebookOptimistic to call loadNotebooks() + triggerRefresh()
  after POST, so new notebooks appear immediately without page reload
- Remove window.location.reload() from delete-notebook-dialog (context
  already handles state refresh)
- Rewrite edit-notebook-dialog to use updateNotebook() from context
  instead of raw fetch + full page reload
- Fix NoteRefreshContext: remove refreshKey from useCallback deps to
  prevent unstable triggerRefresh callback cascade
- Fix notebook actions menu visibility: consolidate NotebookActions and
  expand button into single positioned container with proper z-index
- Add actions menu to active/selected notebook (was previously missing)
- Use proper Notebook type instead of any in sidebar components
- Increase button pr-20 to pr-24 to reserve space for actions
- Remove redundant router.refresh() from create-notebook-dialog

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-03-29 22:23:25 +02:00
parent 8daf50ac3f
commit 806f4c4eeb
7 changed files with 106 additions and 114 deletions

View File

@@ -1,7 +1,6 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Plus, X, Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
@@ -47,7 +46,6 @@ interface CreateNotebookDialogProps {
}
export function CreateNotebookDialog({ open, onOpenChange }: CreateNotebookDialogProps) {
const router = useRouter()
const { t } = useLanguage()
const { createNotebookOptimistic } = useNotebooks()
const [name, setName] = useState('')
@@ -68,9 +66,8 @@ export function CreateNotebookDialog({ open, onOpenChange }: CreateNotebookDialo
icon: selectedIcon,
color: selectedColor,
})
// Close dialog — no full page reload needed
// Close dialog — context already updated sidebar state
onOpenChange?.(false)
router.refresh()
} catch (error) {
console.error('Failed to create notebook:', error)
} finally {

View File

@@ -26,7 +26,6 @@ export function DeleteNotebookDialog({ notebook, open, onOpenChange }: DeleteNot
try {
await deleteNotebook(notebook.id)
onOpenChange(false)
window.location.reload()
} catch (error) {
// Error already handled in UI
}

View File

@@ -1,7 +1,6 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { useState, useEffect } from 'react'
import { useLanguage } from '@/lib/i18n'
import { Button } from '@/components/ui/button'
import {
@@ -14,41 +13,36 @@ import {
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useNotebooks } from '@/context/notebooks-context'
import { Notebook } from '@/lib/types'
interface EditNotebookDialogProps {
notebook: any
notebook: Notebook
open: boolean
onOpenChange: (open: boolean) => void
}
export function EditNotebookDialog({ notebook, open, onOpenChange }: EditNotebookDialogProps) {
const router = useRouter()
const { updateNotebook } = useNotebooks()
const { t } = useLanguage()
const [name, setName] = useState(notebook?.name || '')
const [isSubmitting, setIsSubmitting] = useState(false)
useEffect(() => {
if (open) {
setName(notebook?.name || '')
}
}, [open, notebook?.name])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) return
setIsSubmitting(true)
try {
const response = await fetch(`/api/notebooks/${notebook.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name.trim() }),
})
if (response.ok) {
onOpenChange(false)
window.location.reload()
} else {
const error = await response.json()
}
} catch (error) {
// Error already handled in UI
await updateNotebook(notebook.id, { name: name.trim() })
onOpenChange(false)
} catch {
// Error handled in UI
} finally {
setIsSubmitting(false)
}
@@ -63,7 +57,6 @@ export function EditNotebookDialog({ notebook, open, onOpenChange }: EditNoteboo
{t('notebook.editDescription')}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
@@ -80,7 +73,6 @@ export function EditNotebookDialog({ notebook, open, onOpenChange }: EditNoteboo
/>
</div>
</div>
<DialogFooter>
<Button
type="button"

View File

@@ -9,47 +9,46 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Notebook } from '@/lib/types'
interface NotebookActionsProps {
notebook: any
notebook: Notebook
onEdit: () => void
onDelete: () => void
onSummary?: () => void // NEW: Summary action callback (IA6)
onSummary?: () => void
}
export function NotebookActions({ notebook, onEdit, onDelete, onSummary }: NotebookActionsProps) {
const { t } = useLanguage()
return (
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex items-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
>
<MoreVertical className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{onSummary && (
<DropdownMenuItem onClick={onSummary}>
<FileText className="h-4 w-4 mr-2" />
{t('notebook.summary')}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={onEdit}>
<Edit2 className="h-4 w-4 mr-2" />
{t('notebook.edit')}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
>
<MoreVertical className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{onSummary && (
<DropdownMenuItem onClick={onSummary}>
<FileText className="h-4 w-4 mr-2" />
{t('notebook.summary')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={onDelete}
className="text-red-600"
>
{t('notebook.delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
<DropdownMenuItem onClick={onEdit}>
<Edit2 className="h-4 w-4 mr-2" />
{t('notebook.edit')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={onDelete}
className="text-red-600"
>
{t('notebook.delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -15,6 +15,7 @@ import { EditNotebookDialog } from './edit-notebook-dialog'
import { NotebookSummaryDialog } from './notebook-summary-dialog'
import { useLanguage } from '@/lib/i18n'
import { useLabels } from '@/context/LabelContext'
import { Notebook } from '@/lib/types'
// Map icon names to lucide-react components
const ICON_MAP: Record<string, LucideIcon> = {
@@ -35,8 +36,7 @@ const ICON_MAP: Record<string, LucideIcon> = {
// Function to get icon component by name
const getNotebookIcon = (iconName: string) => {
const IconComponent = ICON_MAP[iconName] || Folder
return IconComponent
return ICON_MAP[iconName] || Folder
}
export function NotebooksList() {
@@ -49,9 +49,9 @@ export function NotebooksList() {
const { labels } = useLabels()
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [editingNotebook, setEditingNotebook] = useState<any>(null)
const [deletingNotebook, setDeletingNotebook] = useState<any>(null)
const [summaryNotebook, setSummaryNotebook] = useState<any>(null)
const [editingNotebook, setEditingNotebook] = useState<Notebook | null>(null)
const [deletingNotebook, setDeletingNotebook] = useState<Notebook | null>(null)
const [summaryNotebook, setSummaryNotebook] = useState<Notebook | null>(null)
const [expandedNotebook, setExpandedNotebook] = useState<string | null>(null)
const currentNotebookId = searchParams.get('notebook')
@@ -140,7 +140,7 @@ export function NotebooksList() {
</div>
{/* Notebooks Loop */}
{notebooks.map((notebook: any) => {
{notebooks.map((notebook: Notebook) => {
const isActive = currentNotebookId === notebook.id
const isExpanded = expandedNotebook === notebook.id
const isDragOver = dragOverNotebookId === notebook.id
@@ -151,39 +151,48 @@ export function NotebooksList() {
return (
<div key={notebook.id} className="group flex flex-col">
{isActive ? (
// Active notebook with expanded labels - STYLE MATCH Sidebar
// Active notebook with expanded labels
<div
onDrop={(e) => handleDrop(e, notebook.id)}
onDragOver={(e) => handleDragOver(e, notebook.id)}
onDragLeave={handleDragLeave}
className={cn(
"flex flex-col mr-2 rounded-r-full overflow-hidden transition-all",
"flex flex-col mr-2 rounded-r-full overflow-hidden transition-all relative",
!notebook.color && "bg-primary/10 dark:bg-primary/20",
isDragOver && "ring-2 ring-primary ring-dashed"
)}
style={notebook.color ? { backgroundColor: `${notebook.color}20` } : undefined}
>
{/* Header - allow pointer events for expand button */}
{/* Header */}
<div className="pointer-events-auto flex items-center justify-between px-6 py-3">
<div className="flex items-center gap-4 min-w-0">
<div className="flex items-center gap-4 min-w-0 flex-1">
<NotebookIcon
className={cn("w-5 h-5 flex-shrink-0 fill-current", !notebook.color && "text-primary dark:text-primary-foreground")}
style={notebook.color ? { color: notebook.color } : undefined}
/>
<span
className={cn("text-sm font-medium tracking-wide truncate max-w-[120px]", !notebook.color && "text-primary dark:text-primary-foreground")}
className={cn("text-sm font-medium tracking-wide truncate min-w-0", !notebook.color && "text-primary dark:text-primary-foreground")}
style={notebook.color ? { color: notebook.color } : undefined}
>
{notebook.name}
</span>
</div>
<button
onClick={() => handleToggleExpand(notebook.id)}
className={cn("transition-colors p-1 flex-shrink-0", !notebook.color && "text-primary hover:text-primary/80 dark:text-primary-foreground dark:hover:text-primary-foreground/80")}
style={notebook.color ? { color: notebook.color } : undefined}
>
<ChevronDown className={cn("w-4 h-4 transition-transform", isExpanded && "rotate-180")} />
</button>
<div className="flex items-center gap-1 flex-shrink-0">
{/* Actions menu for active notebook */}
<NotebookActions
notebook={notebook}
onEdit={() => setEditingNotebook(notebook)}
onDelete={() => setDeletingNotebook(notebook)}
onSummary={() => setSummaryNotebook(notebook)}
/>
<button
onClick={() => handleToggleExpand(notebook.id)}
className={cn("transition-colors p-1", !notebook.color && "text-primary hover:text-primary/80 dark:text-primary-foreground dark:hover:text-primary-foreground/80")}
style={notebook.color ? { color: notebook.color } : undefined}
>
<ChevronDown className={cn("w-4 h-4 transition-transform", isExpanded && "rotate-180")} />
</button>
</div>
</div>
{/* Contextual Labels Tree */}
@@ -221,45 +230,41 @@ export function NotebooksList() {
onDragOver={(e) => handleDragOver(e, notebook.id)}
onDragLeave={handleDragLeave}
className={cn(
"flex items-center group relative",
"flex items-center relative",
isDragOver && "ring-2 ring-blue-500 ring-dashed rounded-r-full mr-2"
)}
>
<div className="w-full flex">
<button
onClick={() => handleSelectNotebook(notebook.id)}
className={cn(
"pointer-events-auto flex items-center gap-4 px-6 py-3 rounded-r-full mr-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800/50 transition-colors w-full pr-20",
isDragOver && "opacity-50"
)}
>
<NotebookIcon className="w-5 h-5 flex-shrink-0" />
<span className="text-sm font-medium tracking-wide truncate min-w-0 text-left">{notebook.name}</span>
{notebook.notesCount > 0 && (
<span className="text-xs text-gray-400 ml-2 flex-shrink-0">({notebook.notesCount})</span>
)}
</button>
<button
onClick={() => handleSelectNotebook(notebook.id)}
className={cn(
"pointer-events-auto flex items-center gap-4 px-6 py-3 rounded-r-full mr-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800/50 transition-colors w-full pr-24",
isDragOver && "opacity-50"
)}
>
<NotebookIcon className="w-5 h-5 flex-shrink-0" />
<span className="text-sm font-medium tracking-wide truncate min-w-0 text-left">{notebook.name}</span>
{(notebook as any).notesCount > 0 && (
<span className="text-xs text-gray-400 ml-2 flex-shrink-0">({(notebook as any).notesCount})</span>
)}
</button>
{/* Expand button separate from main click */}
<button
onClick={(e) => { e.stopPropagation(); handleToggleExpand(notebook.id); }}
className={cn(
"absolute right-4 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 opacity-0 group-hover:opacity-100",
expandedNotebook === notebook.id && "opacity-100 rotate-180"
)}
>
<ChevronDown className="w-4 h-4" />
</button>
</div>
{/* Actions (visible on hover) */}
<div className="absolute right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10" style={{ right: '40px' }}>
{/* Actions + expand on the right — always rendered, visible on hover */}
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-10">
<NotebookActions
notebook={notebook}
onEdit={() => setEditingNotebook(notebook)}
onDelete={() => setDeletingNotebook(notebook)}
onSummary={() => setSummaryNotebook(notebook)}
/>
<button
onClick={(e) => { e.stopPropagation(); handleToggleExpand(notebook.id); }}
className={cn(
"p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors rounded-full hover:bg-gray-200 dark:hover:bg-gray-700",
expandedNotebook === notebook.id && "rotate-180"
)}
>
<ChevronDown className="w-4 h-4" />
</button>
</div>
</div>
)}
@@ -302,7 +307,7 @@ export function NotebooksList() {
onOpenChange={(open) => {
if (!open) setSummaryNotebook(null)
}}
notebookId={summaryNotebook?.id}
notebookId={summaryNotebook?.id ?? null}
notebookName={summaryNotebook?.name}
/>
</>

View File

@@ -14,7 +14,7 @@ export function NoteRefreshProvider({ children }: { children: React.ReactNode })
const triggerRefresh = useCallback(() => {
setRefreshKey(prev => prev + 1)
}, [refreshKey])
}, [])
return (
<NoteRefreshContext.Provider value={{ refreshKey, triggerRefresh }}>

View File

@@ -40,7 +40,7 @@ export interface NotebooksContextValue {
error: string | null
// Actions: Notebooks
createNotebookOptimistic: (data: CreateNotebookInput) => Promise<Notebook>
createNotebookOptimistic: (data: CreateNotebookInput) => Promise<void>
updateNotebook: (notebookId: string, data: UpdateNotebookInput) => Promise<void>
deleteNotebook: (notebookId: string) => Promise<void>
updateNotebookOrderOptimistic: (notebookIds: string[]) => Promise<void>
@@ -114,7 +114,6 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
// ===== ACTIONS: NOTEBOOKS =====
const createNotebookOptimistic = useCallback(async (data: CreateNotebookInput) => {
// Server action sera implémenté plus tard
const response = await fetch('/api/notebooks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -125,9 +124,10 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
throw new Error('Failed to create notebook')
}
const result = await response.json()
return result
}, [])
// Reload notebooks from server to update sidebar state
await loadNotebooks()
triggerRefresh()
}, [loadNotebooks, triggerRefresh])
const updateNotebook = useCallback(async (notebookId: string, data: UpdateNotebookInput) => {
const response = await fetch(`/api/notebooks/${notebookId}`, {