feat: fixed issues of note management

Issues Fixed

- Missing pagination fields in API response schemas (page, page_size, has_more)
- NOTE enum missing from frontend Zod schema
- Missing fields in DocumentRead response construction (content_hash, updated_at)
- BlockNote slash menu clipped by overflow-hidden CSS
- Sidebar click conflicts - hidden action buttons intercepting clicks
- Rewrote All Notes sidebar - replaced fragile custom portal with shadcn Sheet
- Missing translation keys for new UI strings
- Missing NOTE retrieval logic in researcher agent
- Added search to All Notes sidebar
- Removed frontend logging - was causing toasters on every page refresh
- Added backend logging to document reindex Celery task
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-12-17 00:09:43 -08:00
parent 3c3527d498
commit c768730b8c
37 changed files with 758 additions and 740 deletions

View file

@ -2,19 +2,18 @@
import {
ChevronRight,
ExternalLink,
Eye,
FileText,
FolderOpen,
Loader2,
type LucideIcon,
MoreHorizontal,
Plus,
RefreshCw,
Share,
Trash2,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useState, useRef } from "react";
import { useCallback, useState } from "react";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
DropdownMenu,
@ -27,23 +26,12 @@ import {
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
import { AllNotesSidebar } from "./all-notes-sidebar";
// Map of icon names to their components
const actionIconMap: Record<string, LucideIcon> = {
ExternalLink,
FileText,
Share,
Trash2,
MoreHorizontal,
RefreshCw,
};
interface NoteAction {
name: string;
icon: string;
@ -66,14 +54,19 @@ interface NavNotesProps {
searchSpaceId?: string;
}
// Map of icon names to their components
const actionIconMap: Record<string, LucideIcon> = {
FileText,
Trash2,
MoreHorizontal,
};
export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }: NavNotesProps) {
const t = useTranslations("sidebar");
const { isMobile } = useSidebar();
const router = useRouter();
const [isDeleting, setIsDeleting] = useState<number | null>(null);
const [isOpen, setIsOpen] = useState(defaultOpen);
const [isAllNotesSidebarOpen, setIsAllNotesSidebarOpen] = useState(false);
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Handle note deletion with loading state
const handleDeleteNote = useCallback(async (noteId: number, deleteAction: () => void) => {
@ -85,132 +78,148 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }
}
}, []);
// Enhanced note item component
const NoteItemComponent = useCallback(
({ note }: { note: NoteItem }) => {
const isDeletingNote = isDeleting === note.id;
return (
<SidebarMenuItem key={note.id ? `note-${note.id}` : `note-${note.name}`}>
<SidebarMenuButton
onClick={() => router.push(note.url)}
disabled={isDeletingNote}
className={`group/item relative ${isDeletingNote ? "opacity-50" : ""}`}
>
<note.icon className="h-4 w-4 shrink-0" />
<span className={`truncate ${isDeletingNote ? "opacity-50" : ""}`}>{note.name}</span>
</SidebarMenuButton>
{note.actions && note.actions.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction
showOnHover
className="opacity-0 group-hover/item:opacity-100 transition-opacity"
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
{note.actions.map((action, actionIndex) => {
const ActionIcon = actionIconMap[action.icon] || FileText;
const isDeleteAction = action.name.toLowerCase().includes("delete");
return (
<DropdownMenuItem
key={`${action.name}-${actionIndex}`}
onClick={() => {
if (isDeleteAction) {
handleDeleteNote(note.id || 0, action.onClick);
} else {
action.onClick();
}
}}
disabled={isDeletingNote}
className={isDeleteAction ? "text-destructive" : ""}
>
<ActionIcon className="mr-2 h-4 w-4" />
<span>{isDeletingNote && isDeleteAction ? "Deleting..." : action.name}</span>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
)}
</SidebarMenuItem>
);
// Handle note navigation
const handleNoteClick = useCallback(
(url: string) => {
router.push(url);
},
[isDeleting, router, isMobile, handleDeleteNote]
[router]
);
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden relative">
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<div className="flex items-center group/header relative">
<div className="flex items-center group/header">
<CollapsibleTrigger asChild>
<SidebarGroupLabel className="cursor-pointer rounded-md px-2 py-1.5 -mx-2 transition-colors flex items-center gap-1.5">
<SidebarGroupLabel className="cursor-pointer rounded-md px-2 py-1.5 -mx-2 transition-colors flex items-center gap-1.5 flex-1">
<ChevronRight
className={`h-3.5 w-3.5 text-muted-foreground transition-all duration-200 shrink-0 hover:text-sidebar-foreground ${
isOpen ? "rotate-90" : ""
}`}
className={cn(
"h-3.5 w-3.5 text-muted-foreground transition-all duration-200 shrink-0",
isOpen && "rotate-90"
)}
/>
<span>{t("notes") || "Notes"}</span>
</SidebarGroupLabel>
</CollapsibleTrigger>
<div className="absolute top-1.5 right-1 flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity">
{/* Action buttons - always visible on hover */}
<div className="flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity pr-1">
{searchSpaceId && notes.length > 0 && (
<button
type="button"
onMouseEnter={(e) => {
<Button
variant="ghost"
size="icon"
className="h-5 w-5"
onClick={(e) => {
e.stopPropagation();
// Clear any pending close timeout
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = null;
}
setIsAllNotesSidebarOpen(true);
}}
onMouseLeave={(e) => {
e.stopPropagation();
// Add a small delay before closing to allow moving to the sidebar
hoverTimeoutRef.current = setTimeout(() => {
setIsAllNotesSidebarOpen(false);
}, 200);
}}
aria-label="View all notes"
className="text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden relative"
>
<Eye className="h-4 w-4" />
</button>
<FolderOpen className="h-3.5 w-3.5" />
</Button>
)}
{onAddNote && (
<button
type="button"
<Button
variant="ghost"
size="icon"
className="h-5 w-5"
onClick={(e) => {
e.stopPropagation();
onAddNote();
}}
aria-label="Add note"
className="text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden relative"
>
<Plus className="h-4 w-4" />
</button>
<Plus className="h-3.5 w-3.5" />
</Button>
)}
</div>
</div>
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{/* Note Items */}
{notes.length > 0 ? (
notes.map((note) => <NoteItemComponent key={note.id || note.name} note={note} />)
notes.map((note) => {
const isDeletingNote = isDeleting === note.id;
return (
<SidebarMenuItem key={note.id || note.name} className="group/note">
{/* Main navigation button */}
<SidebarMenuButton
onClick={() => handleNoteClick(note.url)}
disabled={isDeletingNote}
className={cn(
"pr-8", // Make room for the action button
isDeletingNote && "opacity-50"
)}
>
<note.icon className="h-4 w-4 shrink-0" />
<span className="truncate">{note.name}</span>
</SidebarMenuButton>
{/* Actions dropdown - positioned absolutely */}
{note.actions && note.actions.length > 0 && (
<div className="absolute right-1 top-1/2 -translate-y-1/2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"h-6 w-6",
"opacity-0 group-hover/note:opacity-100 focus:opacity-100",
"data-[state=open]:opacity-100",
"transition-opacity"
)}
disabled={isDeletingNote}
>
{isDeletingNote ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<MoreHorizontal className="h-3.5 w-3.5" />
)}
<span className="sr-only">More options</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="right" className="w-40">
{note.actions.map((action, actionIndex) => {
const ActionIcon = actionIconMap[action.icon] || FileText;
const isDeleteAction = action.name.toLowerCase().includes("delete");
return (
<DropdownMenuItem
key={`${action.name}-${actionIndex}`}
onClick={() => {
if (isDeleteAction) {
handleDeleteNote(note.id || 0, action.onClick);
} else {
action.onClick();
}
}}
disabled={isDeletingNote}
className={
isDeleteAction
? "text-destructive focus:text-destructive"
: ""
}
>
<ActionIcon className="mr-2 h-4 w-4" />
<span>
{isDeletingNote && isDeleteAction
? "Deleting..."
: action.name}
</span>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</SidebarMenuItem>
);
})
) : (
/* Empty state with create button */
<SidebarMenuItem>
{onAddNote ? (
<SidebarMenuButton
@ -232,13 +241,14 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }
</SidebarGroupContent>
</CollapsibleContent>
</Collapsible>
{/* All Notes Sheet */}
{searchSpaceId && (
<AllNotesSidebar
open={isAllNotesSidebarOpen}
onOpenChange={setIsAllNotesSidebarOpen}
searchSpaceId={searchSpaceId}
onAddNote={onAddNote}
hoverTimeoutRef={hoverTimeoutRef}
/>
)}
</SidebarGroup>