Created a uniform menu for notes that works on mobile

Added list sorting
Added shared notes
Fixed some little bugs here and there
This commit is contained in:
Max G 2020-02-10 17:44:43 +00:00
parent 2828cc9462
commit de646cf1de
23 changed files with 1395 additions and 330 deletions

View File

@ -5,6 +5,8 @@
<router-view />
<global-notification />
</div>
</template>
@ -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 {

View File

@ -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 {
@ -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 {

View File

@ -1,26 +1,16 @@
<template>
<div>
<div class="color-picker" :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
<div class="floating-buttons">
<div :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
<div class="ui basic segment">
<div class="ui fluid buttons">
<div class="ui compact button" v-on:click="closeThisBitch">
<i class="close icon"></i>
Close
</div>
<div class="ui compact button" v-on:click="clearStyles">
<div class="ui grid">
<div class="ui sixteen wide center aligned column">
<div class="ui fluid button" v-on:click="clearStyles">
<i class="refresh icon"></i>
Clear All Styles
</div>
</div>
</div>
</div>
<div class="ui basic segment">
<div class="ui grid">
<div class="row">
<div class="sixteen wide column">
@ -62,8 +52,7 @@
</div>
</div>
<div class="shade-boy" v-on:click="closeThisBitch"></div>
</div>
</template>
<script>
@ -157,43 +146,6 @@
}
</script>
<style type="text/css" scoped>
.shade-boy {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 250;
cursor: pointer;
}
.floating-buttons {
position: fixed;
top: 0;
z-index: 444;
right: 20px;
left: calc(30% + 10px)
}
.color-picker {
color: var(--text_color);
background-color: var(--background_color);
position: fixed;
/*height: 100px;*/
top: 0;
right: 0;
left: 30%;
bottom: 0;
padding: 10px;
z-index: 500;
border-left: 1px solid;
border-color: var(--border_color) !important;
overflow-x: scroll;
}
.icon-button {
height: 40px;
width: 14.2%;

View File

@ -7,11 +7,9 @@
</style>
<template>
<form>
<form data-tooltip="Upload File" data-inverted>
<label :for="`upfile-${noteId}`" class="clickable">
<i class="upload icon"></i>
<span v-if="uploadPercentage != 0">{{uploadPercentage}}%</span>
<span v-else>Upload</span>
<nm-button icon="upload" :text="uploadStatusText"/>
</label>
<input class="hidden-up" type="file" :id="`upfile-${noteId}`" ref="file" v-on:change="handleFileUpload()" />
<!-- <button v-if="file" v-on:click="uploadFileToServer()">Submit</button> -->
@ -23,10 +21,13 @@
export default {
name: 'FileUploadButton',
props: [ 'noteId' ],
components: {
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
},
data () {
return {
file: null,
uploadPercentage: 0,
uploadStatusText: 'Upload',
}
},
mounted(){
@ -44,12 +45,12 @@
formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: ( progressEvent ) => {
this.uploadPercentage = parseInt(
this.uploadStatusText = parseInt(
Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) )
}
}
).then(results => {
this.uploadPercentage = 'Working'
this.uploadStatusText = 'Working'
this.file = null
// console.log('File upload results')
@ -58,17 +59,19 @@
const name = results.data.fileName
const location = results.data.goodFileName
this.$bus.$emit('notification', 'Processing Upload')
if(name && location){
this.uploadPercentage = 0
this.uploadStatusText = 'Upload File'
const imageCode = `<img alt="image" src="/api/static/${location}">`
const imageCode = `<img alt="image" src="/api/static/thumb_${location}">`
this.$bus.$emit('new_file_upload', {noteId: this.noteId, imageCode})
}
})
.catch(results => {
this.uploadPercentage = 0
this.uploadStatusText = 0
})
},
handleFileUpload() {

View File

@ -0,0 +1,82 @@
<style type="text/css" scoped>
.popup-body {
position: fixed;
bottom: 15px;
right: 15px;
min-height: 50px;
min-width: 200px;
max-width: calc(100% - 20px);
z-index: 1002;
border-top: 2px solid #21ba45;
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
border-top-right-radius: 4px;
border-top-left-radius: 4px;
color: var(--text_color);
background-color: var(--background_color);
}
.popup-row {
padding: 1em 5px;
cursor: pointer;
white-space: nowrap;
}
.popup-row > span {
width: calc(100% - 50px);
display: inline-block;
text-align: center;
box-sizing: border-box;
padding: 0 10px 0;
font-size: 1.25em;
}
.popup-row + .popup-row {
border-top: 1px solid #000;
}
</style>
<template>
<div class="popup-body" v-on:click="dismiss" v-if="notifications.length > 0">
<div class="popup-row" v-for="item in notifications">
<i class="disabled angle left icon"></i>
<span>{{ item }}</span>
<i class="disabled angle right icon"></i>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'GlobalNotificationComponent',
data () {
return {
notifications: [],
totalTimeout: null,
}
},
beforeMount(){
this.$bus.$on('notification', info => {
this.displayNotification(info)
})
},
mounted(){
},
methods: {
displayNotification(newNotification){
this.notifications.push(newNotification)
clearTimeout(this.totalTimeout)
this.totalTimeout = setTimeout(() => {
this.dismiss()
}, 4000)
},
dismiss(){
this.notifications = []
}
}
}
</script>

View File

@ -163,6 +163,12 @@
</div>
</div>
<div class="menu-section" v-if="loggedIn">
<div v-on:click="updateFastFilters(3)" class="menu-item menu-button">
<i class="mail icon"></i>Inbox
</div>
</div>
<div class="menu-section" v-if="loggedIn">
<div v-on:click="updateFastFilters(2)" class="menu-item menu-button">
<i class="archive icon"></i>Archived
@ -239,6 +245,7 @@
if(this.mobile){
this.menuOpen = false
}
},
computed: {
loggedIn () {
@ -279,6 +286,7 @@
})
},
destroyLoginToken() {
this.$bus.$emit('notification', 'Logged Out')
this.$store.commit('destroyLoginToken')
this.$router.push('/')
},
@ -309,6 +317,7 @@
'withLinks', // 'Only Show Notes with Links'
'withTags', // 'Only Show Notes with Tags'
'onlyArchived', //'Only Show Archived Notes'
'onlyShowSharedNotes', //Only show shared notes
]
let filter = {}

View File

@ -17,30 +17,23 @@
<div class="note-menu">
<div class="nm-button" v-on:click="close">
<i class="close icon"></i>
</div>
<div class="nm-button" v-on:click="toggleList('ol')">
<i class="list ol icon"></i>
</div>
<div class="nm-button" v-on:click="toggleList('ul')">
<i class="tasks icon"></i>
</div>
<div class="nm-button" v-on:click="toggleBold()">
<i class="bold icon"></i>
</div>
<nm-button v-on:click.native="close" icon="close" />
<div class="nm-button" v-on:click="toggleItalic()">
<i class="quote left icon"></i>
</div>
<nm-button v-on:click.native="toggleList('ol')" icon="list ol" />
<div class="nm-button" v-on:click="modifyFont('2.286em') ">
<i class="text height icon"></i>
</div>
<nm-button v-on:click.native="toggleList('ul')" icon="tasks" />
<div v-if="usersOnNote > 1" class="nm-button">
<i class="green user circle icon"></i>{{usersOnNote}}
</div>
<nm-button v-on:click.native="toggleBold()" icon="bold" />
<nm-button v-on:click.native="toggleItalic()" icon="quote left" />
<nm-button v-on:click.native="modifyFont('1.4em')" icon="text height" />
<nm-button v-on:click.native="undoCustom()" icon="undo" />
<nm-button v-if="usersOnNote > 1" icon="green user circle" />
<nm-button icon="ellipsis horizontal" v-on:click.native="showNoteOptions = !showNoteOptions" />
</div>
@ -51,47 +44,136 @@
<div class="ui green button">{{statusText}}</div>
</span>
<!-- Note options on the bottom of note -->
<div class="all-settings" :class="{ 'low-settings':!extraToolbarsVisible }">
<div class="note-menu shrink-icons-on-mobile">
<!-- Pin Button -->
<nm-button
v-on:click.native="onToggleArchived"
:icon="(archived == 1)?'green archive':'archive'"
:text="(archived == 1)?'Archived':'Archive'"
tip="Show in archive"
:showText="true"
></nm-button>
<!-- archive button -->
<nm-button
v-on:click.native="onTogglePinned"
:icon="(pinned == 1)?'green pin':'pin'"
:text="(pinned == 1)?'Pinned':'Pin'"
tip="Pin to top of list"
:showText="true"
></nm-button>
<!-- colors button -->
<nm-button
v-on:click.native="showColorPicker"
icon="paint brush"
text="Colors"
tip="Colors"
></nm-button>
<!-- add images panel -->
<nm-button
v-on:click.native="showFilesSideMenu = !showFilesSideMenu"
icon="image"
text="Images"
tip="Images"
></nm-button>
<!-- Tags -->
<nm-button
v-on:click.native="showTagSlideMenu = !showTagSlideMenu"
icon="tags"
text="Tags"
tip="Tags"
></nm-button>
<!-- file upload button -->
<file-upload-button
class="nm-button"
:noteId="noteid" />
<!-- files button -->
<nm-button
v-on:click.native="openEditAttachment"
icon="folder"
text="Files"
tip="Files on Note"
:showText="true"
></nm-button>
</div>
</div>
<!-- Side slide menus for colors, tags and images -->
<side-slide-menu v-if="colorPickerVisible" v-on:close="colorPickerVisible = false" name="colors">
<color-picker
v-if="colorPickerVisible"
:location="colorPickerLocation"
@changeColor="onChangeColor"
@close="onCloseColorChanger"
:style-object="styleObject"
/>
</side-slide-menu>
<!-- Note options on the bottom of note -->
<div class="all-settings" :class="{ 'low-settings':!extraToolbarsVisible }">
<div class="note-menu">
<!-- <note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/><br> -->
<!-- Pin Button -->
<div @click="onToggleArchived" class="nm-button">
<i class="archive icon" :class="{green:(archived == 1)}"></i>
{{(archived == 1)?'Archived':'Archive'}}
<side-slide-menu v-if="showTagSlideMenu" v-on:close="showTagSlideMenu = false" name="tags">
<div class="ui basic segment">
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/>
</div>
<!-- archive button -->
<div @click="onTogglePinned" class="nm-button">
<i class="pin icon" :class="{green:(pinned == 1)}"></i>
{{(pinned == 1)?'Pinned':'Pin'}}
</div>
<!-- colors button -->
<span class="nm-button" v-on:click="showColorPicker">
<i class="paint brush icon"></i>
Colors
</span>
</side-slide-menu>
<!-- attachment button -->
<div class="nm-button" v-on:click="openEditAttachment">
<i class="folder icon"></i> Files
<side-slide-menu v-if="showFilesSideMenu" v-on:close="showFilesSideMenu = false" name="images">
<div class="ui basic segment">
<simple-attachment-note
v-on:close="showFilesSideMenu = false"
:note-id="noteid"
:squire-editor="editor">
</simple-attachment-note>
</div>
<!-- file upload button -->
<file-upload-button class="nm-button" :noteId="noteid" />
</side-slide-menu>
<side-slide-menu v-if="showNoteOptions" v-on:close="showNoteOptions = false" name="note-options">
<div class="ui basic padded segment">
<div class="ui grid">
<div class="sixteen wide column">
<h2>Additional Note Options</h2>
</div>
<div class="sixteen wide column">
<div class="ui labeled icon fluid basic button" v-on:click="sortList">
<i class="sort amount up icon"></i>
Sort List items (Move checked to bottom)
</div>
</div>
<!-- <div class="shade" v-on:click="showAllSettings = false"></div> -->
<div class="eight wide column">
<div class="ui labeled icon fluid basic button" v-on:click="deleteCompletedListItems">
<i class="trash icon"></i>
Delete Checked Items
</div>
</div>
<div class="eight wide column">
<div class="ui labeled icon fluid basic button" v-on:click="uncheckAllListItems">
<i class="list ul icon"></i>
Uncheck all Checked items
</div>
</div>
<div class="sixteen wide column">
<div class="ui labeled icon fluid basic button" v-on:click="undoCustom">
<i class="undo icon"></i>
Undo last change
</div>
</div>
<div class="sixteen wide column" v-if="rawTextId > 0">
<share-note-component
:note-id="noteid"
:raw-text-id="rawTextId"
:share-username="shareUsername"
/>
</div>
</div>
</div>
</side-slide-menu>
</div>
</template>
@ -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,7 +703,7 @@
// console.log(patch_text)
this.$io.emit('note_diff', {
id:this.currentNoteId,
id: this.rawTextId,
diff: patch_text
})
@ -575,9 +805,6 @@
}, 20)
// }
},
xpath(el) {
//Skip things we can't use
@ -593,6 +820,7 @@
return this.xpath(el.parentNode) + '/' + el.tagName.toLowerCase() + (sames.length > 1 ? '['+([].indexOf.call(sames, el)+1)+']' : '')
},
getElementByXPath(xpath){
return new XPathEvaluator()
.createExpression(xpath)
.evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE) .singleNodeValue
@ -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 @@
<style type="text/css" scoped>
/* squire note menu button */
.note-menu {
width: 100%;
display: block;
background: #221f2b;
color: white;
overflow: hidden;
border: 1px solid #534c68;
}
.note-menu > .nm-button {
padding: 10px 10px;
cursor: pointer;
text-align: center;
box-sizing: border-box;
font-size: 1.1em;
vertical-align: middle;
/*height: 40px;*/
display: table-cell;
}
.nm-button > i {
font-size: 1.1em;
margin: 0;
}
.nm-button:hover {
background-color: #534c68;
color: white;
}
.nm-button + .nm-button {
border-left: 1px solid #534c68;
}
/* squire styles */
/*Settings manager styles */

View File

@ -0,0 +1,44 @@
<template>
<div class="nm-button" :class="moreClass" :data-tooltip="tip" data-inverted>
<!-- Display Icon and text -->
<i v-if="icon" :class="`${icon} icon`"></i>
<span v-if="(text && mobile) || (text && showText)">{{text}}</span>
<slot></slot>
</div>
</template>
<script>
/*
Menu button
Single Icon View
Single Icon With small text on mobile
Tooltips on desktop
Tooltip above or below
*/
export default {
name: 'NoteMenuButtonComponent',
props: [ 'icon', 'text', 'tooltip', 'moreClass', 'showText', 'tip'],
data () {
return {
files: [],
mobile: false,
showTooltip: false,
}
},
beforeMount(){
this.mobile = this.$store.getters.getIsUserOnMobile
},
mounted(){
// console.log('Im a button')
},
methods: {
onFileClick(file){
},
}
}
</script>

View File

@ -1,40 +1,35 @@
<template>
<div v-on:click="fullTagEdit = true">
<!-- simple string view -->
<!-- class="ui basic segment" -->
<div v-if="!fullTagEdit">
<div class="simple-tag-display">
<!-- Show Loading -->
<span v-if="!loaded">Loading Tags...</span>
<!-- Default count -->
<span v-if="loaded">
<i class="tags icon"></i> <b>{{tags.length}} Tag<span v-if="tags.length > 1">s</span></b>
</span>
<!-- No tags default text -->
<span v-if="tags.length == 0 && loaded" class="ui small compact basic green button">
<i class="plus icon"></i> Add a tag
</span>
<!-- display tags in comma delimited list -->
<span v-if="tags.length > 0">
<span v-for="(tag, i) in tags"><span v-if="i > 0">, </span>{{ucWords(tag.text)}}</span>
</span>
</div>
</div>
<div>
<!-- hover over view -->
<div v-if="fullTagEdit" class="full-tag-area fade-in-fwd" v-on:mouseleave="fullTagEdit = false; clearSuggestions()">
<div v-if="fullTagEdit" class="full-tag-area fade-in-fwd">
<!-- existing tags -->
<div class="delete-tag-display" v-if="tags.length > 0">
<div class="ui icon large label" v-for="tag in tags" :class="{ 'green':(newTagInput == tag.text) }">
{{ucWords(tag.text)}} <i class="delete icon" v-on:click="removeTag(tag.id)"></i>
<div class="ui grid">
<div class="sixteen wide column">
<h2><i class="green tags icon"></i>Edit Tags</h2>
</div>
<div class="sixteen wide column">
<h3>All Tags</h3>
<div v-if="allTags.length > 0">
<div class="ui icon large label" v-for="tag in allTags" :class="{ 'green':isTagOnNote(tag.id) }">
{{ ucWords(tag.text) }}
</div>
</div>
</div>
<div class="sixteen wide column">
<h3>Tags on Note</h3>
<div v-if="allTags.length > 0 && noteTagIds.length > 0">
<div class="ui icon large label" v-for="tag in noteTagIds">
{{ getTagTextById(tag['tagId']) }} <i class="delete icon" v-on:click="removeTag(tag['entryId'])"></i>
</div>
</div>
</div>
</div>
<!-- tag input and suggestion popup -->
<div class="ui form">
@ -70,9 +65,11 @@
newTagInput: '',
typeDebounce: null,
allTags: [],
noteTagIds: [],
suggestions: [],
selection: 0,
fullTagEdit: false,
fullTagEdit: true,
loaded: false,
}
},
@ -85,14 +82,37 @@
methods: {
getTags(){
//Get Note Tags -> /api/tags/get
let vm = this
axios.post('/api/tag/get', {'noteId': this.noteId})
.then(response => {
vm.loaded = true
//Set up local data
vm.tags = response.data
.then( ({data}) => {
this.loaded = true
this.allTags = data.allTags
this.noteTagIds = data.noteTagIds
})
},
isTagOnNote(id){
for (let i = 0; i < this.noteTagIds.length; i++) {
const current = this.noteTagIds[i]
if(current && current['tagId'] == id){
return true
}
}
},
getTagTextById(id){
let tag = this.getTagById(id)
if(tag && tag.text){
return this.ucWords( tag.text )
}
},
getTagById(id){
for (let i = 0; i < this.allTags.length; i++) {
const current = this.allTags[i]
if(current && current['id'] == id){
return current
}
}
},
tagInput(event){
let vm = this
@ -177,6 +197,7 @@
})
},
onFocus(){
return
//Show suggested tags
let vm = this
let postData = {
@ -211,10 +232,10 @@
'tagId':tagId,
'noteId':this.noteId
}
let vm = this
axios.post('/api/tag/removefromnote', postData)
.then(response => {
vm.getTags()
this.getTags()
})
},
clearSuggestions(){

View File

@ -40,37 +40,40 @@
</div>
<!-- Toolbar on the bottom -->
<div class="bottom aligned row icon-bar" @click.self="onClick(note.id)">
<div class="six wide column clickable" @click="onClick(note.id)">
{{$helpers.timeAgo(note.updated)}}
</div>
<div class="ten wide right aligned column">
<!-- ALways show delete button on mobile -->
<delete-button :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }" :note-id="note.id" />
<div class="bottom aligned row" @click.self="onClick(note.id)">
<div class="sixteen wide column">
<div class="ui grid reduced-padding">
<div class="thirteen wide column clickable icon-bar" @click="onClick(note.id)">
<!-- {{$helpers.timeAgo(note.updated)}} -->
<span v-if="note.tags">
<span v-for="tag in (note.tags.split(','))" class="little-tag">{{ tag }}</span>
</span>
<span v-if="note.pinned == 1" data-position="top right" data-tooltip="Pinned" data-inverted="">
<i class="green pin icon"></i>
</span>
<span v-if="note.archived == 1" data-position="top right" data-tooltip="Archived" data-inverted="">
<i class="green archive icon"></i>
</span>
<span v-if="note.attachment_count > 0" v-on:click.stop="openEditAttachment" class="clickable">
<i class="linkify icon"></i> {{note.attachment_count}}
</span>
<span v-if="note.tag_count == 1" data-position="top right" data-tooltip="Note has 1 tag" data-inverted="">
<i class="tags icon"></i> {{note.tag_count}}
</span>
<span v-if="note.tag_count > 1" :data-tooltip="`Note has ${note.tag_count} tags`" data-position="top right" data-inverted="">
<i class="tags icon"></i> {{note.tag_count}}
</span>
</div>
<div class="three wide right aligned column">
<delete-button :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }" :note-id="note.id" />
</div>
<div class="row" v-if="note.thumbs">
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
<img v-for="thumb in note.thumbs.split(',').reverse()" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
@ -145,13 +148,16 @@
/*Strict font sizes for card display*/
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
font-size: 1.0em !important;
/*font-size: 1.0em !important;*/
font-size: 15px !important;
}
.small-text > p, , .small-text > h1, .small-text > h2 {
margin-bottom: 0.5em;
}
.big-text, .big-text > p, .big-text > h1, .big-text > h2 {
font-size: 1.3em !important;
/*font-size: 1.3em !important;*/
font-size: 16px !important;
font-weight: bold;
}
.big-text > p, .big-text > h1, .big-text > h2 {
margin-bottom: 0.3em;
@ -186,7 +192,7 @@
/*box-shadow: 0 1px 2px 0 rgba(34,36,38,.15);*/
box-shadow: 0 0px 5px 1px rgba(34,36,38,0);
margin: 5px;
padding: 1em 1.5em;
padding: 0.7em 1em;
border-radius: .28571429rem;
border: 1px solid;
border-color: var(--border_color);
@ -205,11 +211,42 @@
}
.icon-bar {
opacity: 0.8;
margin-top: -2.2rem;
/*margin-top: -2.2rem;*/
}
.hover-hide {
opacity: 0.0;
}
.little-tag {
font-size: 0.7em;
padding: 5px 5px;
border: 1px solid var(--border_color);
margin: 5px 3px 0 0;
border-radius: 3px;
white-space: nowrap;
max-width: 100px;
overflow: hidden;
display: inline-block;
line-height: 0.8em;
text-overflow: ellipsis;
}
.tiny-thumb-box {
max-height: 70px;
overflow: hidden;
width: 100%;
display: inline-block;
background-color: rgba(200, 200, 200, 0.2);
white-space: nowrap;
overflow-x: scroll;
border: 1px solid var(--border_color);
border-left: none;
border-right: none;
text-align: center;
scrollbar-width: none;
}
.tiny-thumb {
max-height: 70px;
display: inline-block;
}
.note-title-display-card:hover .icon-bar {
opacity: 1;
@ -262,7 +299,6 @@
.note-title-display-card {
width: calc(100% + 10px);
margin: 0px -5px 10px -5px;
padding: 15px;
}
}
</style>

View File

@ -2,7 +2,7 @@
<div class="ui form">
<div class="fields">
<div class="sixteen wide field">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" />
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes and Files" />
</div>
</div>
</div>

View File

@ -0,0 +1,112 @@
<style type="text/css" scoped>
</style>
<template>
<div>
<div class="ui grid" v-if="this.shareUsername == null">
<div class="row">
<div class="eight wide column">
<div class="ui form">
<div class="field">
<input type="text" placeholder="Share with someone" v-model="shareUserInput" v-on:keyup="onKeyup">
</div>
</div>
</div>
<div class="eight wide column">
<div class="ui disabled button" v-if="shareUserInput.length == 0">
Share
</div>
<div class="ui green button" v-if="shareUserInput.length > 0" v-on:click="onSubmitClick">
Share
</div>
</div>
</div>
<div class="sixteen wide column" v-if="sharedWithUsers.length > 0">
<h3>Users who can edit this note</h3>
</div>
<div class="row" v-for="item in sharedWithUsers">
<div class="eight wide middle aligned column">
<h3><i class="green user circle icon"></i>{{item.username}}</h3>
</div>
<div class="eight wide column">
<div class="ui basic compact button" v-on:click="onRevokeAccess(item.noteId)">Remove Access</div>
</div>
</div>
</div>
<div class="ui grid" v-if="this.shareUsername != null">
<div class="sixteen wide column">
Shared with you by <h3><i class="green user circle icon"></i>{{shareUsername}}</h3>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'ShareNoteComponent',
props: [ 'noteId', 'rawTextId', 'shareUsername' ],
data () {
return {
sharedWithUsers: [],
shareUserInput: '',
debounce: null,
enableSubmitShare: false,
}
},
beforeMount(){
},
mounted(){
if(this.shareUsername == null){
this.loadShareList()
}
},
methods: {
loadShareList(){
axios.post('/api/note/getshareusers', {'rawTextId':this.rawTextId })
.then( ({data}) => {
this.sharedWithUsers = data
})
},
onRevokeAccess(noteId){
axios.post('/api/note/shareremoveuser', {'noteId':noteId})
.then( ({data}) => {
console.log(data)
if(data == true){
this.loadShareList()
}
})
},
onKeyup(event){
if(event.keyCode == 13){
this.onSubmitClick()
return
}
},
onSubmitClick(){
axios.post('/api/note/shareadduser', {'noteId':this.noteId, 'rawTextId':this.rawTextId, 'username':this.shareUserInput })
.then( ({data}) => {
if(data == true){
this.shareUserInput = ''
this.loadShareList()
} else {
this.$bus.$emit('notification', 'User not found')
}
})
},
}
}
</script>

View File

@ -0,0 +1,133 @@
<style type="text/css" scoped>
.slide-container {
position: fixed;
top: 0;
left: 0;
right: 55%;
bottom: 0;
z-index: 400;
overflow: hidden;
height: 100%;
}
.slide-content {
box-sizing: border-box;
/*padding: 1em 1.5em;*/
height: calc(100% - 43px);
border-right: 1px solid var(--menu-border);
background-color: var(--background_color);
overflow-x: scroll;
}
.slide-shadow {
position: fixed;
top: 0;
left: 0;
right: 50%;
bottom: 0;
color: red;
background-color: rgba(0,0,0,0.5);
/*background: linear-gradient(90deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 55%);*/
z-index: 399;
overflow: hidden;
cursor: pointer;
}
.note-menu {
height: 43px;
}
@media only screen and (max-width: 740px) {
.slide-shadow {
background-color: rgba(0,0,0,0.5);
}
.slide-content {
height: calc(100% - 55px);
}
.slide-container {
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.note-menu {
height: 55px;
padding: 0 30px;
}
}
.modal-fade-enter,
.modal-fade-leave-active {
opacity: 0;
}
.modal-fade-enter-active,
.modal-fade-leave-active {
transition: opacity .5s ease;
}
</style>
<template>
<transition name="modal-fade">
<div>
<div class="slide-container">
<!-- content of the editor -->
<div class="slide-content">
<slot></slot>
</div>
<!-- close menu on bottom -->
<div class="note-menu">
<nm-button more-class="right" icon="close" text="close" :show-text="true" v-on:click.native="close" />
</div>
</div>
<div class="slide-shadow" v-on:click="close"></div>
</div>
</transition>
</template>
<script>
export default {
name: 'SideSlideMenu',
props: [ 'name' ],
components: {
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
},
data () {
return {
items: []
}
},
beforeMount(){
//Other panels will tell this one to close
this.$bus.$on('destroy_all_other_side_panels', (name) => {
if(this.name != name){
this.close()
}
})
},
beforeDestroy(){
},
mounted(){
//Close all other panels that are not this one
this.$nextTick( () => {
this.$bus.$emit('destroy_all_other_side_panels', this.name)
})
},
methods: {
onClickTag(index){
console.log('yup')
},
close() {
this.$emit('close');
},
}
}
</script>

View File

@ -0,0 +1,119 @@
<style type="text/css" scoped>
.img-container {
display: flex;
flex-wrap: wrap;
}
.img-row {
height: 30vh;
flex-grow: 1;
}
.img-row:last-child {
/* There's no science in using "10" here. In all my testing, this delivered the best results. */
flex-grow: 10;
}
.img-row > img {
max-height: calc(100% - 10px);
min-width: calc(100% - 10px);
max-width: calc(100% - 10px);
object-fit: cover;
vertical-align: bottom;
/*padding: 5px;*/
box-shadow: 0px 2px 2px 1px rgba(34,36,38,0.3);
cursor: pointer;
}
</style>
<template>
<div>
<div v-if="uploadedToNote.length > 0">
<h2>Images Uploaded to Note</h2>
<div class="ui fluid green button" v-on:click="$router.push('/attachments/note/'+noteId)">
Manage Files on this Note
<i class="chevron circle right icon"></i>
</div>
<p></p>
<div class="img-container">
<div v-for="file in uploadedToNote" class="img-row" v-on:click="onFileClick(file)">
<img :src="`/api/static/thumb_${file.file_location}`">
</div>
<!-- extra row helps it display properly -->
<div class="img-row"></div>
</div>
</div>
<!-- Add images to note -->
<div v-if="files.length > 0">
<h2>All other Images</h2>
<div class="ui fluid green button" v-on:click="$router.push('/attachments')">
Manage All Files
<i class="chevron circle right icon"></i>
</div>
<p></p>
<div class="img-container">
<div v-for="file in files" class="img-row" v-on:click="onFileClick(file)">
<img :src="`/api/static/thumb_${file.file_location}`">
</div>
<!-- extra row helps it display properly -->
<div class="img-row"></div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'SimpleAttachmentNoteComponent',
props: [ 'noteId', 'squireEditor' ],
data () {
return {
files: [],
uploadedToNote: [],
}
},
beforeMount(){
},
mounted(){
axios.post('/api/attachment/search', {'attachmentType':2})
.then( ({data}) => {
//Sort files into two categories
data.forEach(file => {
if(file['note_id'] == this.noteId){
this.uploadedToNote.push(file)
} else {
this.files.push(file)
}
})
})
},
methods: {
onFileClick(file){
const imageCode = `<img alt="image" src="/api/static/thumb_${file.file_location}">`
this.$bus.$emit('new_file_upload', {noteId: this.noteId, imageCode})
if(this.$store.getters.getIsUserOnMobile){
this.close()
}
},
close() {
this.$emit('close');
},
}
}
</script>

View File

@ -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')
})
}
}

View File

@ -11,13 +11,12 @@
<div class="fields">
<div class="ten wide field">
<search-input></search-input>
<!-- <input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" /> -->
</div>
<div class="six wide field">
<span class="ui fluid green button"
v-if="showClear"
@click="reset">
<i class="undo icon"></i>Reset Filters
<i class="undo icon"></i>Back to All Notes
</span>
<!-- <fast-filters /> -->
</div>
@ -36,6 +35,7 @@
</div>
<div v-if="commonTags.length > 0" class="sixteen wide column">
<h4><i class="green tags icon"></i>Tags</h4>
<span v-for="tag in commonTags" @click="toggleTagFilter(tag.id)">
<span class="ui clickable basic label" :class="{ 'green':(searchTags.includes(tag.id)) }">
{{ucWords(tag.text)}} <span class="detail">{{tag.usages}}</span>
@ -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,'')

View File

@ -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()

View File

@ -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,39 +105,23 @@ 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])
.query(`INSERT INTO note_raw_text (text) VALUE ('')`)
.then( (rows, fields) => {
// New notes are empty, don't add to solr index
// Note.reindex(userId, rows[0].insertId)
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])
})
.then((rows, fields) => {
// Indexing is done on save
resolve(rows[0].insertId) //Only return the new note ID when creating a new note
})
.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)
})
.catch(console.log)
})
}
Note.reindex = (userId, noteId) => {
return new Promise((resolve, reject) => {
@ -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

127
server/models/ShareNote.js Normal file
View File

@ -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)
}
})
})
}

View File

@ -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)
Tag.userTags(userId).then(userTags => {
db.promise()
.query('UPDATE note SET opened = ? WHERE id = ? AND user_id = ? LIMIT 1', [now, noteId, userId])
.then((rows, fields) => {})
db.promise()
.query(`SELECT note_tag.id, tag.text FROM note_tag
JOIN tag ON (tag.id = note_tag.tag_id)
.query(`SELECT tag_id as tagId, id as entryId
FROM note_tag
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)
})
})
}
//
// 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)
})
}

View File

@ -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) )
})

View File

@ -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'))
// })
})

4
staticFiles/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*
*/
!.gitignore
!assets