mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-07 01:12:44 +00:00
@@ -3,6 +3,8 @@ require('./editPage.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch';
|
||||||
|
import { md5 } from 'hash-wasm';
|
||||||
|
|
||||||
import request from '../../utils/request-middleware.js';
|
import request from '../../utils/request-middleware.js';
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
@@ -196,12 +198,19 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
trySave : function(immediate=false){
|
trySave : function(immediate=false){
|
||||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||||
if(this.hasChanges()){
|
if(this.state.isSaving)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(immediate) {
|
||||||
this.debounceSave();
|
this.debounceSave();
|
||||||
} else {
|
this.debounceSave.flush();
|
||||||
this.debounceSave.cancel();
|
return;
|
||||||
}
|
}
|
||||||
if(immediate) this.debounceSave.flush();
|
|
||||||
|
if(this.hasChanges())
|
||||||
|
this.debounceSave();
|
||||||
|
else
|
||||||
|
this.debounceSave.cancel();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleGoogleClick : function(){
|
handleGoogleClick : function(){
|
||||||
@@ -215,8 +224,7 @@ const EditPage = createClass({
|
|||||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||||
}));
|
}));
|
||||||
this.setState({
|
this.setState({
|
||||||
error : null,
|
error : null
|
||||||
isSaving : false
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -232,9 +240,8 @@ const EditPage = createClass({
|
|||||||
toggleGoogleStorage : function(){
|
toggleGoogleStorage : function(){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
saveGoogle : !prevState.saveGoogle,
|
saveGoogle : !prevState.saveGoogle,
|
||||||
isSaving : false,
|
|
||||||
error : null
|
error : null
|
||||||
}), ()=>this.save());
|
}), ()=>this.trySave(true));
|
||||||
},
|
},
|
||||||
|
|
||||||
save : async function(){
|
save : async function(){
|
||||||
@@ -249,11 +256,17 @@ const EditPage = createClass({
|
|||||||
await updateHistory(this.state.brew).catch(console.error);
|
await updateHistory(this.state.brew).catch(console.error);
|
||||||
await versionHistoryGarbageCollection().catch(console.error);
|
await versionHistoryGarbageCollection().catch(console.error);
|
||||||
|
|
||||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
const preSaveSnapshot = { ...this.state.brew }
|
||||||
|
|
||||||
const brew = this.state.brew;
|
//Prepare content to send to server
|
||||||
|
const brew = { ...this.state.brew };
|
||||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||||
|
brew.patches = makePatches(this.savedBrew.text, brew.text);
|
||||||
|
brew.hash = await md5(this.savedBrew.text);
|
||||||
|
brew.text = undefined;
|
||||||
|
brew.textBin = undefined;
|
||||||
|
|
||||||
|
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||||
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
|
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
|
||||||
const res = await request
|
const res = await request
|
||||||
.put(`/api/update/${brew.editId}${params}`)
|
.put(`/api/update/${brew.editId}${params}`)
|
||||||
@@ -265,20 +278,28 @@ const EditPage = createClass({
|
|||||||
if(!res) return;
|
if(!res) return;
|
||||||
|
|
||||||
this.savedBrew = {
|
this.savedBrew = {
|
||||||
...this.state.brew,
|
...preSaveSnapshot,
|
||||||
googleId : res.body.googleId ? res.body.googleId : null,
|
googleId : res.body.googleId ? res.body.googleId : null,
|
||||||
editId : res.body.editId,
|
editId : res.body.editId,
|
||||||
shareId : res.body.shareId,
|
shareId : res.body.shareId,
|
||||||
version : res.body.version
|
version : res.body.version
|
||||||
};
|
};
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
|
||||||
|
|
||||||
this.setState(()=>({
|
this.setState((prevState) => ({
|
||||||
brew : this.savedBrew,
|
brew: {
|
||||||
unsavedChanges : false,
|
...prevState.brew,
|
||||||
|
googleId : res.body.googleId ? res.body.googleId : null,
|
||||||
|
editId : res.body.editId,
|
||||||
|
shareId : res.body.shareId,
|
||||||
|
version : res.body.version
|
||||||
|
},
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
unsavedTime : new Date()
|
unsavedTime : new Date()
|
||||||
}));
|
}), ()=>{
|
||||||
|
this.setState({ unsavedChanges : this.hasChanges() });
|
||||||
|
});
|
||||||
|
|
||||||
|
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderGoogleDriveIcon : function(){
|
renderGoogleDriveIcon : function(){
|
||||||
|
|||||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"@babel/preset-react": "^7.27.1",
|
"@babel/preset-react": "^7.27.1",
|
||||||
"@babel/runtime": "^7.27.6",
|
"@babel/runtime": "^7.27.6",
|
||||||
"@googleapis/drive": "^13.0.1",
|
"@googleapis/drive": "^13.0.1",
|
||||||
|
"@sanity/diff-match-patch": "^3.2.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "3.0.0",
|
"express-static-gzip": "3.0.0",
|
||||||
"fs-extra": "11.3.0",
|
"fs-extra": "11.3.0",
|
||||||
|
"hash-wasm": "^4.12.0",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
@@ -2895,6 +2897,15 @@
|
|||||||
"url": "https://opencollective.com/pkgr"
|
"url": "https://opencollective.com/pkgr"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@sanity/diff-match-patch": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sanity/diff-match-patch/-/diff-match-patch-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-4hPADs0qUThFZkBK/crnfKKHg71qkRowfktBljH2UIxGHHTxIzt8g8fBiXItyCjxkuNy+zpYOdRMifQNv8+Yww==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.34.37",
|
"version": "0.34.37",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz",
|
||||||
@@ -7440,6 +7451,11 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hash-wasm": {
|
||||||
|
"version": "4.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz",
|
||||||
|
"integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ=="
|
||||||
|
},
|
||||||
"node_modules/hash.js": {
|
"node_modules/hash.js": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
"@babel/preset-react": "^7.27.1",
|
"@babel/preset-react": "^7.27.1",
|
||||||
"@babel/runtime": "^7.27.6",
|
"@babel/runtime": "^7.27.6",
|
||||||
"@googleapis/drive": "^13.0.1",
|
"@googleapis/drive": "^13.0.1",
|
||||||
|
"@sanity/diff-match-patch": "^3.2.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
@@ -102,6 +103,7 @@
|
|||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "3.0.0",
|
"express-static-gzip": "3.0.0",
|
||||||
"fs-extra": "11.3.0",
|
"fs-extra": "11.3.0",
|
||||||
|
"hash-wasm": "^4.12.0",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"codemirror/addon/selection/active-line.js",
|
"codemirror/addon/selection/active-line.js",
|
||||||
"codemirror/addon/hint/show-hint.js",
|
"codemirror/addon/hint/show-hint.js",
|
||||||
"moment",
|
"moment",
|
||||||
"superagent"
|
"superagent",
|
||||||
|
"@sanity/diff-match-patch"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import Markdown from '../shared/naturalcrit/markdown.js';
|
|||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import asyncHandler from 'express-async-handler';
|
import asyncHandler from 'express-async-handler';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
|
||||||
|
import { md5 } from 'hash-wasm';
|
||||||
import { splitTextStyleAndMetadata,
|
import { splitTextStyleAndMetadata,
|
||||||
brewSnippetsToJSON } from '../shared/helpers.js';
|
brewSnippetsToJSON } from '../shared/helpers.js';
|
||||||
import checkClientVersion from './middleware/check-client-version.js';
|
import checkClientVersion from './middleware/check-client-version.js';
|
||||||
@@ -337,21 +339,30 @@ const api = {
|
|||||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||||
const brewFromServer = req.brew;
|
const brewFromServer = req.brew;
|
||||||
if(brewFromServer.version && brewFromClient.version && brewFromServer.version > brewFromClient.version) {
|
splitTextStyleAndMetadata(brewFromServer);
|
||||||
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
brewFromServer.hash = await md5(brewFromServer.text);
|
||||||
return res.status(409).send(JSON.stringify({ message: `The brew has been changed on a different device. Please save your changes elsewhere, refresh, and try again.` }));
|
|
||||||
}
|
if((brewFromServer?.version !== brewFromClient?.version) || (brewFromServer?.hash !== brewFromClient?.hash)) {
|
||||||
|
if(brewFromClient?.version !== brewFromClient?.version)
|
||||||
|
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||||
|
if(brewFromServer?.hash !== brewFromClient?.hash) {
|
||||||
|
console.log(`Hash mismatch on brew ${brewFromClient.editId}`);
|
||||||
|
}
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||||
|
}
|
||||||
|
|
||||||
|
let brew = _.assign(brewFromServer, brewFromClient);
|
||||||
|
brew.title = brew.title.trim();
|
||||||
|
brew.description = brew.description.trim() || '';
|
||||||
|
brew.text = applyPatches(brewFromClient.patches, brewFromServer.text)[0];
|
||||||
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
let brew = _.assign(brewFromServer, brewFromClient);
|
|
||||||
const googleId = brew.googleId;
|
const googleId = brew.googleId;
|
||||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||||
let afterSave = async ()=>true;
|
let afterSave = async ()=>true;
|
||||||
|
|
||||||
brew.title = brew.title.trim();
|
|
||||||
brew.description = brew.description.trim() || '';
|
|
||||||
brew.text = api.mergeBrewText(brew);
|
|
||||||
|
|
||||||
if(brew.googleId && removeFromGoogle) {
|
if(brew.googleId && removeFromGoogle) {
|
||||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||||
afterSave = async ()=>{
|
afterSave = async ()=>{
|
||||||
@@ -484,8 +495,8 @@ const api = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||||
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||||
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||||
|
|||||||
Reference in New Issue
Block a user