0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-06 20:52:40 +00:00

Make autosaving work

debouncing does not play nice with functional component. Any debounced function gets locked in as the original state, meaning we keep saving the original document and overwriting the current document when a save fires.

Must pass in the parameters instead of pulling directly from state to work properly.
This commit is contained in:
Trevor Buckner
2025-09-09 01:57:13 -04:00
parent 90a81237ec
commit 90f6e7ec37

View File

@@ -1,7 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import './editPage.less'; import './editPage.less';
import React, { useState, useEffect, useRef, useMemo } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js'; import Markdown from 'naturalcrit/markdown.js';
@@ -35,16 +35,13 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers
import googleDriveIcon from '../../googleDrive.svg'; import googleDriveIcon from '../../googleDrive.svg';
const SAVE_TIMEOUT = 10000; const SAVE_TIMEOUT = 5000;
const EditPage = (props) => { const EditPage = (props) => {
props = { props = {
brew: DEFAULT_BREW_LOAD, brew: DEFAULT_BREW_LOAD,
...props ...props
}; };
const editorRef = useRef(null);
const savedBrew = useRef(_.cloneDeep(props.brew));
const warningTimer = useRef(null);
const [currentBrew , setCurrentBrew ] = useState(props.brew); const [currentBrew , setCurrentBrew ] = useState(props.brew);
const [isSaving , setIsSaving ] = useState(false); const [isSaving , setIsSaving ] = useState(false);
@@ -64,7 +61,10 @@ const EditPage = (props) => {
const [autoSaveWarning , setAutoSaveWarning ] = useState(false); const [autoSaveWarning , setAutoSaveWarning ] = useState(false);
const [unsavedTime , setUnsavedTime ] = useState(new Date()); const [unsavedTime , setUnsavedTime ] = useState(new Date());
const debounceSave = useMemo(() => _.debounce(() => trySave(), SAVE_TIMEOUT), []); const editorRef = useRef(null);
const savedBrew = useRef(_.cloneDeep(props.brew));
const warningTimer = useRef(null);
const debounceSave = useCallback(_.debounce((brew, saveToGoogle)=>save(brew, saveToGoogle), SAVE_TIMEOUT), []);
useEffect(() => { useEffect(() => {
setUrl(window.location.href); setUrl(window.location.href);
@@ -92,7 +92,7 @@ const EditPage = (props) => {
const hasChange = !_.isEqual(currentBrew, savedBrew.current); const hasChange = !_.isEqual(currentBrew, savedBrew.current);
setUnsavedChanges(hasChange); setUnsavedChanges(hasChange);
if(autoSaveEnabled) save(); if(hasChange && autoSaveEnabled) trySave();
}, [currentBrew]); }, [currentBrew]);
const handleControlKeys = (e) => { const handleControlKeys = (e) => {
@@ -130,12 +130,10 @@ const EditPage = (props) => {
setHTMLErrors(HTMLErrors); setHTMLErrors(HTMLErrors);
setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); setCurrentBrew((prevBrew) => ({ ...prevBrew, text }));
if (autoSaveEnabled) debounceSave();
}; };
const handleStyleChange = (style) => { const handleStyleChange = (style) => {
setCurrentBrew(prevBrew => ({ ...prevBrew, style })); setCurrentBrew(prevBrew => ({ ...prevBrew, style }));
if (autoSaveEnabled) debounceSave();
}; };
const handleSnipChange = (snippet)=>{ const handleSnipChange = (snippet)=>{
@@ -145,7 +143,6 @@ const EditPage = (props) => {
setHTMLErrors(HTMLErrors); setHTMLErrors(HTMLErrors);
setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet }));
if (autoSaveEnabled) debounceSave();
}; };
const handleMetaChange = (metadata, field = undefined) => { const handleMetaChange = (metadata, field = undefined) => {
@@ -153,7 +150,6 @@ const EditPage = (props) => {
fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme);
setCurrentBrew(prev => ({ ...prev, ...metadata })); setCurrentBrew(prev => ({ ...prev, ...metadata }));
if (autoSaveEnabled) debounceSave();
}; };
const updateBrew = (newData) => const updateBrew = (newData) =>
@@ -165,22 +161,21 @@ const EditPage = (props) => {
})); }));
const trySave = (immediate = false) => { const trySave = (immediate = false) => {
if (!debounceSave.current) return; //debounceSave = _.debounce(save, SAVE_TIMEOUT);
if (isSaving) return; if (isSaving) return;
const hasChange = !_.isEqual(currentBrew, savedBrew.current); const hasChange = !_.isEqual(currentBrew, savedBrew.current);
if (immediate) { if (immediate) {
debounceSave.current(); debounceSave(currentBrew, saveGoogle);
debounceSave.current.flush?.(); debounceSave.flush?.();
return; return;
} }
if (hasChange) { if (hasChange)
debounceSave.current(); debounceSave(currentBrew, saveGoogle);
} else { else
debounceSave.current.cancel?.(); debounceSave.cancel?.();
}
}; };
const handleGoogleClick = () => { const handleGoogleClick = () => {
@@ -206,29 +201,29 @@ const EditPage = (props) => {
trySave(true); trySave(true);
}; };
const save = async () => { const save = async (brew, saveToGoogle) => {
debounceSave.current?.cancel?.(); debounceSave?.cancel?.();
setIsSaving(true); setIsSaving(true);
setError(null); setError(null);
setHTMLErrors(Markdown.validate(currentBrew.text)); setHTMLErrors(Markdown.validate(brew.text));
await updateHistory(currentBrew).catch(console.error); await updateHistory(brew).catch(console.error);
await versionHistoryGarbageCollection().catch(console.error); await versionHistoryGarbageCollection().catch(console.error);
//Prepare content to send to server //Prepare content to send to server
const brewToSave = { const brewToSave = {
...currentBrew, ...brew,
text : currentBrew.text.normalize('NFC'), text : brew.text.normalize('NFC'),
pageCount: ((currentBrew.renderer === 'legacy' ? currentBrew.text.match(/\\page/g) : currentBrew.text.match(/^\\page$/gm)) || []).length + 1, pageCount: ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1,
patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(currentBrew.text.normalize('NFC')))), patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
hash : await md5(savedBrew.current.text), hash : await md5(savedBrew.current.text),
textBin : undefined textBin : undefined
}; };
const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave)));
const transfer = saveGoogle === _.isNil(currentBrew.googleId); const transfer = saveToGoogle === _.isNil(brew.googleId);
const params = transfer ? `?${saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''; const params = transfer ? `?${saveToGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : '';
const res = await request const res = await request
.put(`/api/update/${brewToSave.editId}${params}`) .put(`/api/update/${brewToSave.editId}${params}`)
@@ -244,24 +239,17 @@ const EditPage = (props) => {
const { googleId, editId, shareId, version } = res.body; const { googleId, editId, shareId, version } = res.body;
savedBrew.current = { savedBrew.current = {
...currentBrew, ...brew,
googleId: googleId ?? null, googleId: googleId ?? null,
editId, editId,
shareId, shareId,
version version
}; };
setCurrentBrew(prev => ({ setCurrentBrew(savedBrew.current);
...prev,
googleId: googleId ?? null,
editId,
shareId,
version
}));
setIsSaving(false); setIsSaving(false);
setUnsavedTime(new Date()); setUnsavedTime(new Date());
setUnsavedChanges(!_.isEqual(currentBrew, savedBrew.current));
history.replaceState(null, null, `/edit/${editId}`); history.replaceState(null, null, `/edit/${editId}`);
}; };