diff --git a/client/src/App.vue b/client/src/App.vue index 8d3b116..253a418 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -5,6 +5,8 @@ + + @@ -15,6 +17,7 @@ export default { components: { 'global-site-menu': require('@/components/GlobalSiteMenu.vue').default, + 'global-notification':require('@/components/GlobalNotificationComponent.vue').default }, data: function(){ return { diff --git a/client/src/assets/semantic-helper.css b/client/src/assets/semantic-helper.css index e1e83b7..a19ab28 100644 --- a/client/src/assets/semantic-helper.css +++ b/client/src/assets/semantic-helper.css @@ -21,6 +21,14 @@ --text_color: #3d3d3d; --outline_color: rgba(34,36,38,.15); --border_color: rgba(34,36,38,.20); + + /*Global purple menu styles */ + --menu-border: #534c68; + --menu-background: #221f2b; +} + +html { + scrollbar-width: none; } div.ui.basic.segment.no-fluf-segment { @@ -28,7 +36,7 @@ div.ui.basic.segment.no-fluf-segment { } /* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/ -body{ +body { color: var(--text_color); background-color: var(--background_color); font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif; @@ -101,6 +109,77 @@ a:hover { text-decoration: underline; } +/*// +// Purple Global Menu +//*/ +.note-menu { + width: 100%; + /*display: block;*/ + display: inline-table; + background: var(--menu-background); + color: white; + /*overflow: hidden;*/ + border: 1px solid var(--menu-border); + /*height: 50px;*/ +} +.note-menu > .nm-button { + padding: 10px 15px; + cursor: pointer; + text-align: center; + box-sizing: border-box; + font-size: 1.2em; + vertical-align: middle; + /*height: 40px;*/ + display: table-cell; + position: relative; +} +.nm-button i.icon { + margin: 0; +} +.nm-button span { + font-size: 0.9em; +} +.nm-button.right { + float: right; + border-left: 1px solid var(--menu-border); +} +.nm-button:hover { + background-color: #534c68; + color: white; +} +.nm-button + .nm-button { + border-left: 1px solid #534c68; +} +/*.shrink-icons-on-mobile.note-menu span { + display: none; +}*/ + +/* Shrink button text for mobile */ +@media only screen and (max-width: 740px) { + .note-menu .nm-button span { + font-size: 0.7em; + line-height: 0.4em; + margin-left: 0; + } + .nm-button i.icon { + width: 100%; + } + /*prevents buttons from being jammed into corners of round phones*/ + .shrink-icons-on-mobile.note-menu { + padding: 0 20px; + } + .shrink-icons-on-mobile .nm-button { + padding: 2px 3px; + } + .shrink-icons-on-mobile .nm-button i.icon { + font-size: 0.7em; + } +} + +/*// +// Purple Global Menu +//*/ + .note-status-indicator { position: absolute; width: 100px; @@ -117,16 +196,17 @@ a:hover { /* squire text styles */ .squire-box { border: none; - height: calc(100% - 60px); + height: calc(100% - 69px); box-sizing: border-box; - padding: 10px 15px 40px; + padding: 10px 15px 10px; background: transparent; overflow-x: scroll; /*color: var(--text_color);*/ font-size: 1.2em; line-height: 1.5em; word-wrap: break-word; - border-bottom: 1px solid #ccc; + /*border-bottom: 1px solid #ccc;*/ + scrollbar-width: none; } /*Makes the first line real big */ .squire-box p:first-child { diff --git a/client/src/components/ColorPicker.vue b/client/src/components/ColorPicker.vue index 64e4621..3c6ed3f 100644 --- a/client/src/components/ColorPicker.vue +++ b/client/src/components/ColorPicker.vue @@ -1,27 +1,17 @@ @@ -105,10 +187,15 @@ name: 'InputNotes', props: [ 'noteid', 'position' ], components:{ - 'note-tag-edit': require('@/components/NoteTagEdit.vue').default, - 'color-picker': require('@/components/ColorPicker.vue').default, - 'file-upload-button': require('@/components/FileUploadButton.vue').default, - 'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default, + 'note-tag-edit': () => import('@/components/NoteTagEdit.vue'), + 'color-picker': () => import('@/components/ColorPicker.vue'), + 'file-upload-button': () => import('@/components/FileUploadButton.vue'), + // 'delete-button': () => import('@/components/NoteDeleteButtonComponent.vue'), + 'side-slide-menu': () => import('@/components/SideSlideMenuComponent.vue'), + 'simple-attachment-note': () => import('@/components/SimpleAttachmentNoteComponent.vue'), + 'share-note-component': () => import('@/components/ShareNoteComponent.vue'), + + 'nm-button':require('@/components/NoteMenuButtonComponent.vue').default }, data(){ return { @@ -116,6 +203,8 @@ loadingMessage: 'Loading Note', currentNoteId: 0, noteText: '', + rawTextId: 0, + shareUsername: null, diffNoteText: '', statusText: 'Saved', lastNoteHash: null, @@ -130,7 +219,7 @@ styleObject: { 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, //Style object. Determines colors and badges sizeDown: false, //Used to animate close state - colorPickerVisible: false, + colorPickerLocation: null, tinymce: null, //Initialized editor instance @@ -145,6 +234,10 @@ usersOnNote: 0, extraToolbarsVisible: true, + showTagSlideMenu: false, + colorPickerVisible: false, + showFilesSideMenu: false, + showNoteOptions: false, } }, watch: { @@ -168,12 +261,13 @@ if(this.noteid == noteId && this.editor){ this.editor.moveCursorToEnd() this.editor.insertHTML(imageCode) + this.save() } }) }, beforeDestroy(){ - this.$io.emit('leave_room', this.noteid) + this.$io.emit('leave_room', this.rawTextId) document.removeEventListener('visibilitychange', this.checkForUpdatedNote) @@ -189,24 +283,7 @@ this.$nextTick(() => { this.loadNote(this.noteid) - - //Tell server to push this note into a room - this.$io.emit('join_room', this.noteid) - - this.$io.on('update_user_count', userCount => { - this.usersOnNote = userCount - }) - - //Server will hand deliver diffs from other notes to this one - this.$io.on('incoming_diff', incomingDiffData => { - this.patchText(incomingDiffData) - }) - - }) - - - }, methods: { initSquire(){ @@ -215,7 +292,7 @@ this.editor = new Squire( this.$refs.squirebox, {blockTag: 'p' }) this.setText(this.noteText) - //Open links when clicked in editor + //Click Event - Open links when clicked in editor or toggle checks this.editor.addEventListener('click', e => { //Link clicked in editor - open link @@ -284,6 +361,7 @@ }, //If nothing is selected, select the entire line selectLineIfNoSelect(){ + //Select entire line if range is not set let selection = this.editor.getSelection() @@ -297,6 +375,7 @@ } }, modifyFont(inSize){ + this.selectLineIfNoSelect() let fontInfo = this.editor.getFontInfo() @@ -338,6 +417,149 @@ this.editor.italic() } }, + undoCustom(){ + //The same as pressing CTRL + Z + // this.editor.focus() + // document.execCommand("undo", false, null) + this.editor.undo() + }, + uncheckAllListItems(){ + // + // Uncheck All List Items + // + + //Close menu if user is on mobile, then sort list + if(this.$store.getters.getIsUserOnMobile){ + this.showNoteOptions = false + } + + //Fetch the container + let container = document.getElementById('squire-id') + + Array.from( container.getElementsByClassName('active') ).forEach(item => { + item.classList.remove('active'); + }) + }, + deleteCompletedListItems(){ + // + // Delete Completed List Items + // + + //Close menu if user is on mobile, then sort list + if(this.$store.getters.getIsUserOnMobile){ + this.showNoteOptions = false + } + + //Fetch the container + let container = document.getElementById('squire-id') + + //Go through each item, on first level, look for Unordered Lists + container.childNodes.forEach( (node) => { + if(node.nodeName == 'UL'){ + + //Create two categories, done and not done list items + let undoneElements = document.createDocumentFragment() + + //Go through each item in each list we found + node.childNodes.forEach( (checkListItem, index) => { + + //Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together + if(checkListItem.nodeName == 'UL'){ + return + } + + //Check if list item has active class + const checkedItem = checkListItem.classList.contains('active') + + //Check if the next item is a list, Keep lists with intented items together + let sublist = null + if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){ + sublist = node.childNodes[index+1] + } + + //Push checked items and their sub lists to the done set + if(!checkedItem){ + + undoneElements.appendChild( checkListItem.cloneNode(true) ) + if(sublist){ + undoneElements.appendChild( sublist.cloneNode(true) ) + } + + } + + }) + + //Remove all HTML from node, push unfinished items, then finished below them + node.innerHTML = null + node.appendChild(undoneElements) + + } + }) + }, + sortList(){ + // + // Sort list, checked at the bottom, unchecked at the top + // + + //Close menu if user is on mobile, then sort list + if(this.$store.getters.getIsUserOnMobile){ + this.showNoteOptions = false + } + + //Fetch the container + let container = document.getElementById('squire-id') + + //Go through each item, on first level, look for Unordered Lists + container.childNodes.forEach( (node) => { + if(node.nodeName == 'UL'){ + + //Create two categories, done and not done list items + let doneElements = document.createDocumentFragment() + let undoneElements = document.createDocumentFragment() + + //Go through each item in each list we found + node.childNodes.forEach( (checkListItem, index) => { + + //Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together + if(checkListItem.nodeName == 'UL'){ + return + } + + //Check if list item has active class + const checkedItem = checkListItem.classList.contains('active') + + //Check if the next item is a list, Keep lists with intented items together + let sublist = null + if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){ + sublist = node.childNodes[index+1] + } + + //Push checked items and their sub lists to the done set + if(checkedItem){ + + doneElements.appendChild( checkListItem.cloneNode(true) ) + if(sublist){ + doneElements.appendChild( sublist.cloneNode(true) ) + } + + } else { + + undoneElements.appendChild( checkListItem.cloneNode(true) ) + if(sublist){ + undoneElements.appendChild( sublist.cloneNode(true) ) + } + } + + }) + + //Remove all HTML from node, push unfinished items, then finished below them + node.innerHTML = null + node.appendChild(undoneElements) + node.appendChild(doneElements) + + } + }) + }, setText(inText){ this.editor.setHTML(inText) @@ -349,13 +571,16 @@ return this.editor.getHTML() }, showColorPicker(event){ + this.colorPickerVisible = !this.colorPickerVisible this.colorPickerLocation = {'x':event.clientX, 'y':event.clientY} }, openEditAttachment(){ + this.$router.push('/attachments/note/'+this.currentNoteId) }, onTogglePinned(){ + if(this.pinned == 0){ this.pinned = 1 } else { @@ -366,6 +591,7 @@ this.save() }, onToggleArchived(){ + if(this.archived == 0){ this.archived = 1 } else { @@ -376,6 +602,7 @@ this.save() }, onCloseColorChanger(){ + this.colorPickerVisible = false }, onChangeColor(newStyleObject){ @@ -403,6 +630,8 @@ //Set up local data vm.currentNoteId = noteId + this.rawTextId = response.data.rawTextId + this.shareUsername = response.data.shareUsername vm.noteText = response.data.text vm.diffNoteText = response.data.text @@ -423,7 +652,8 @@ this.loading = false vm.$nextTick(() => { - // this.initTinyMce() + + this.setupWebSockets() this.initSquire() }) @@ -473,8 +703,8 @@ // console.log(patch_text) this.$io.emit('note_diff', { - id:this.currentNoteId, - diff:patch_text + id: this.rawTextId, + diff: patch_text }) @@ -575,9 +805,6 @@ }, 20) // } - - - }, xpath(el) { //Skip things we can't use @@ -592,12 +819,13 @@ const sames = [].filter.call(el.parentNode.children, function (x) { return x.tagName == el.tagName }) return this.xpath(el.parentNode) + '/' + el.tagName.toLowerCase() + (sames.length > 1 ? '['+([].indexOf.call(sames, el)+1)+']' : '') }, - getElementByXPath(xpath) { + getElementByXPath(xpath){ + return new XPathEvaluator() .createExpression(xpath) .evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE) .singleNodeValue }, - onKeyup(event){ + onKeyup(event){ this.statusText = 'Save' @@ -661,7 +889,6 @@ }) // }, 300) }) - }, checkForUpdatedNote(){ @@ -669,7 +896,7 @@ //If user leaves page then returns to page, reload the first batch if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){ - console.log('Checking for note updates after visibility change.') + // console.log('Checking for note updates after visibility change.') const postData = { noteId:this.currentNoteId, @@ -726,7 +953,19 @@ return }, 300) }) + }, + setupWebSockets(){ + //Tell server to push this note into a room + this.$io.emit('join_room', this.rawTextId) + + this.$io.on('update_user_count', userCount => { + this.usersOnNote = userCount + }) + //Server will hand deliver diffs from other notes to this one + this.$io.on('incoming_diff', incomingDiffData => { + this.patchText(incomingDiffData) + }) } } } @@ -734,38 +973,6 @@ \ No newline at end of file diff --git a/client/src/components/SearchInput.vue b/client/src/components/SearchInput.vue index ab63dae..1870583 100644 --- a/client/src/components/SearchInput.vue +++ b/client/src/components/SearchInput.vue @@ -2,7 +2,7 @@
- +
diff --git a/client/src/components/ShareNoteComponent.vue b/client/src/components/ShareNoteComponent.vue new file mode 100644 index 0000000..292ff3d --- /dev/null +++ b/client/src/components/ShareNoteComponent.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/client/src/components/SideSlideMenuComponent.vue b/client/src/components/SideSlideMenuComponent.vue new file mode 100644 index 0000000..ed0e8f0 --- /dev/null +++ b/client/src/components/SideSlideMenuComponent.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/client/src/components/SimpleAttachmentNoteComponent.vue b/client/src/components/SimpleAttachmentNoteComponent.vue new file mode 100644 index 0000000..95d09f5 --- /dev/null +++ b/client/src/components/SimpleAttachmentNoteComponent.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/client/src/pages/LoginPage.vue b/client/src/pages/LoginPage.vue index 04fbf24..3e7781a 100644 --- a/client/src/pages/LoginPage.vue +++ b/client/src/pages/LoginPage.vue @@ -71,11 +71,12 @@ //Redirect user to notes section after login vm.$router.push('/notes') } else { + this.$bus.$emit('notification', 'Incorrect Username or Password') vm.$store.commit('destroyLoginToken') } }) .catch(error => { - console.log('There was an error with log in request') + this.$bus.$emit('notification', 'Incorrect Username or Password') }) } } diff --git a/client/src/pages/NotesPage.vue b/client/src/pages/NotesPage.vue index 667199a..d027acf 100644 --- a/client/src/pages/NotesPage.vue +++ b/client/src/pages/NotesPage.vue @@ -11,13 +11,12 @@
-
- Reset Filters + Back to All Notes
@@ -36,6 +35,7 @@
+

Tags

{{ucWords(tag.text)}} {{tag.usages}} @@ -388,6 +388,7 @@ }, //Try to close notes on URL hash change /notes/open/123 to /notes - parse 123, close note id 123 hashChangeAction(event){ + //Clean up path of hash change let path = window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.hash let newPath = event.newURL.replace(path,'') diff --git a/server/models/Attachment.js b/server/models/Attachment.js index 334c07e..112af8f 100644 --- a/server/models/Attachment.js +++ b/server/models/Attachment.js @@ -43,7 +43,7 @@ Attachment.textSearch = (userId, searchTerm) => { }) } -Attachment.search = (userId, noteId) => { +Attachment.search = (userId, noteId, attachmentType) => { return new Promise((resolve, reject) => { let params = [userId] @@ -54,6 +54,11 @@ Attachment.search = (userId, noteId) => { params.push(noteId) } + if(Number.isInteger(attachmentType)){ + query += 'AND attachment_type = ? ' + params.push(attachmentType) + } + query += 'ORDER BY last_indexed DESC ' db.promise() diff --git a/server/models/Note.js b/server/models/Note.js index 8ac786c..3f31e30 100644 --- a/server/models/Note.js +++ b/server/models/Note.js @@ -14,6 +14,35 @@ let Note = module.exports = {} const gm = require('gm') +// -------------- + +Note.migrateNoteTextToNewTable = () => { + + return new Promise((resolve, reject) => { + db.promise() + .query('SELECT id, text FROM note WHERE note_raw_text_id IS NULL') + .then((rows, fields) => { + rows[0].forEach( ({id, text}) => { + + db.promise() + .query('INSERT INTO note_raw_text (text) VALUES (?)', [text]) + .then((rows, fields) => { + + db.promise() + .query(`UPDATE note SET note_raw_text_id = ? WHERE (id = ?)`, [rows[0].insertId, id]) + .then((rows, fields) => { + + return 'Nice' + }) + }) + + }) + + resolve('Its probably running... :-D') + }) + }) +} + Note.fixAttachmentThumbnails = () => { const filePath = '../staticFiles/' db.promise() @@ -66,6 +95,8 @@ Note.stressTest = () => { }) } +// -------------- + Note.create = (userId, noteText, quickNote = 0) => { return new Promise((resolve, reject) => { @@ -74,34 +105,18 @@ Note.create = (userId, noteText, quickNote = 0) => { const created = Math.round((+new Date)/1000) db.promise() - .query('INSERT INTO note (user_id, text, updated, created, quick_note) VALUES (?,?,?,?,?)', - [userId, noteText, created, created, quickNote]) - .then((rows, fields) => { - // New notes are empty, don't add to solr index - // Note.reindex(userId, rows[0].insertId) - resolve(rows[0].insertId) //Only return the new note ID when creating a new note + .query(`INSERT INTO note_raw_text (text) VALUE ('')`) + .then( (rows, fields) => { + + const rawTextId = rows[0].insertId + + return db.promise() + .query('INSERT INTO note (user_id, note_raw_text_id, updated, created, quick_note) VALUES (?,?,?,?,?)', + [userId, rawTextId, created, created, quickNote]) }) - .catch(console.log) - }) -} - -Note.reindexAll = () => { - return new Promise((resolve, reject) => { - - db.promise() - .query(` - - SELECT id, user_id FROM note; - - `) .then((rows, fields) => { - console.log() - - rows[0].forEach(item => { - Note.reindex(item.user_id, item.id) - }) - - resolve(true) + // Indexing is done on save + resolve(rows[0].insertId) //Only return the new note ID when creating a new note }) .catch(console.log) }) @@ -154,8 +169,23 @@ Note.update = (userId, noteId, noteText, color, pinned, archived) => { const now = Math.round((+new Date)/1000) db.promise() - .query('UPDATE note SET text = ?, pinned = ?, archived = ?, updated = ?, color = ? WHERE id = ? AND user_id = ? LIMIT 1', - [noteText, pinned, archived, now, color, noteId, userId]) + .query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId]) + .then((rows, fields) => { + + const textId = rows[0][0]['note_raw_text_id'] + + //Update Note text + return db.promise() + .query('UPDATE note_raw_text SET text = ? WHERE id = ?', [noteText, textId]) + }) + .then( (rows, fields) => { + + //Update other note attributes + return db.promise() + .query('UPDATE note SET pinned = ?, archived = ?, updated = ?, color = ? WHERE id = ? AND user_id = ? LIMIT 1', + [pinned, archived, now, color, noteId, userId]) + + }) .then((rows, fields) => { //Async solr note reindex @@ -171,7 +201,6 @@ Note.update = (userId, noteId, noteText, color, pinned, archived) => { }) } - // // Delete a note and all its remaining parts // @@ -179,16 +208,54 @@ Note.delete = (userId, noteId) => { return new Promise((resolve, reject) => { // - // Delete, note, text index, tags + // Delete, note, text, search index and associated tags // Leave the attachments, they can be deleted on their own - // + // Leave Tags, their text is shared - db.promise().query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId,userId]) + let rawTextId = null + let noteTextCount = 0 + + // Lookup the note text ID, we need this to count usages + db.promise() + .query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId]) .then((rows, fields) => { - return db.promise().query('DELETE FROM note_text_index WHERE note_text_index.note_id = ? AND note_text_index.user_id = ?', [noteId,userId]) + + //Save the raw text ID + rawTextId = rows[0][0]['note_raw_text_id'] + + return db.promise() + .query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId]) }) .then((rows, fields) => { - return db.promise().query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId]) + + //Save the number of times the note is used + noteTextCount = rows[0][0]['count'] + + //Don't delete text if its shared + if(noteTextCount == 1){ + //If text is only used on one note, delete it (its not shared) + return db.promise() + .query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId]) + } else { + return new Promise((resolve, reject) => { + resolve(true) + }) + } + }) + .then( results => { + // Delete Note entry for this user. + return db.promise() + .query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId, userId]) + }) + .then((rows, fields) => { + // Delete search index + return db.promise() + .query('DELETE FROM note_text_index WHERE note_text_index.note_id = ? AND note_text_index.user_id = ?', [noteId,userId]) + }) + .then((rows, fields) => { + // delete tags + return db.promise() + .query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId]) }) .then((rows, fields) => { resolve(true) @@ -251,9 +318,19 @@ Note.get = (userId, noteId) => { return new Promise((resolve, reject) => { db.promise() .query(` - SELECT note.text, note.updated, note.pinned, note.archived, note.color, count(distinct attachment.id) as attachment_count + SELECT + note_raw_text.text, + note.updated, + note.pinned, + note.archived, + note.color, + count(distinct attachment.id) as attachment_count, + note.note_raw_text_id as rawTextId, + user.username as shareUsername FROM note + JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) LEFT JOIN attachment ON (note.id = attachment.note_id) + LEFT JOIN user ON (note.share_user_id = user.id) WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId,noteId]) .then((rows, fields) => { @@ -265,6 +342,7 @@ Note.get = (userId, noteId) => { }) } +//Public note share action -> may not be used Note.getShared = (noteId) => { return new Promise((resolve, reject) => { db.promise() @@ -356,20 +434,33 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => { // Base of the query, modified with fastFilters // Add to query for character counts -> CHAR_LENGTH(note.text) as chars + let searchParams = [userId] let noteSearchQuery = ` SELECT note.id, - SUBSTRING(note.text, 1, 1500) as text, + SUBSTRING(note_raw_text.text, 1, 1500) as text, updated, color, count(distinct note_tag.id) as tag_count, count(distinct attachment.id) as attachment_count, note.pinned, - note.archived + note.archived, + GROUP_CONCAT(DISTINCT tag.text) as tags, + GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs, + shareUser.username as username FROM note + JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) LEFT JOIN note_tag ON (note.id = note_tag.note_id) + LEFT JOIN tag ON (tag.id = note_tag.tag_id) LEFT JOIN attachment ON (note.id = attachment.note_id) + LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id) WHERE note.user_id = ?` - let searchParams = [userId] + + //Show shared notes + if(fastFilters.onlyShowSharedNotes == 1){ + noteSearchQuery += ' AND note.share_user_id IS NOT NULL' //Show Archived + } else { + noteSearchQuery += ' AND note.share_user_id IS NULL' + } //If text search returned results, limit search to those ids if(textSearchIds.length > 0){ @@ -418,15 +509,15 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => { // Always prioritize pinned notes in searches. //Default Sort, order by last updated - let defaultOrderBy = ' ORDER BY note.pinned DESC, updated DESC, created DESC, opened DESC, id DESC' + let defaultOrderBy = ' ORDER BY note.pinned DESC, note.updated DESC, note.created DESC, note.opened DESC, id DESC' //Order by Last Created Date if(fastFilters.lastCreated == 1){ - defaultOrderBy = ' ORDER BY note.pinned DESC, created DESC, updated DESC, opened DESC, id DESC' + defaultOrderBy = ' ORDER BY note.pinned DESC, note.created DESC, note.updated DESC, note.opened DESC, id DESC' } //Order by last Opened Date if(fastFilters.lastOpened == 1){ - defaultOrderBy = ' ORDER BY note.pinned DESC, opened DESC, updated DESC, created DESC, id DESC' + defaultOrderBy = ' ORDER BY note.pinned DESC, opened DESC, note.updated DESC, note.created DESC, id DESC' } //Append Order by to query diff --git a/server/models/ShareNote.js b/server/models/ShareNote.js new file mode 100644 index 0000000..bf1f7f0 --- /dev/null +++ b/server/models/ShareNote.js @@ -0,0 +1,127 @@ +// +// All actions in noteController.js +// + + +const db = require('@config/database') + +const Note = require('@models/Note') + +let ShareNote = module.exports = {} + +// Share a note with a user, given the correct username +ShareNote.addUser = (userId, noteId, rawTextId, username) => { + return new Promise((resolve, reject) => { + + let shareUserId = null + let newNoteShare = null + + //Check that user actually exists + db.promise().query(`SELECT id FROM user WHERE username = ?`, [username]) + .then((rows, fields) => { + + if(rows[0].length == 0){ + throw new Error('User Does Not Exist') + } + + shareUserId = rows[0][0]['id'] + + //Check if note has already been added for user + return db.promise() + .query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, rawTextId]) + + }) + .then((rows, fields) => { + + if(rows[0].length != 0){ + throw new Error('User Already Has Note') + } + + //Lookup note to share with user, clone this data to create users new note + return db.promise() + .query(`SELECT * FROM note WHERE id = ? LIMIT 1`, [noteId]) + }) + .then((rows, fields) => { + + newNoteShare = rows[0][0] + + //Modify note with the share attributes we want + delete newNoteShare['id'] + delete newNoteShare['opened'] + newNoteShare['share_user_id'] = userId //User who shared the note + newNoteShare['user_id'] = shareUserId //User who gets note + + //Setup db colums, db values and number of '?' to put into prepared statement + let dbColumns = [] + let dbValues = [] + let escapeChars = [] + + //Pull out all the data we need from object to create prepared statemnt + Object.keys(newNoteShare).forEach( key => { + escapeChars.push('?') + dbColumns.push(key) + dbValues.push(newNoteShare[key]) + }) + + //Stick all the note value back into query, insert updated note + return db.promise() + .query(`INSERT INTO note (${dbColumns.join()}) VALUES (${escapeChars.join()})`, dbValues) + }) + .then((rows, fields) => { + + //Success! + return resolve(true) + }) + .catch(error => { + // console.log(error) + resolve(false) + }) + }) +} + +// Get users who see a shared note +ShareNote.getUsers = (userId, rawTextId) => { + return new Promise((resolve, reject) => { + db.promise() + .query(` + SELECT username, note.id as noteId + FROM note + JOIN user ON (user.id = note.user_id) + WHERE note_raw_text_id = ? + AND share_user_id = ? + AND user_id != ? + `, [rawTextId, userId, userId]) + .then((rows, fields) => { + + //Return a list of user names + return resolve (rows[0]) + }) + }) +} + +// Remove a user from a shared note +ShareNote.removeUser = (userId, noteId) => { + return new Promise((resolve, reject) => { + + //note.id = noteId, share_user_id = userId + db.promise() + .query('SELECT user_id FROM note WHERE id = ? AND share_user_id = ?', [noteId, userId]) + .then( (rows, fields) => { + + //User has shared this note, with this user + if(rows[0].length == 1 && Number.isInteger(rows[0][0]['user_id'])){ + + Note.delete(rows[0][0]['user_id'], noteId) + .then( result => { + resolve(result) + }) + + } else { + return resolve(false) + } + + }) + + + }) +} \ No newline at end of file diff --git a/server/models/Tag.js b/server/models/Tag.js index a7e17c4..9b50bd8 100644 --- a/server/models/Tag.js +++ b/server/models/Tag.js @@ -10,7 +10,7 @@ Tag.userTags = (userId) => { JOIN note_tag ON tag.id = note_tag.tag_id WHERE note_tag.user_id = ? GROUP BY tag.id - ORDER BY usages DESC + ORDER BY id DESC `, [userId]) .then( (rows, fields) => { resolve(rows[0]) @@ -98,39 +98,50 @@ Tag.add = (tagText) => { }) } +// +// Get all tags AND tags associated to note +// Tag.get = (userId, noteId) => { return new Promise((resolve, reject) => { - //Update last opened date of note - const now = Math.round((+new Date)/1000) - db.promise() - .query('UPDATE note SET opened = ? WHERE id = ? AND user_id = ? LIMIT 1', [now, noteId, userId]) - .then((rows, fields) => {}) + Tag.userTags(userId).then(userTags => { + db.promise() + .query(`SELECT tag_id as tagId, id as entryId + FROM note_tag + WHERE user_id = ? AND note_id = ?;`, [userId, noteId]) + .then((rows, fields) => { - db.promise() - .query(`SELECT note_tag.id, tag.text FROM note_tag - JOIN tag ON (tag.id = note_tag.tag_id) - WHERE user_id = ? AND note_id = ?;`, [userId, noteId]) - .then((rows, fields) => { - resolve(rows[0]) //Return all tags found by query + //pull IDs out of returned results + // let ids = rows[0].map( item => {}) + + resolve({'noteTagIds':rows[0], 'allTags':userTags }) //Return all tags found by query + }) + .catch(console.log) }) - .catch(console.log) + }) } +// +// Get all tags for a note and concatinate into a string 'all, tags, like, this' +// Tag.string = (userId, noteId) => { return new Promise((resolve, reject) => { - Tag.get(userId, noteId).then(tagArray => { + db.promise() + .query(`SELECT GROUP_CONCAT(DISTINCT tag.text SEPARATOR ', ') as text + FROM note_tag + JOIN tag ON note_tag.tag_id = tag.id + WHERE user_id = ? AND note_id = ?;`, [userId, noteId]) + .then((rows, fields) => { - let tagString = '' - tagArray.forEach( (tag, i) => { - if(i > 0){ tagString += ',' } - tagString += tag.text - }) - //Output comma delimited list of tag strings - resolve(tagString) + let finalText = rows[0][0]['text'] + if(finalText == null){ + finalText = '' + } + return resolve(finalText) //Return all tags found by query }) + .catch(console.log) }) } diff --git a/server/routes/attachmentController.js b/server/routes/attachmentController.js index 9cbf697..819850f 100644 --- a/server/routes/attachmentController.js +++ b/server/routes/attachmentController.js @@ -18,7 +18,7 @@ router.use(function setUserId (req, res, next) { }) router.post('/search', function (req, res) { - Attachment.search(userId, req.body.noteId) + Attachment.search(userId, req.body.noteId, req.body.attachmentType) .then( data => res.send(data) ) }) diff --git a/server/routes/noteController.js b/server/routes/noteController.js index b4be6e4..facd32f 100644 --- a/server/routes/noteController.js +++ b/server/routes/noteController.js @@ -2,6 +2,7 @@ var express = require('express') var router = express.Router() let Notes = require('@models/Note'); +let ShareNote = require('@models/ShareNote'); let userId = null let socket = null @@ -18,6 +19,9 @@ router.use(function setUserId (req, res, next) { next() }) +// +// Note actions +// router.post('/get', function (req, res) { req.io.emit('welcome_homie', 'Welcome, dont poop from excitement') Notes.get(userId, req.body.noteId) @@ -58,15 +62,35 @@ router.post('/difftext', function (req, res) { }) }) +// +// Share Note Actions +// +router.post('/getshareusers', function (req, res) { + ShareNote.getUsers(userId, req.body.rawTextId) + .then(results => res.send(results)) +}) + +router.post('/shareadduser', function (req, res) { + ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username) + .then(results => res.send(results)) +}) + +router.post('/shareremoveuser', function (req, res) { + ShareNote.removeUser(userId, req.body.noteId) + .then(results => res.send(results)) +}) + + +// +// Testing Action +// //Reindex all notes. Not a very good function, not public router.get('/reindex5yu43prchuj903mrc', function (req, res) { - Notes.fixAttachmentThumbnails() - res.send('A whole mess is going on in the background') + Notes.migrateNoteTextToNewTable().then(status => { + return res.send(status) + }) - // Notes.stressTest().then( i => { - // // Notes.reindexAll().then( result => res.send('Welcome to reindex...oh god')) - // }) }) diff --git a/staticFiles/.gitignore b/staticFiles/.gitignore new file mode 100644 index 0000000..6ea6a2f --- /dev/null +++ b/staticFiles/.gitignore @@ -0,0 +1,4 @@ +* +*/ +!.gitignore +!assets