From b53b19170e0a7e174d3d70f42d2c4851754b642d Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 19 Dec 2025 22:59:42 +0530 Subject: [PATCH] feat: implement unsaved changes handling in editor and sidebar - Introduced global state management for unsaved changes and pending navigation using Jotai atoms. - Updated the editor component to sync local unsaved changes with global state and handle navigation prompts. - Enhanced sidebar functionality to check for unsaved changes before navigating to a new note. - Added cleanup logic for global state on component unmount to prevent stale data. --- .../editor/[documentId]/page.tsx | 67 ++++++++++++++++++- surfsense_web/atoms/editor/ui.atoms.ts | 27 ++++++++ .../components/sidebar/AppSidebarProvider.tsx | 19 +++++- surfsense_web/contracts/types/chat.types.ts | 2 +- 4 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 surfsense_web/atoms/editor/ui.atoms.ts diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx index 4888faceb..e0984801e 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx @@ -1,11 +1,13 @@ "use client"; import { useQueryClient } from "@tanstack/react-query"; +import { useAtom } from "jotai"; 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 { toast } from "sonner"; +import { hasUnsavedEditorChangesAtom, pendingEditorNavigationAtom } from "@/atoms/editor/ui.atoms"; import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor"; import { AlertDialog, @@ -76,6 +78,46 @@ export default function EditorPage() { const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [showUnsavedDialog, setShowUnsavedDialog] = useState(false); + // Global state for cross-component communication + const [, setGlobalHasUnsavedChanges] = useAtom(hasUnsavedEditorChangesAtom); + const [pendingNavigation, setPendingNavigation] = useAtom(pendingEditorNavigationAtom); + + // Sync local unsaved changes state with global atom + useEffect(() => { + setGlobalHasUnsavedChanges(hasUnsavedChanges); + }, [hasUnsavedChanges, setGlobalHasUnsavedChanges]); + + // Cleanup global state when component unmounts + useEffect(() => { + return () => { + setGlobalHasUnsavedChanges(false); + setPendingNavigation(null); + }; + }, [setGlobalHasUnsavedChanges, setPendingNavigation]); + + // Handle pending navigation from sidebar (e.g., when user clicks "+" to create new note) + useEffect(() => { + if (pendingNavigation) { + if (hasUnsavedChanges) { + // Show dialog to confirm navigation + setShowUnsavedDialog(true); + } else { + // No unsaved changes, navigate immediately + router.push(pendingNavigation); + setPendingNavigation(null); + } + } + }, [pendingNavigation, hasUnsavedChanges, router, setPendingNavigation]); + + // Reset state when documentId changes (e.g., navigating from existing note to new note) + useEffect(() => { + setDocument(null); + setEditorContent(null); + setError(null); + setHasUnsavedChanges(false); + setLoading(true); + }, [documentId]); + // Fetch document content - DIRECT CALL TO FASTAPI // Skip fetching if this is a new note useEffect(() => { @@ -287,7 +329,23 @@ export default function EditorPage() { const handleConfirmLeave = () => { setShowUnsavedDialog(false); - router.push(`/dashboard/${searchSpaceId}/researcher`); + // Clear global unsaved state + setGlobalHasUnsavedChanges(false); + setHasUnsavedChanges(false); + + // If there's a pending navigation (from sidebar), use that; otherwise go back to researcher + if (pendingNavigation) { + router.push(pendingNavigation); + setPendingNavigation(null); + } else { + router.push(`/dashboard/${searchSpaceId}/researcher`); + } + }; + + const handleCancelLeave = () => { + setShowUnsavedDialog(false); + // Clear pending navigation if user cancels + setPendingNavigation(null); }; if (loading) { @@ -402,6 +460,7 @@ export default function EditorPage() { )}