From efc988fb9fcc217e399c17824ff9384887bf1f1d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 20:15:54 -0400 Subject: [PATCH] Prevent unintentional trashing of non-empty untitled drafts on editor unmount (#12418) * Initial plan * Fix draft auto-delete check for non-empty untitled docs --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../Document/hooks/useDocumentSave.test.ts | 31 +++++++++++ app/scenes/Document/hooks/useDocumentSave.ts | 51 +++++++++++++++---- 2 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 app/scenes/Document/hooks/useDocumentSave.test.ts diff --git a/app/scenes/Document/hooks/useDocumentSave.test.ts b/app/scenes/Document/hooks/useDocumentSave.test.ts new file mode 100644 index 0000000000..e03d2a7bac --- /dev/null +++ b/app/scenes/Document/hooks/useDocumentSave.test.ts @@ -0,0 +1,31 @@ +import { shouldAutoDeleteDraftOnUnmount } from "./useDocumentSave"; + +describe("shouldAutoDeleteDraftOnUnmount", () => { + const baseOptions = { + title: "", + createdById: "user-1", + currentUserId: "user-1", + isDraft: true, + isActive: true, + hasEmptyTitle: true, + isPersistedOnce: true, + }; + + it("does not auto delete drafts with non-empty editor content", () => { + expect( + shouldAutoDeleteDraftOnUnmount({ + ...baseOptions, + isEditorEmpty: false, + }) + ).toBe(false); + }); + + it("auto deletes drafts that are still empty and untitled", () => { + expect( + shouldAutoDeleteDraftOnUnmount({ + ...baseOptions, + isEditorEmpty: true, + }) + ).toBe(true); + }); +}); diff --git a/app/scenes/Document/hooks/useDocumentSave.ts b/app/scenes/Document/hooks/useDocumentSave.ts index ccd7db3097..54b3f3e8a5 100644 --- a/app/scenes/Document/hooks/useDocumentSave.ts +++ b/app/scenes/Document/hooks/useDocumentSave.ts @@ -50,6 +50,36 @@ interface UseDocumentSaveResult { onFileUploadStop: () => void; } +export function shouldAutoDeleteDraftOnUnmount({ + isEditorEmpty, + title, + createdById, + currentUserId, + isDraft, + isActive, + hasEmptyTitle, + isPersistedOnce, +}: { + isEditorEmpty: boolean; + title: string; + createdById?: string; + currentUserId?: string; + isDraft: boolean; + isActive: boolean; + hasEmptyTitle: boolean; + isPersistedOnce: boolean; +}) { + return ( + isEditorEmpty && + title.trim() === "" && + createdById === currentUserId && + isDraft && + isActive && + hasEmptyTitle && + isPersistedOnce + ); +} + /** * Hook that encapsulates save, autosave, dirty-tracking, and template * insertion logic for the document editor scene. @@ -77,8 +107,6 @@ export function useDocumentSave({ // Companion refs for stale closure avoidance const isEditorDirtyRef = useRef(isEditorDirty); isEditorDirtyRef.current = isEditorDirty; - const isEmptyRef = useRef(isEmpty); - isEmptyRef.current = isEmpty; const titleRef = useRef(title); titleRef.current = title; @@ -89,7 +117,6 @@ export function useDocumentSave({ isEditorDirtyRef.current = dirty; const empty = (!doc || ProsemirrorHelper.isEmpty(doc)) && !titleRef.current; setIsEmpty(empty); - isEmptyRef.current = empty; }, [document, editorRef]); const updateIsDirtyRef = useRef(updateIsDirty); @@ -313,14 +340,20 @@ export function useDocumentSave({ useEffect( () => () => { autosave.cancel(); + const currentDoc = editorRef.current?.view.state.doc; + const isEditorEmpty = !currentDoc || ProsemirrorHelper.isEmpty(currentDoc); if ( - isEmptyRef.current && - document.createdBy?.id === auth.user?.id && - document.isDraft && - document.isActive && - document.hasEmptyTitle && - document.isPersistedOnce + shouldAutoDeleteDraftOnUnmount({ + isEditorEmpty, + title: titleRef.current, + createdById: document.createdBy?.id, + currentUserId: auth.user?.id, + isDraft: document.isDraft, + isActive: document.isActive, + hasEmptyTitle: document.hasEmptyTitle, + isPersistedOnce: document.isPersistedOnce, + }) ) { void document.delete(); } else if (document.isDirty()) {