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>
This commit is contained in:
Copilot
2026-05-21 20:15:54 -04:00
committed by GitHub
parent b639841555
commit efc988fb9f
2 changed files with 73 additions and 9 deletions
@@ -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);
});
});
+42 -9
View File
@@ -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()) {