diff --git a/README.md b/README.md index 5206f4cbf..a33f2f073 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,9 @@ it using the two commands: 1. `npm install` 1. `npm start` -You should now be able to go to [http://localhost:8000](http://localhost:8000) -in your browser and use The Homebrewery offline. +When the Homebrewery server is started for the first time, it will modify the database to create the indexes required for better Homebrewery performance. This may take a few moments to complete for each index, dependent on how much content is in your local database - a brand new, empty database should be done in seconds. + +On completion, you should be able to go to [http://localhost:8000](http://localhost:8000) in your browser and use The Homebrewery offline. If you had any issue at all, here are some links that may be useful: - [Course](https://learn.mongodb.com/courses/m103-basic-cluster-administration) on cluster administration, useful for beginners @@ -145,3 +146,4 @@ your contribution to the project, please join our [gitter chat][gitter-url]. [github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request [gitter-url]: https://gitter.im/naturalcrit/Lobby + diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index 8c53cb333..a067ac4af 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -40,11 +40,8 @@ const Editor = createClass({ style : '' }, - onTextChange : ()=>{}, - onStyleChange : ()=>{}, - onMetaChange : ()=>{}, - onSnipChange : ()=>{}, - reportError : ()=>{}, + onBrewChange : ()=>{}, + reportError : ()=>{}, onCursorPageChange : ()=>{}, onViewPageChange : ()=>{}, @@ -438,7 +435,7 @@ const Editor = createClass({ language='gfm' view={this.state.view} value={this.props.brew.text} - onChange={this.props.onTextChange} + onChange={this.props.onBrewChange('text')} editorTheme={this.state.editorTheme} rerenderParent={this.rerenderParent} style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} /> @@ -451,7 +448,7 @@ const Editor = createClass({ language='css' view={this.state.view} value={this.props.brew.style ?? DEFAULT_STYLE_TEXT} - onChange={this.props.onStyleChange} + onChange={this.props.onBrewChange('style')} enableFolding={true} editorTheme={this.state.editorTheme} rerenderParent={this.rerenderParent} @@ -467,7 +464,7 @@ const Editor = createClass({ ; @@ -481,7 +478,7 @@ const Editor = createClass({ language='gfm' view={this.state.view} value={this.props.brew.snippets} - onChange={this.props.onSnipChange} + onChange={this.props.onBrewChange('snippets')} enableFolding={true} editorTheme={this.state.editorTheme} rerenderParent={this.rerenderParent} diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 593a184ca..200a754c2 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -39,7 +39,13 @@ const SAVE_TIMEOUT = 10000; const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds + const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; +const BREWKEY = 'HB_newPage_content'; +const STYLEKEY = 'HB_newPage_style'; +const SNIPKEY = 'HB_newPage_snippets'; +const METAKEY = 'HB_newPage_meta'; + const EditPage = (props)=>{ props = { @@ -71,6 +77,8 @@ const EditPage = (props)=>{ const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges + const useLocalStorage = false; + useEffect(()=>{ const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); setAutoSaveEnabled(autoSavePref); @@ -127,29 +135,27 @@ const EditPage = (props)=>{ setCurrentBrewRendererPageNum(pageNumber); }; - const handleTextChange = (text)=>{ + const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' + if (subfield == 'renderer' || subfield == 'theme') + fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); + //If there are HTML errors, run the validator on every change to give quick feedback - if(HTMLErrors.length) - setHTMLErrors(Markdown.validate(text)); - setCurrentBrew((prevBrew)=>({ ...prevBrew, text })); - }; + if(HTMLErrors.length && (field == 'text' || field == 'snippets')) + setHTMLErrors(Markdown.validate(value)); - const handleStyleChange = (style)=>{ - setCurrentBrew((prevBrew)=>({ ...prevBrew, style })); - }; + if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); + else setCurrentBrew(prev => ({ ...prev, [field]: value })); - const handleSnipChange = (snippet)=>{ - //If there are HTML errors, run the validator on every change to give quick feedback - if(HTMLErrors.length) - setHTMLErrors(Markdown.validate(snippet)); - setCurrentBrew((prevBrew)=>({ ...prevBrew, snippets: snippet })); - }; - - const handleMetaChange = (metadata, field = undefined)=>{ - if(field === 'theme' || field === 'renderer') - fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); - - setCurrentBrew((prev)=>({ ...prev, ...metadata })); + if(useLocalStorage) { + if(field == 'text') localStorage.setItem(BREWKEY, value); + if(field == 'style') localStorage.setItem(STYLEKEY, value); + if(field == 'snippets') localStorage.setItem(SNIPKEY, value); + if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({ + renderer : value.renderer, + theme : value.theme, + lang : value.lang + })); + } }; const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ @@ -382,10 +388,7 @@ const EditPage = (props)=>{ { props = { brew : DEFAULT_BREW, @@ -28,9 +34,10 @@ const HomePage =(props)=>{ ...props }; - const [brew , setBrew] = useState(props.brew); + const [currentBrew , setCurrentBrew] = useState(props.brew); const [welcomeText , setWelcomeText] = useState(props.brew.text); const [error , setError] = useState(undefined); + const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1); const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); @@ -39,13 +46,15 @@ const HomePage =(props)=>{ const editorRef = useRef(null); + const useLocalStorage = false; + useEffect(()=>{ - fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme); + fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); }, []); const save = ()=>{ request.post('/api') - .send(brew) + .send(currentBrew) .end((err, res)=>{ if(err) { setError(err); @@ -72,8 +81,27 @@ const HomePage =(props)=>{ setCurrentBrewRendererPageNum(pageNumber); }; - const handleTextChange = (text)=>{ - setBrew((prevBrew) => ({ ...prevBrew, text })); + const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' + if (subfield == 'renderer' || subfield == 'theme') + fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); + + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length && (field == 'text' || field == 'snippets')) + setHTMLErrors(Markdown.validate(value)); + + if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); + else setCurrentBrew(prev => ({ ...prev, [field]: value })); + + if(useLocalStorage) { + if(field == 'text') localStorage.setItem(BREWKEY, value); + if(field == 'style') localStorage.setItem(STYLEKEY, value); + if(field == 'snippets') localStorage.setItem(SNIPKEY, value); + if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({ + renderer : value.renderer, + theme : value.theme, + lang : value.lang + })); + } }; const clearError = ()=>{ @@ -105,9 +133,9 @@ const HomePage =(props)=>{ { currentBrewRendererPageNum={currentBrewRendererPageNum} /> { /> -
+
Save current
diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 67ff227ae..b8c4e8100 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -20,10 +20,13 @@ import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; + const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; const METAKEY = 'HB_newPage_metadata'; -const SAVEKEY = `HB_editor_defaultSave_${global.account?.username || ''}`; +const SNIPKEY = 'HB_newPage_snippets'; +const SAVEKEYPREFIX = 'HB_editor_defaultSave_'; + const NewPage = (props) => { props = { @@ -43,6 +46,8 @@ const NewPage = (props) => { const editorRef = useRef(null); + const useLocalStorage = true; + useEffect(() => { document.addEventListener('keydown', handleControlKeys); loadBrew(); @@ -67,6 +72,7 @@ const NewPage = (props) => { brew.lang = metaStorage?.lang ?? brew.lang; } + const SAVEKEY = `${SAVEKEYPREFIX}${global.account?.username}`; const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY'; setCurrentBrew(brew); @@ -108,40 +114,27 @@ const NewPage = (props) => { setCurrentBrewRendererPageNum(pageNumber); }; - const handleTextChange = (text)=>{ + const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' + if (subfield == 'renderer' || subfield == 'theme') + fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); + //If there are HTML errors, run the validator on every change to give quick feedback - if(HTMLErrors.length) - HTMLErrors = Markdown.validate(text); + if(HTMLErrors.length && (field == 'text' || field == 'snippets')) + setHTMLErrors(Markdown.validate(value)); - setHTMLErrors(HTMLErrors); - setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); - localStorage.setItem(BREWKEY, text); - }; + if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); + else setCurrentBrew(prev => ({ ...prev, [field]: value })); - const handleStyleChange = (style) => { - setCurrentBrew(prevBrew => ({ ...prevBrew, style })); - localStorage.setItem(STYLEKEY, style); - }; - - const handleSnipChange = (snippet)=>{ - //If there are HTML errors, run the validator on every change to give quick feedback - if(HTMLErrors.length) - HTMLErrors = Markdown.validate(snippet); - - setHTMLErrors(HTMLErrors); - setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); - }; - - const handleMetaChange = (metadata, field = undefined) => { - if (field === 'theme' || field === 'renderer') - fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); - - setCurrentBrew(prev => ({ ...prev, ...metadata })); - localStorage.setItem(METAKEY, JSON.stringify({ - renderer : metadata.renderer, - theme : metadata.theme, - lang : metadata.lang - })); + if(useLocalStorage) { + if(field == 'text') localStorage.setItem(BREWKEY, value); + if(field == 'style') localStorage.setItem(STYLEKEY, value); + if(field == 'snippets') localStorage.setItem(SNIPKEY, value); + if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({ + renderer : value.renderer, + theme : value.theme, + lang : value.lang + })); + } }; const save = async () => { @@ -215,10 +208,7 @@ const NewPage = (props) => { { }; const connect = async (config)=>{ - return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false }) + return await Mongoose.connect(getMongoDBURL(config), { + retryWrites : false, + autoIndex : (config.get('local_environments').includes(config.get('node_env'))) + }) .catch((error)=>handleConnectionError(error)); }; diff --git a/server/homebrew.model.js b/server/homebrew.model.js index 2e74b1de2..ff371ee42 100644 --- a/server/homebrew.model.js +++ b/server/homebrew.model.js @@ -7,29 +7,29 @@ import zlib from '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: '' }, + googleId : { type: String, index: true }, + title : { type: String, default: '', index: true }, text : { type: String, default: '' }, textBin : { type: Buffer }, - pageCount : { type: Number, default: 1 }, + pageCount : { type: Number, default: 1, index: true }, description : { type: String, default: '' }, - tags : [String], + tags : { type: [String], index: true }, systems : [String], - lang : { type: String, default: 'en' }, - renderer : { type: String, default: '' }, - authors : [String], + lang : { type: String, default: 'en', index: true }, + renderer : { type: String, default: '', index: true }, + authors : { type: [String], index: true }, invitedAuthors : [String], - published : { type: Boolean, default: false }, - thumbnail : { type: String, default: '' }, + published : { type: Boolean, default: false, index: true }, + thumbnail : { type: String, default: '', index: true }, - createdAt : { type: Date, default: Date.now }, - updatedAt : { type: Date, default: Date.now }, - lastViewed : { type: Date, default: Date.now }, + createdAt : { type: Date, default: Date.now, index: true }, + updatedAt : { type: Date, default: Date.now, index: true }, + lastViewed : { type: Date, default: Date.now, index: true }, views : { type: Number, default: 0 }, - version : { type: Number, default: 1 }, + version : { type: Number, default: 1, index: true }, - lock : { type: Object } + lock : { type: Object, index: true } }, { versionKey: false }); HomebrewSchema.statics.increaseView = async function(query) { @@ -43,6 +43,8 @@ HomebrewSchema.statics.increaseView = async function(query) { return brew; }; +// STATIC FUNCTIONS + HomebrewSchema.statics.get = async function(query, fields=null){ const brew = await Homebrew.findOne(query, fields).orFail() .catch((error)=>{throw 'Can not find brew';}); @@ -63,6 +65,15 @@ HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, f return brews; }; +// INDEXES + +HomebrewSchema.index({ updatedAt: -1, lastViewed: -1 }); +HomebrewSchema.index({ published: 1, title: 'text' }); + +HomebrewSchema.index({ lock: 1, sparse: true }); +HomebrewSchema.path('lock.reviewRequested').index({ sparse: true }); + + const Homebrew = mongoose.model('Homebrew', HomebrewSchema); export { diff --git a/shared/naturalcrit/codeEditor/codeEditor.less b/shared/naturalcrit/codeEditor/codeEditor.less index 84a5c63f1..2a57ae8e6 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.less +++ b/shared/naturalcrit/codeEditor/codeEditor.less @@ -38,15 +38,6 @@ animation-duration : 0.4s; } - .CodeMirror-vscrollbar { - &::-webkit-scrollbar { width : 20px; } - &::-webkit-scrollbar-thumb { - width : 20px; - background : linear-gradient(90deg, #858585 15px, #808080 15px); - } - } - - //.cm-tab { // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right; //}