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() { )}
{/* Unsaved Changes Dialog */} - + { + if (!open) handleCancelLeave(); + }}> Unsaved Changes @@ -420,7 +481,7 @@ export default function EditorPage() { - Cancel + Cancel OK diff --git a/surfsense_web/atoms/editor/ui.atoms.ts b/surfsense_web/atoms/editor/ui.atoms.ts new file mode 100644 index 000000000..81a89a945 --- /dev/null +++ b/surfsense_web/atoms/editor/ui.atoms.ts @@ -0,0 +1,27 @@ +import { atom } from "jotai"; + +interface EditorUIState { + hasUnsavedChanges: boolean; + pendingNavigation: string | null; // URL to navigate to after user confirms +} + +export const editorUIAtom = atom({ + hasUnsavedChanges: false, + pendingNavigation: null, +}); + +// Derived atom for just the unsaved changes state +export const hasUnsavedEditorChangesAtom = atom( + (get) => get(editorUIAtom).hasUnsavedChanges, + (get, set, value: boolean) => { + set(editorUIAtom, { ...get(editorUIAtom), hasUnsavedChanges: value }); + } +); + +// Derived atom for pending navigation +export const pendingEditorNavigationAtom = atom( + (get) => get(editorUIAtom).pendingNavigation, + (get, set, value: string | null) => { + set(editorUIAtom, { ...get(editorUIAtom), pendingNavigation: value }); + } +); diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index 393d86b74..0e4ca0460 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -9,6 +9,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms"; import { chatsAtom } from "@/atoms/chats/chat-query.atoms"; import { globalChatsQueryParamsAtom } from "@/atoms/chats/ui.atoms"; +import { hasUnsavedEditorChangesAtom, pendingEditorNavigationAtom } from "@/atoms/editor/ui.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { AppSidebar } from "@/components/sidebar/app-sidebar"; import { Button } from "@/components/ui/button"; @@ -55,6 +56,10 @@ export function AppSidebarProvider({ const { data: chats, error: chatError, isLoading: isLoadingChats } = useAtomValue(chatsAtom); const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] = useAtom(deleteChatMutationAtom); + + // Editor state for handling unsaved changes + const hasUnsavedEditorChanges = useAtomValue(hasUnsavedEditorChangesAtom); + const setPendingNavigation = useSetAtom(pendingEditorNavigationAtom); useEffect(() => { setChatsQueryParams((prev) => ({ ...prev, search_space_id: searchSpaceId, skip: 0, limit: 4 })); @@ -233,10 +238,18 @@ export function AppSidebarProvider({ })); }, [notesData]); - // Handle add note + // Handle add note - check for unsaved changes first const handleAddNote = useCallback(() => { - router.push(`/dashboard/${searchSpaceId}/editor/new`); - }, [router, searchSpaceId]); + const newNoteUrl = `/dashboard/${searchSpaceId}/editor/new`; + + if (hasUnsavedEditorChanges) { + // Set pending navigation - the editor will show the unsaved changes dialog + setPendingNavigation(newNoteUrl); + } else { + // No unsaved changes, navigate directly + router.push(newNoteUrl); + } + }, [router, searchSpaceId, hasUnsavedEditorChanges, setPendingNavigation]); // Memoized updated navSecondary const updatedNavSecondary = useMemo(() => { diff --git a/surfsense_web/contracts/types/chat.types.ts b/surfsense_web/contracts/types/chat.types.ts index 0b8b5975c..a4e5b8f5b 100644 --- a/surfsense_web/contracts/types/chat.types.ts +++ b/surfsense_web/contracts/types/chat.types.ts @@ -14,7 +14,7 @@ export const chatSummary = z.object({ }); export const chatDetails = chatSummary.extend({ - initial_connectors: z.array(z.string()), + initial_connectors: z.array(z.string()).nullable().optional(), messages: z.array(z.any()), });