diff --git a/changelog.md b/changelog.md index 9fed601f8..c4f5e4420 100644 --- a/changelog.md +++ b/changelog.md @@ -39,6 +39,46 @@ pre { ## changelog For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). +### Thursday 09/06/2022 - v3.1.1 +{{taskList + +##### Calculuschild: + +* [x] Fixed class table decorations appearing on top of the table in PDF output. + + Fixes issues: [#1784](https://github.com/naturalcrit/homebrewery/issues/1784) + +* [x] Fix bottom decoration on half class tables disappearing when the table is too short. + + Fixes issues: [#2202](https://github.com/naturalcrit/homebrewery/issues/2202) +}} + +### Monday 06/06/2022 - v3.1.0 +{{taskList + +##### G-Ambatte: + +* [x] "Jump to Preview/Editor" buttons added to the divider bar. Easily sync between the editor and preview panels! + + Fixes issues: [#1756](https://github.com/naturalcrit/homebrewery/issues/1756) + +* [x] Speedups to the user page for users with large and/or many brews. + + Fixes issues: [#2147](https://github.com/naturalcrit/homebrewery/issues/2147) + +* [x] Search text on the user page is saved to the URL for easy bookmarking in your browser + + Fixes issues: [#1858](https://github.com/naturalcrit/homebrewery/issues/1858) + +* [x] Added easy login system for offline installs. + + Fixes issues: [#269](https://github.com/naturalcrit/homebrewery/issues/269) + +* [x] New **THUMBNAIL** option in the {{fa,fa-info-circle}} **Properties** menu. This image will show up in social media links. + + Fixes issues: [#820](https://github.com/naturalcrit/homebrewery/issues/820) +}} + ### Wednesday 27/03/2022 - v3.0.8 {{taskList * [x] Style updates to user page. diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 918ec176e..08911074b 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -194,7 +194,7 @@ const BrewRenderer = createClass({ : null} -
${text}`;
res.status(200).send(text);
-}));
+});
//Download brew source page
-app.get('/download/:id', asyncHandler(async (req, res)=>{
- const brew = await getBrewFromId(req.params.id, 'raw');
+app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
+ const { brew } = req;
+ sanitizeBrew(brew, 'share');
const prefix = 'HB - ';
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
@@ -189,17 +159,19 @@ app.get('/download/:id', asyncHandler(async (req, res)=>{
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
});
res.status(200).send(brew.text);
-}));
+});
//User Page
app.get('/user/:username', async (req, res, next)=>{
const ownAccount = req.account && (req.account.username == req.params.username);
const fields = [
+ 'googleId',
'title',
'pageCount',
'description',
'authors',
+ 'published',
'views',
'shareId',
'editId',
@@ -220,58 +192,71 @@ app.get('/user/:username', async (req, res, next)=>{
console.error(err);
});
- if(googleBrews) {
+ if(googleBrews && googleBrews.length > 0) {
+ for (const brew of brews.filter((brew)=>brew.googleId)) {
+ const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
+ if(match !== -1) {
+ brew.googleId = googleBrews[match].googleId;
+ brew.stubbed = true;
+ brew.pageCount = googleBrews[match].pageCount;
+ brew.renderer = googleBrews[match].renderer;
+ brew.version = googleBrews[match].version;
+ googleBrews.splice(match, 1);
+ }
+ }
+
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
brews = _.concat(brews, googleBrews);
}
}
req.brews = _.map(brews, (brew)=>{
- return sanitizeBrew(brew, !ownAccount);
+ return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
});
return next();
});
//Edit Page
-app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
+app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
+ req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
+ sanitizeBrew(req.brew, 'edit');
+ splitTextStyleAndMetadata(req.brew);
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
- const brew = await getBrewFromId(req.params.id, 'edit');
- req.brew = brew;
return next();
-}));
+});
//New Page
-app.get('/new/:id', asyncHandler(async (req, res, next)=>{
- const brew = await getBrewFromId(req.params.id, 'share');
- brew.title = `CLONE - ${brew.title}`;
- req.brew = brew;
+app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
+ sanitizeBrew(req.brew, 'share');
+ splitTextStyleAndMetadata(req.brew);
+ req.brew.title = `CLONE - ${req.brew.title}`;
return next();
-}));
+});
//Share Page
-app.get('/share/:id', asyncHandler(async (req, res, next)=>{
- const brew = await getBrewFromId(req.params.id, 'share');
+app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
+ const { brew } = req;
- if(req.params.id.length > 12) {
+ if(req.params.id.length > 12 && !brew._id) {
const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12);
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
- .catch((err)=>{next(err);});
+ .catch((err)=>{next(err);});
} else {
await HomebrewModel.increaseView({ shareId: brew.shareId });
}
-
- req.brew = brew;
+ sanitizeBrew(req.brew, 'share');
+ splitTextStyleAndMetadata(req.brew);
return next();
}));
//Print Page
-app.get('/print/:id', asyncHandler(async (req, res, next)=>{
- const brew = await getBrewFromId(req.params.id, 'share');
- req.brew = brew;
- return next();
-}));
+app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
+ sanitizeBrew(req.brew, 'share');
+ splitTextStyleAndMetadata(req.brew);
+ next();
+});
const nodeEnv = config.get('node_env');
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
@@ -291,7 +276,7 @@ if(isLocalEnvironment){
//Render the page
const templateFn = require('./../client/template.js');
-app.use((req, res)=>{
+app.use(asyncHandler(async (req, res, next)=>{
// Create configuration object
const configuration = {
local : isLocalEnvironment,
@@ -309,13 +294,14 @@ app.use((req, res)=>{
config : configuration
};
const title = req.brew ? req.brew.title : '';
- templateFn('homebrew', title, props)
- .then((page)=>{ res.send(page); })
- .catch((err)=>{
- console.log(err);
- return res.sendStatus(500);
- });
-});
+ const page = await templateFn('homebrew', title, props)
+ .catch((err)=>{
+ console.log(err);
+ return res.sendStatus(500);
+ });
+ if(!page) return;
+ res.send(page);
+}));
//v=====----- Error-Handling Middleware -----=====v//
//Format Errors so all fields will be sent
@@ -339,6 +325,13 @@ app.use((err, req, res, next)=>{
console.error(err);
res.status(status).send(getPureError(err));
});
+
+app.use((req, res)=>{
+ if(!res.headersSent) {
+ console.error('Headers have not been sent, responding with a server error.', req.url);
+ res.status(500).send('An error occurred and the server did not send a response. The error has been logged, please note the time this occurred and report this issue.');
+ }
+});
//^=====--------------------------------------=====^//
module.exports = {
diff --git a/server/googleActions.js b/server/googleActions.js
index 89691e0d8..5d9e43b71 100644
--- a/server/googleActions.js
+++ b/server/googleActions.js
@@ -143,12 +143,11 @@ const GoogleActions = {
description : `${brew.description}`,
properties : {
title : brew.title,
- published : brew.published,
- version : brew.version,
- renderer : brew.renderer,
- tags : brew.tags,
+ shareId : brew.shareId || nanoid(12),
+ editId : brew.editId || nanoid(12),
pageCount : brew.pageCount,
- systems : brew.systems.join(),
+ renderer : brew.renderer || 'legacy',
+ isStubbed : true,
thumbnail : brew.thumbnail
}
},
@@ -161,10 +160,9 @@ const GoogleActions = {
console.log('Error saving to google');
console.error(err);
throw (err);
- //return res.status(500).send('Error while saving');
});
- return (brew);
+ return true;
},
newGoogleBrew : async (auth, brew)=>{
@@ -178,17 +176,18 @@ const GoogleActions = {
const folderId = await GoogleActions.getGoogleFolder(auth);
const fileMetadata = {
- 'name' : `${brew.title}.txt`,
- 'description' : `${brew.description}`,
- 'parents' : [folderId],
- 'properties' : { //AppProperties is not accessible
- 'shareId' : brew.shareId || nanoid(12),
- 'editId' : brew.editId || nanoid(12),
- 'title' : brew.title,
- 'views' : '0',
- 'pageCount' : brew.pageCount,
- 'renderer' : brew.renderer || 'legacy',
- 'thumbnail' : brew.thumbnail || ''
+ name : `${brew.title}.txt`,
+ description : `${brew.description}`,
+ parents : [folderId],
+ properties : { //AppProperties is not accessible
+ shareId : brew.shareId || nanoid(12),
+ editId : brew.editId || nanoid(12),
+ title : brew.title,
+ pageCount : brew.pageCount,
+ renderer : brew.renderer || 'legacy',
+ isStubbed : true,
+ version : 1,
+ thumbnail : brew.thumbnail || ''
}
};
@@ -215,26 +214,7 @@ const GoogleActions = {
console.error(err);
});
- const newHomebrew = {
- text : brew.text,
- shareId : fileMetadata.properties.shareId,
- editId : fileMetadata.properties.editId,
- createdAt : new Date(),
- updatedAt : new Date(),
- gDrive : true,
- googleId : obj.data.id,
- pageCount : fileMetadata.properties.pageCount,
-
- title : brew.title,
- description : brew.description,
- tags : '',
- published : brew.published,
- renderer : brew.renderer,
- authors : [],
- systems : []
- };
-
- return newHomebrew;
+ return obj.data.id;
},
getGoogleBrew : async (id, accessId, accessType)=>{
@@ -247,7 +227,6 @@ const GoogleActions = {
.catch((err)=>{
console.log('Error loading from Google');
throw (err);
- return;
});
if(obj) {
@@ -257,9 +236,7 @@ const GoogleActions = {
throw ('Share ID does not match');
}
- const serviceDrive = google.drive({ version: 'v3' });
-
- const file = await serviceDrive.files.get({
+ const file = await drive.files.get({
fileId : id,
fields : 'description, properties',
alt : 'media'
@@ -276,7 +253,7 @@ const GoogleActions = {
text : file.data,
description : obj.data.description,
- tags : obj.data.properties.tags ? obj.data.properties.tags : '',
+ tags : obj.data.properties.tags ? obj.data.properties.tags : '',
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
authors : [],
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
@@ -291,7 +268,6 @@ const GoogleActions = {
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
thumbnail : obj.data.properties.thumbnail || '',
- gDrive : true,
googleId : id
};
@@ -299,14 +275,11 @@ const GoogleActions = {
}
},
- deleteGoogleBrew : async (auth, id)=>{
+ deleteGoogleBrew : async (auth, id, accessId)=>{
const drive = google.drive({ version: 'v3', auth });
- const googleId = id.slice(0, -12);
- const accessId = id.slice(-12);
-
const obj = await drive.files.get({
- fileId : googleId,
+ fileId : id,
fields : 'properties'
})
.catch((err)=>{
@@ -315,11 +288,11 @@ const GoogleActions = {
});
if(obj && obj.data.properties.editId != accessId) {
- throw ('Not authorized to delete this Google brew');
+ throw { status: 403, message: 'Not authorized to delete this Google brew' };
}
await drive.files.update({
- fileId : googleId,
+ fileId : id,
resource : { trashed: true }
})
.catch((err)=>{
diff --git a/server/homebrew.api.js b/server/homebrew.api.js
index 3c1267f0b..0f12ee6b8 100644
--- a/server/homebrew.api.js
+++ b/server/homebrew.api.js
@@ -1,3 +1,4 @@
+/* eslint-disable max-lines */
const _ = require('lodash');
const HomebrewModel = require('./homebrew.model.js').model;
const router = require('express').Router();
@@ -6,6 +7,7 @@ const GoogleActions = require('./googleActions.js');
const Markdown = require('../shared/naturalcrit/markdown.js');
const yaml = require('js-yaml');
const asyncHandler = require('express-async-handler');
+const { nanoid } = require('nanoid');
// const getTopBrews = (cb) => {
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
@@ -13,6 +15,53 @@ const asyncHandler = require('express-async-handler');
// });
// };
+const getBrew = (accessType)=>{
+ // Create middleware with the accessType passed in as part of the scope
+ return async (req, res, next)=>{
+ // Set the id and initial potential google id, where the google id is present on the existing brew.
+ let id = req.params.id, googleId = req.body?.googleId;
+
+ // If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
+ if(id.length > 12) {
+ googleId = id.slice(0, -12);
+ id = id.slice(-12);
+ }
+ // Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
+ let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id })
+ .catch((err)=>{
+ if(googleId) {
+ console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
+ } else {
+ console.warn(err);
+ }
+ });
+ stub = stub?.toObject();
+
+ // If there is a google id, try to find the google brew
+ if(googleId || stub?.googleId) {
+ let googleError;
+ const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
+ .catch((err)=>{
+ console.warn(err);
+ googleError = err;
+ });
+ // If we can't find the google brew and there is a google id for the brew, throw an error.
+ if(!googleBrew) throw googleError;
+ // Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
+ stub = stub ? _.assign({ ...excludeStubProps(stub), stubbed: true }, excludeGoogleProps(googleBrew)) : googleBrew;
+ }
+
+ // If after all of that we still don't have a brew, throw an exception
+ if(!stub) {
+ throw 'Brew not found in Homebrewery database or Google Drive';
+ }
+
+ req.brew = stub;
+
+ next();
+ };
+};
+
const mergeBrewText = (brew)=>{
let text = brew.text;
if(brew.style !== undefined) {
@@ -33,15 +82,33 @@ const MAX_TITLE_LENGTH = 100;
const getGoodBrewTitle = (text)=>{
const tokens = Markdown.marked.lexer(text);
- return (tokens.find((token)=>token.type == 'heading' || token.type == 'paragraph')?.text || 'No Title')
+ return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
.slice(0, MAX_TITLE_LENGTH);
};
const excludePropsFromUpdate = (brew)=>{
// Remove undesired properties
- const propsToExclude = ['views', 'lastViewed'];
+ const modified = _.clone(brew);
+ const propsToExclude = ['_id', 'views', 'lastViewed', 'editId', 'shareId', 'googleId'];
for (const prop of propsToExclude) {
- delete brew[prop];
+ delete modified[prop];
+ }
+ return modified;
+};
+
+const excludeGoogleProps = (brew)=>{
+ const modified = _.clone(brew);
+ const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views'];
+ for (const prop of propsToExclude) {
+ delete modified[prop];
+ }
+ return modified;
+};
+
+const excludeStubProps = (brew)=>{
+ const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount', 'version'];
+ for (const prop of propsToExclude) {
+ brew[prop] = undefined;
}
return brew;
};
@@ -55,33 +122,17 @@ const beforeNewSave = (account, brew)=>{
brew.text = mergeBrewText(brew);
};
-const newLocalBrew = async (brew)=>{
- const newHomebrew = new HomebrewModel(brew);
- // Compress brew text to binary before saving
- newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
- // Delete the non-binary text field since it's not needed anymore
- newHomebrew.text = undefined;
-
- let saved = await newHomebrew.save()
- .catch((err)=>{
- console.error(err, err.toString(), err.stack);
- throw `Error while creating new brew, ${err.toString()}`;
- });
-
- saved = saved.toObject();
- saved.gDrive = false;
- return saved;
-};
-
const newGoogleBrew = async (account, brew, res)=>{
const oAuth2Client = GoogleActions.authCheck(account, res);
- return await GoogleActions.newGoogleBrew(oAuth2Client, brew);
+ const newBrew = excludeGoogleProps(brew);
+
+ return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
};
const newBrew = async (req, res)=>{
const brew = req.body;
- const { transferToGoogle } = req.query;
+ const { saveToGoogle } = req.query;
delete brew.editId;
delete brew.shareId;
@@ -89,148 +140,179 @@ const newBrew = async (req, res)=>{
beforeNewSave(req.account, brew);
- let saved;
- if(transferToGoogle) {
- saved = await newGoogleBrew(req.account, brew, res)
+ const newHomebrew = new HomebrewModel(brew);
+ newHomebrew.editId = nanoid(12);
+ newHomebrew.shareId = nanoid(12);
+
+ let googleId, saved;
+ if(saveToGoogle) {
+ googleId = await newGoogleBrew(req.account, newHomebrew, res)
.catch((err)=>{
- res.status(err.status || err.response.status).send(err.message || err);
+ console.error(err);
+ res.status(err?.status || err?.response?.status || 500).send(err?.message || err);
});
+ if(!googleId) return;
+ excludeStubProps(newHomebrew);
+ newHomebrew.googleId = googleId;
} else {
- saved = await newLocalBrew(brew)
- .catch((err)=>{
- res.status(500).send(err);
- });
+ // Compress brew text to binary before saving
+ newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
+ // Delete the non-binary text field since it's not needed anymore
+ newHomebrew.text = undefined;
}
+
+ saved = await newHomebrew.save()
+ .catch((err)=>{
+ console.error(err, err.toString(), err.stack);
+ throw `Error while creating new brew, ${err.toString()}`;
+ });
if(!saved) return;
- return res.status(200).send(saved);
+ saved = saved.toObject();
+
+ res.status(200).send(saved);
};
const updateBrew = async (req, res)=>{
- let brew = excludePropsFromUpdate(req.body);
- const { transferToGoogle, transferFromGoogle } = req.query;
+ // Initialize brew from request and body, destructure query params, set a constant for the google id, and set the initial value for the after-save method
+ let brew = _.assign(req.brew, excludePropsFromUpdate(req.body));
+ const { saveToGoogle, removeFromGoogle } = req.query;
+ const googleId = brew.googleId;
+ let afterSave = async ()=>true;
- let saved;
- if(brew.googleId && transferFromGoogle) {
- beforeNewSave(req.account, brew);
+ brew.text = mergeBrewText(brew);
- saved = await newLocalBrew(brew)
- .catch((err)=>{
- console.error(err);
- res.status(500).send(err);
- });
- if(!saved) return;
+ 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
+ afterSave = async ()=>{
+ return await deleteGoogleBrew(req.account, googleId, brew.editId, res)
+ .catch((err)=>{
+ console.error(err);
+ res.status(err?.status || err?.response?.status || 500).send(err.message || err);
+ });
+ };
- await deleteGoogleBrew(req.account, `${brew.googleId}${brew.editId}`, res)
+ brew.googleId = undefined;
+ } else if(!brew.googleId && saveToGoogle) {
+ // If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
+ brew.googleId = await newGoogleBrew(req.account, excludeGoogleProps(brew), res)
.catch((err)=>{
console.error(err);
res.status(err.status || err.response.status).send(err.message || err);
});
- } else if(!brew.googleId && transferToGoogle) {
- saved = await newGoogleBrew(req.account, brew, res)
- .catch((err)=>{
- console.error(err);
- res.status(err.status || err.response.status).send(err.message || err);
- });
- if(!saved) return;
-
- await deleteLocalBrew(req.account, brew.editId)
- .catch((err)=>{
- console.error(err);
- res.status(err.status).send(err.message);
- });
+ if(!brew.googleId) return;
} else if(brew.googleId) {
- brew.text = mergeBrewText(brew);
-
- saved = await GoogleActions.updateGoogleBrew(brew)
+ // If the google id exists and no other actions are being performed, update the google brew
+ const updated = await GoogleActions.updateGoogleBrew(excludeGoogleProps(brew))
.catch((err)=>{
console.error(err);
- res.status(err.response?.status || 500).send(err);
+ res.status(err?.response?.status || 500).send(err);
});
+ if(!updated) return;
+ }
+
+ if(brew.googleId) {
+ // If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
+ excludeStubProps(brew);
} else {
- const dbBrew = await HomebrewModel.get({ editId: req.params.id })
- .catch((err)=>{
- console.error(err);
- return res.status(500).send('Error while saving');
- });
-
- brew = _.merge(dbBrew, brew);
- brew.text = mergeBrewText(brew);
-
// Compress brew text to binary before saving
brew.textBin = zlib.deflateRawSync(brew.text);
// Delete the non-binary text field since it's not needed anymore
brew.text = undefined;
- brew.updatedAt = new Date();
-
- if(req.account) {
- brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
- }
-
- brew.markModified('authors');
- brew.markModified('systems');
-
- saved = await brew.save();
}
+ brew.updatedAt = new Date();
+
+ if(req.account) {
+ brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
+ }
+
+ // Fetch the brew from the database again (if it existed there to begin with), and assign the existing brew to it
+ brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
+
+ if(!brew.markModified) {
+ // If it wasn't in the database, create a new db brew
+ brew = new HomebrewModel(brew);
+ }
+
+ brew.markModified('authors');
+ brew.markModified('systems');
+
+ // Save the database brew
+ const saved = await brew.save()
+ .catch((err)=>{
+ console.error(err);
+ res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
+ });
if(!saved) return;
+ // Call and wait for afterSave to complete
+ const after = await afterSave();
+ if(!after) return;
- if(!res.headersSent) return res.status(200).send(saved);
+ res.status(200).send(saved);
};
-const deleteBrew = async (req, res)=>{
- if(req.params.id.length > 12) {
- const deleted = await deleteGoogleBrew(req.account, req.params.id, res)
- .catch((err)=>{
- res.status(500).send(err);
- });
- if(deleted) return res.status(200).send();
- } else {
- const deleted = await deleteLocalBrew(req.account, req.params.id)
- .catch((err)=>{
- res.status(err.status).send(err.message);
- });
- if(deleted) return res.status(200).send(deleted);
- return res.status(200).send();
- }
-};
-
-const deleteLocalBrew = async (account, id)=>{
- const brew = await HomebrewModel.findOne({ editId: id });
- if(!brew) {
- throw { status: 404, message: 'Can not find homebrew with that id' };
- }
-
- if(account) {
- // Remove current user as author
- brew.authors = _.pull(brew.authors, account.username);
- brew.markModified('authors');
- }
-
- if(brew.authors.length === 0) {
- // Delete brew if there are no authors left
- await brew.remove()
- .catch((err)=>{
- console.error(err);
- throw { status: 500, message: 'Error while removing' };
- });
- } else {
- // Otherwise, save the brew with updated author list
- return await brew.save()
- .catch((err)=>{
- throw { status: 500, message: err };
- });
- }
-};
-
-const deleteGoogleBrew = async (account, id, res)=>{
+const deleteGoogleBrew = async (account, id, editId, res)=>{
const auth = await GoogleActions.authCheck(account, res);
- await GoogleActions.deleteGoogleBrew(auth, id);
+ await GoogleActions.deleteGoogleBrew(auth, id, editId);
return true;
};
-router.post('/api', asyncHandler(newBrew));
-router.put('/api/:id', asyncHandler(updateBrew));
-router.put('/api/update/:id', asyncHandler(updateBrew));
-router.delete('/api/:id', asyncHandler(deleteBrew));
-router.get('/api/remove/:id', asyncHandler(deleteBrew));
+const deleteBrew = async (req, res)=>{
+ let brew = req.brew;
+ const { googleId, editId } = brew;
+ const account = req.account;
+ const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username);
+ // If the user is the owner and the file is saved to google, mark the google brew for deletion
+ const shouldDeleteGoogleBrew = googleId && isOwner;
-module.exports = router;
+ if(brew._id) {
+ brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
+ if(account) {
+ // Remove current user as author
+ brew.authors = _.pull(brew.authors, account.username);
+ brew.markModified('authors');
+ }
+
+ if(brew.authors.length === 0) {
+ // Delete brew if there are no authors left
+ await brew.remove()
+ .catch((err)=>{
+ console.error(err);
+ throw { status: 500, message: 'Error while removing' };
+ });
+ } else {
+ if(shouldDeleteGoogleBrew) {
+ // When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database
+ brew.googleId = undefined;
+ brew.textBin = zlib.deflateRawSync(brew.text);
+ brew.text = undefined;
+ }
+
+ // Otherwise, save the brew with updated author list
+ await brew.save()
+ .catch((err)=>{
+ throw { status: 500, message: err };
+ });
+ }
+ }
+ if(shouldDeleteGoogleBrew) {
+ const deleted = await deleteGoogleBrew(account, googleId, editId, res)
+ .catch((err)=>{
+ console.error(err);
+ res.status(500).send(err);
+ });
+ if(!deleted) return;
+ }
+
+ res.status(204).send();
+};
+
+router.post('/api', asyncHandler(newBrew));
+router.put('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
+router.put('/api/update/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
+router.delete('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(deleteBrew));
+router.get('/api/remove/:id', asyncHandler(getBrew('edit')), asyncHandler(deleteBrew));
+
+module.exports = {
+ homebrewApi : router,
+ getBrew
+};
diff --git a/server/homebrew.model.js b/server/homebrew.model.js
index d0692f0a9..db0669e42 100644
--- a/server/homebrew.model.js
+++ b/server/homebrew.model.js
@@ -6,6 +6,7 @@ const zlib = require('zlib');
const HomebrewSchema = mongoose.Schema({
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
+ googleId : { type: String },
title : { type: String, default: '' },
text : { type: String, default: '' },
textBin : { type: Buffer },
diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx
index 42076ed76..6340a58fe 100644
--- a/shared/naturalcrit/codeEditor/codeEditor.jsx
+++ b/shared/naturalcrit/codeEditor/codeEditor.jsx
@@ -30,6 +30,8 @@ if(typeof navigator !== 'undefined'){
// require('codemirror/addon/edit/trailingspace.js');
//Active line highlighting
// require('codemirror/addon/selection/active-line.js');
+ //Scroll past last line
+ require('codemirror/addon/scroll/scrollpastend.js');
//Auto-closing
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
require('codemirror/addon/fold/xml-fold.js');
@@ -98,6 +100,7 @@ const CodeEditor = createClass({
indentWithTabs : true,
tabSize : 2,
historyEventDelay : 250,
+ scrollPastEnd : true,
extraKeys : {
'Ctrl-B' : this.makeBold,
'Cmd-B' : this.makeBold,
diff --git a/shared/naturalcrit/codeEditor/codeEditor.less b/shared/naturalcrit/codeEditor/codeEditor.less
index bf36293ed..c929e0d21 100644
--- a/shared/naturalcrit/codeEditor/codeEditor.less
+++ b/shared/naturalcrit/codeEditor/codeEditor.less
@@ -3,6 +3,11 @@
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
@import (less) 'codemirror/addon/dialog/dialog.css';
+@keyframes sourceMoveAnimation {
+ 50% {background-color: red; color: white;}
+ 100% {background-color: unset; color: unset;}
+}
+
.codeEditor{
.CodeMirror-foldmarker {
font-family: inherit;
@@ -10,6 +15,11 @@
font-weight: 600;
}
+ .sourceMoveFlash .CodeMirror-line{
+ animation-name: sourceMoveAnimation;
+ animation-duration: 0.4s;
+ }
+
//.cm-tab {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
//}
@@ -19,4 +29,4 @@
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
// }
//}
-}
\ No newline at end of file
+}
diff --git a/shared/naturalcrit/splitPane/splitPane.jsx b/shared/naturalcrit/splitPane/splitPane.jsx
index 4d138e30b..f7ad4bed4 100644
--- a/shared/naturalcrit/splitPane/splitPane.jsx
+++ b/shared/naturalcrit/splitPane/splitPane.jsx
@@ -17,7 +17,10 @@ const SplitPane = createClass({
return {
currentDividerPos : null,
windowWidth : 0,
- isDragging : false
+ isDragging : false,
+ moveSource : false,
+ moveBrew : false,
+ showMoveArrows : true
};
},
@@ -29,6 +32,11 @@ const SplitPane = createClass({
userSetDividerPos : dividerPos,
windowWidth : window.innerWidth
});
+ } else {
+ this.setState({
+ currentDividerPos : window.innerWidth / 2,
+ userSetDividerPos : window.innerWidth / 2
+ });
}
window.addEventListener('resize', this.handleWindowResize);
},
@@ -83,20 +91,58 @@ const SplitPane = createClass({
window.getSelection().removeAllRanges();
}
},
-*/
+ */
+
+ setMoveArrows : function(newState) {
+ if(this.state.showMoveArrows != newState){
+ this.setState({
+ showMoveArrows : newState
+ });
+ }
+ },
+
+ renderMoveArrows : function(){
+ if(this.state.showMoveArrows) {
+ return <>
+