mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
feat: enhance sidebar and editor functionality
- Updated AllNotesSidebar to render via portal for improved stacking context. - Refactored sidebar styles and behavior for better user experience. - Modified AppSidebarProvider to limit recent notes to 5 and sort by updated date. - Improved editor page to handle document state updates and query invalidation on note creation. - Added loading messages to translations for better user feedback during operations.
This commit is contained in:
parent
5da41d91c8
commit
82d8320928
8 changed files with 139 additions and 118 deletions
|
|
@ -150,7 +150,7 @@ export function DocumentsTableShell({
|
|||
<>
|
||||
<div className="hidden md:block max-h-[60vh] overflow-auto">
|
||||
<Table className="table-fixed w-full">
|
||||
<TableHeader className="sticky top-0 bg-background z-10">
|
||||
<TableHeader className="sticky top-0 bg-background">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead style={{ width: 28 }}>
|
||||
<Checkbox
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { AlertCircle, ArrowLeft, FileText, Loader2, Plus, SquarePen, Save, X } from "lucide-react";
|
||||
import { AlertCircle, ArrowLeft, FileText, Loader2, Save } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor";
|
||||
import {
|
||||
|
|
@ -62,6 +63,7 @@ function extractTitleFromBlockNote(blocknoteDocument: any[] | null | undefined):
|
|||
export default function EditorPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const documentId = params.documentId as string;
|
||||
const searchSpaceId = Number(params.search_space_id);
|
||||
const isNewNote = documentId === "new";
|
||||
|
|
@ -209,10 +211,25 @@ export default function EditorPage() {
|
|||
setHasUnsavedChanges(false);
|
||||
toast.success("Note created successfully! Reindexing in background...");
|
||||
|
||||
// Redirect to editor with the new document ID
|
||||
setTimeout(() => {
|
||||
router.push(`/dashboard/${searchSpaceId}/editor/${note.id}`);
|
||||
}, 500);
|
||||
// Invalidate notes query to refresh the sidebar
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["notes", String(searchSpaceId)],
|
||||
});
|
||||
|
||||
// Update URL to reflect the new document ID without navigation
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"",
|
||||
`/dashboard/${searchSpaceId}/editor/${note.id}`
|
||||
);
|
||||
// Update document state to reflect the new ID
|
||||
setDocument({
|
||||
document_id: note.id,
|
||||
title: title,
|
||||
document_type: "NOTE",
|
||||
blocknote_document: editorContent,
|
||||
updated_at: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
// Existing document - save normally
|
||||
if (!editorContent) {
|
||||
|
|
@ -241,10 +258,12 @@ export default function EditorPage() {
|
|||
setHasUnsavedChanges(false);
|
||||
toast.success("Document saved! Reindexing in background...");
|
||||
|
||||
// Small delay before redirect to show success message
|
||||
setTimeout(() => {
|
||||
router.push(`/dashboard/${params.search_space_id}/documents`);
|
||||
}, 500);
|
||||
// Invalidate notes query when updating notes to refresh the sidebar
|
||||
if (isNote) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["notes", String(searchSpaceId)],
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving document:", error);
|
||||
|
|
@ -265,13 +284,13 @@ export default function EditorPage() {
|
|||
if (hasUnsavedChanges) {
|
||||
setShowUnsavedDialog(true);
|
||||
} else {
|
||||
router.back();
|
||||
router.push(`/dashboard/${searchSpaceId}/researcher`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmLeave = () => {
|
||||
setShowUnsavedDialog(false);
|
||||
router.back();
|
||||
router.push(`/dashboard/${searchSpaceId}/researcher`);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
|
|
@ -304,9 +323,9 @@ export default function EditorPage() {
|
|||
<CardDescription>{error}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={() => router.back()} variant="outline" className="w-full">
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Go Back
|
||||
<Button onClick={() => router.push(`/dashboard/${searchSpaceId}/researcher`)} variant="outline" className="gap-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -356,17 +375,10 @@ export default function EditorPage() {
|
|||
{isNewNote ? "Creating..." : "Saving..."}
|
||||
</>
|
||||
) : (
|
||||
isNewNote ? (
|
||||
<>
|
||||
<SquarePen className="h-4 w-4" />
|
||||
Create Note
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4" />
|
||||
Save & Exit
|
||||
</>
|
||||
)
|
||||
<>
|
||||
<Save className="h-4 w-4" />
|
||||
Save
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export function AppSidebarProvider({
|
|||
queryFn: () =>
|
||||
notesApiService.getNotes({
|
||||
search_space_id: Number(searchSpaceId),
|
||||
page_size: 10, // Get recent 10 notes
|
||||
page_size: 5, // Get 5 notes (changed from 10)
|
||||
}),
|
||||
enabled: !!searchSpaceId,
|
||||
});
|
||||
|
|
@ -183,32 +183,40 @@ export function AppSidebarProvider({
|
|||
|
||||
// Transform notes to the format expected by NavNotes
|
||||
const recentNotes = useMemo(() => {
|
||||
return notesData?.items
|
||||
? notesData.items.map((note) => ({
|
||||
name: note.title,
|
||||
url: `/dashboard/${note.search_space_id}/editor/${note.id}`,
|
||||
icon: "FileText",
|
||||
id: note.id,
|
||||
search_space_id: note.search_space_id,
|
||||
actions: [
|
||||
{
|
||||
name: "Delete",
|
||||
icon: "Trash2",
|
||||
onClick: async () => {
|
||||
try {
|
||||
await notesApiService.deleteNote({
|
||||
search_space_id: note.search_space_id,
|
||||
note_id: note.id,
|
||||
});
|
||||
refetchNotes();
|
||||
} catch (error) {
|
||||
console.error("Error deleting note:", error);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
: [];
|
||||
if (!notesData?.items) return [];
|
||||
|
||||
// Sort notes by updated_at (most recent first), fallback to created_at if updated_at is null
|
||||
const sortedNotes = [...notesData.items].sort((a, b) => {
|
||||
const dateA = a.updated_at ? new Date(a.updated_at).getTime() : new Date(a.created_at).getTime();
|
||||
const dateB = b.updated_at ? new Date(b.updated_at).getTime() : new Date(b.created_at).getTime();
|
||||
return dateB - dateA; // Descending order (most recent first)
|
||||
});
|
||||
|
||||
// Limit to 5 notes
|
||||
return sortedNotes.slice(0, 5).map((note) => ({
|
||||
name: note.title,
|
||||
url: `/dashboard/${note.search_space_id}/editor/${note.id}`,
|
||||
icon: "FileText",
|
||||
id: note.id,
|
||||
search_space_id: note.search_space_id,
|
||||
actions: [
|
||||
{
|
||||
name: "Delete",
|
||||
icon: "Trash2",
|
||||
onClick: async () => {
|
||||
try {
|
||||
await notesApiService.deleteNote({
|
||||
search_space_id: note.search_space_id,
|
||||
note_id: note.id,
|
||||
});
|
||||
refetchNotes();
|
||||
} catch (error) {
|
||||
console.error("Error deleting note:", error);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
}, [notesData, refetchNotes]);
|
||||
|
||||
// Handle add note
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
// Map of icon names to their components
|
||||
const actionIconMap: Record<string, LucideIcon> = {
|
||||
|
|
@ -284,42 +285,40 @@ export function AllNotesSidebar({
|
|||
[isDeleting, router, onOpenChange, handleDeleteNote]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating Sidebar */}
|
||||
<section
|
||||
ref={sidebarRef}
|
||||
aria-label="All notes sidebar"
|
||||
className={cn(
|
||||
"fixed top-16 bottom-0 z-[60] w-80 bg-sidebar text-sidebar-foreground shadow-2xl",
|
||||
"transition-all duration-300 ease-out",
|
||||
!open && "pointer-events-none"
|
||||
)}
|
||||
style={{
|
||||
// Position it to slide from the right edge of the main sidebar
|
||||
left: `${sidebarLeft}px`,
|
||||
transform: open ? `scaleX(1)` : `scaleX(0)`,
|
||||
transformOrigin: "left",
|
||||
opacity: open ? 1 : 0,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
// Clear any pending close timeout when hovering over sidebar
|
||||
if (hoverTimeoutRef?.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
hoverTimeoutRef.current = null;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
// Close sidebar when mouse leaves
|
||||
if (hoverTimeoutRef) {
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
onOpenChange(false);
|
||||
}, 200);
|
||||
} else {
|
||||
const sidebarContent = (
|
||||
<section
|
||||
ref={sidebarRef}
|
||||
aria-label="All notes sidebar"
|
||||
className={cn(
|
||||
"fixed top-16 bottom-0 z-[100] w-80 bg-sidebar text-sidebar-foreground shadow-xl",
|
||||
"transition-all duration-300 ease-out",
|
||||
!open && "pointer-events-none"
|
||||
)}
|
||||
style={{
|
||||
// Position it to slide from the right edge of the main sidebar
|
||||
left: `${sidebarLeft}px`,
|
||||
transform: open ? `scaleX(1)` : `scaleX(0)`,
|
||||
transformOrigin: "left",
|
||||
opacity: open ? 1 : 0,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
// Clear any pending close timeout when hovering over sidebar
|
||||
if (hoverTimeoutRef?.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
hoverTimeoutRef.current = null;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
// Close sidebar when mouse leaves
|
||||
if (hoverTimeoutRef) {
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
onOpenChange(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
}, 200);
|
||||
} else {
|
||||
onOpenChange(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex h-16 shrink-0 items-center justify-between px-4">
|
||||
|
|
@ -348,13 +347,13 @@ export function AllNotesSidebar({
|
|||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
) : allNotes.length > 0 ? (
|
||||
<SidebarMenu>
|
||||
<SidebarMenu className="list-none">
|
||||
{allNotes.map((note) => (
|
||||
<NoteItemComponent key={note.id || note.name} note={note} />
|
||||
))}
|
||||
</SidebarMenu>
|
||||
) : (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuItem className="list-none">
|
||||
{onAddNote ? (
|
||||
<SidebarMenuButton
|
||||
onClick={() => {
|
||||
|
|
@ -398,7 +397,13 @@ export function AllNotesSidebar({
|
|||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
// Render sidebar via portal to avoid stacking context issues
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(sidebarContent, document.body);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }
|
|||
</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">
|
||||
{searchSpaceId && (
|
||||
{searchSpaceId && notes.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onMouseEnter={(e) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { ChevronRight, Mail } from "lucide-react";
|
||||
import { Mail } from "lucide-react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
SidebarGroup,
|
||||
|
|
@ -8,7 +8,6 @@ import {
|
|||
SidebarGroupLabel,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
|
||||
interface PageUsageDisplayProps {
|
||||
pagesUsed: number;
|
||||
|
|
@ -22,16 +21,19 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
|
|||
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<Collapsible defaultOpen={false} className="group-data-[collapsible=icon]:hidden">
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarGroupLabel className="cursor-pointer hover:bg-sidebar-accent rounded-md px-2 py-1.5 -mx-2 transition-colors flex items-center justify-between group">
|
||||
<span>Page Usage</span>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-90" />
|
||||
</SidebarGroupLabel>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent>
|
||||
<div className="space-y-2 px-2 py-2">
|
||||
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">
|
||||
Page Usage
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<div className="space-y-2 px-2 py-2">
|
||||
{isCollapsed ? (
|
||||
// Show only a compact progress indicator when collapsed
|
||||
<div className="flex justify-center">
|
||||
<Progress value={usagePercentage} className="h-2 w-8" />
|
||||
</div>
|
||||
) : (
|
||||
// Show full details when expanded
|
||||
<>
|
||||
<div className="flex justify-between items-center text-xs">
|
||||
<span className="text-muted-foreground">
|
||||
{pagesUsed.toLocaleString()} / {pagesLimit.toLocaleString()} pages
|
||||
|
|
@ -52,18 +54,10 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
|
|||
to increase limits
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarGroupContent>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
{isCollapsed && (
|
||||
// Show only a compact progress indicator when sidebar is collapsed
|
||||
<SidebarGroupContent>
|
||||
<div className="flex justify-center px-2 py-2">
|
||||
<Progress value={usagePercentage} className="h-2 w-8" />
|
||||
</div>
|
||||
</SidebarGroupContent>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
|
|
@ -647,7 +647,8 @@
|
|||
"all_notes": "All Notes",
|
||||
"no_notes": "No notes yet",
|
||||
"create_new_note": "Create a new note",
|
||||
"error_loading_notes": "Error loading notes"
|
||||
"error_loading_notes": "Error loading notes",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"errors": {
|
||||
"something_went_wrong": "Something went wrong",
|
||||
|
|
|
|||
|
|
@ -647,7 +647,8 @@
|
|||
"all_notes": "所有笔记",
|
||||
"no_notes": "暂无笔记",
|
||||
"create_new_note": "创建新笔记",
|
||||
"error_loading_notes": "加载笔记时出错"
|
||||
"error_loading_notes": "加载笔记时出错",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"errors": {
|
||||
"something_went_wrong": "出错了",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue