Skip to content

Commit 2453627

Browse files
committed
WEB-109 Replace sentinel-based history hack with history.state.idx reference, fix regression on history state replacement on save, simplify API
1 parent d167d19 commit 2453627

4 files changed

Lines changed: 35 additions & 54 deletions

File tree

src/app/editor/[id]/[slug]/SaveButton.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { createUsePuck } from "@puckeditor/core";
44
import { useTransition } from "react";
55
import { toast } from "sonner";
66
import { useDocumentContext } from "./document-context";
7-
import { useUnsavedChangesContext } from "./unsaved-changes-context";
87
import { saveVersionAction } from "../../../../lib/documents/actions";
98
import { getEditorUrl, getPreviewUrl } from "../../../../lib/editor-url";
109
import { runAction } from "../../runAction";
@@ -15,7 +14,6 @@ const usePuck = createUsePuck();
1514
export function SaveButton() {
1615
const data = usePuck((s) => s.appState.data);
1716
const { documentId, documentName, isArchived, isDirty, addVersion } = useDocumentContext();
18-
const { clearUnsavedChangesGuard } = useUnsavedChangesContext();
1917
const { alert } = useDialogs();
2018

2119
const [isSaving, startTransition] = useTransition();
@@ -29,9 +27,8 @@ export function SaveButton() {
2927
return;
3028
}
3129

32-
await clearUnsavedChangesGuard();
3330
addVersion(result.data.version);
34-
window.history.replaceState(null, "", getEditorUrl(documentId, documentName, result.data.version.id));
31+
window.history.replaceState(window.history.state, "", getEditorUrl(documentId, documentName, result.data.version.id));
3532

3633
const previewUrl = getPreviewUrl(documentId, documentName, result.data.version.id);
3734
toast.success("Saved", {

src/app/editor/[id]/[slug]/client.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function Client({
3636
const [versionId, setVersionId] = useState(initialVersionId);
3737
const [publishedVersionId, setPublishedVersionId] = useState(initialPublishedVersionId);
3838
const [isDirty, setIsDirty] = useState(false);
39-
const { confirmDiscardChanges, clearUnsavedChangesGuard } = useUnsavedChangesGuard(isDirty);
39+
const { confirmDiscardChanges } = useUnsavedChangesGuard(isDirty);
4040

4141
const addVersion = useCallback((version: Version) => {
4242
setVersions(prev => [version, ...prev]);
@@ -49,7 +49,7 @@ export function Client({
4949
<DocumentContext.Provider
5050
value={{ documentId, documentName, versionId, publishedVersionId, versions, isArchived, isDirty, addVersion, setPublishedVersionId }}
5151
>
52-
<UnsavedChangesContext.Provider value={{ confirmDiscardChanges, clearUnsavedChangesGuard }}>
52+
<UnsavedChangesContext.Provider value={{ confirmDiscardChanges }}>
5353
<Puck
5454
config={config}
5555
data={data}

src/app/editor/[id]/[slug]/unsaved-changes-context.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import { createContext, useContext } from "react";
44

55
type UnsavedChangesContextType = {
66
confirmDiscardChanges: () => Promise<boolean>;
7-
clearUnsavedChangesGuard: () => Promise<void>;
87
};
98

109
const UnsavedChangesContext = createContext<UnsavedChangesContextType>({
1110
confirmDiscardChanges: async () => true,
12-
clearUnsavedChangesGuard: async () => {},
1311
});
1412

1513
export function useUnsavedChangesContext() {

src/app/editor/[id]/[slug]/useUnsavedChangesGuard.ts

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,15 @@ import { useDialogs } from "@/components/ui/dialog-provider";
55

66
const LEAVE_MESSAGE = "You have unsaved changes. Leave this page without saving?";
77

8+
function getCurrentHistoryIndex() {
9+
const historyState = window.history.state as { idx?: number } | null;
10+
return typeof historyState?.idx === "number" ? historyState.idx : null;
11+
}
12+
813
export function useUnsavedChangesGuard(isDirty: boolean) {
914
const { confirm } = useDialogs();
10-
const isHistoryGuardActiveRef = useRef(false);
11-
const skipNextPopStateRef = useRef(false);
12-
const pendingNavigationRef = useRef<(() => void) | null>(null);
13-
14-
const clearUnsavedChangesGuard = useCallback(() => {
15-
if (!isHistoryGuardActiveRef.current) {
16-
return Promise.resolve();
17-
}
18-
19-
isHistoryGuardActiveRef.current = false;
20-
21-
return new Promise<void>((resolve) => {
22-
pendingNavigationRef.current = resolve;
23-
skipNextPopStateRef.current = true;
24-
window.history.back();
25-
});
26-
}, []);
15+
const historyIndexRef = useRef<number | null>(null);
16+
const isRevertingNavigationRef = useRef(false);
2717

2818
const confirmDiscardChanges = useCallback(async () => {
2919
if (!isDirty) {
@@ -40,63 +30,59 @@ export function useUnsavedChangesGuard(isDirty: boolean) {
4030
return false;
4131
}
4232

43-
await clearUnsavedChangesGuard();
4433
return true;
45-
}, [clearUnsavedChangesGuard, confirm, isDirty]);
34+
}, [confirm, isDirty]);
4635

4736
useEffect(() => {
4837
if (!isDirty) {
4938
return;
5039
}
5140

41+
historyIndexRef.current = getCurrentHistoryIndex();
42+
isRevertingNavigationRef.current = false;
43+
5244
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
5345
event.preventDefault();
5446
// Needed for Chrome version <=119 to show the confirmation dialog
5547
event.returnValue = true;
5648
};
5749

5850
const handlePopState = () => {
59-
if (skipNextPopStateRef.current) {
60-
skipNextPopStateRef.current = false;
61-
pendingNavigationRef.current?.();
62-
pendingNavigationRef.current = null;
63-
return;
51+
const nextHistoryIndex = getCurrentHistoryIndex();
52+
53+
if (!isRevertingNavigationRef.current) {
54+
const shouldLeave = window.confirm(LEAVE_MESSAGE);
55+
56+
if (!shouldLeave) {
57+
if (
58+
historyIndexRef.current !== null &&
59+
nextHistoryIndex !== null &&
60+
historyIndexRef.current !== nextHistoryIndex
61+
) {
62+
isRevertingNavigationRef.current = true;
63+
window.history.go(historyIndexRef.current > nextHistoryIndex ? 1 : -1);
64+
return;
65+
}
66+
67+
console.warn("Unable to determine history direction for unsaved-changes guard");
68+
}
6469
}
6570

66-
if (!isHistoryGuardActiveRef.current) {
67-
return;
68-
}
69-
70-
const shouldLeave = window.confirm(LEAVE_MESSAGE);
71-
72-
if (!shouldLeave) {
73-
window.history.pushState(window.history.state, "", window.location.href);
74-
isHistoryGuardActiveRef.current = true;
75-
return;
76-
}
77-
78-
isHistoryGuardActiveRef.current = false;
79-
skipNextPopStateRef.current = true;
80-
window.history.back();
71+
isRevertingNavigationRef.current = false;
72+
historyIndexRef.current = nextHistoryIndex;
8173
};
8274

83-
if (!isHistoryGuardActiveRef.current) {
84-
window.history.pushState(window.history.state, "", window.location.href);
85-
isHistoryGuardActiveRef.current = true;
86-
}
87-
8875
window.addEventListener("beforeunload", handleBeforeUnload);
8976
window.addEventListener("popstate", handlePopState);
9077

9178
return () => {
92-
isHistoryGuardActiveRef.current = false;
79+
isRevertingNavigationRef.current = false;
9380
window.removeEventListener("beforeunload", handleBeforeUnload);
9481
window.removeEventListener("popstate", handlePopState);
9582
};
9683
}, [isDirty]);
9784

9885
return {
99-
clearUnsavedChangesGuard,
10086
confirmDiscardChanges,
10187
};
10288
}

0 commit comments

Comments
 (0)