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

@@ -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}
/>
</>