Compare commits

..

3 Commits

Author SHA1 Message Date
Max G
05152cd5a4 Added counts to each category
Counts update on certain events and show or hide various elements
Fixed various little ui element issues

fixes #6
2020-02-11 21:11:14 +00:00
Max G
cf3289aac6 Fixing quick notes
Updating all the icons
making search bar thinner
2020-02-11 06:05:28 +00:00
Max G
acf72ca67e * Tags can now be toggled by clicking
* Side slide component now respects note colors
2020-02-10 22:21:06 +00:00
18 changed files with 267 additions and 103 deletions

View File

@ -13,6 +13,7 @@
<script>
// import io from 'socket.io-client'
import axios from 'axios'
export default {
components: {
@ -50,7 +51,6 @@ export default {
},
mounted: function(){
},
computed: {
loggedIn () {

View File

@ -28,7 +28,7 @@
}
html {
scrollbar-width: none;
/*scrollbar-width: none;*/
}
div.ui.basic.segment.no-fluf-segment {

View File

@ -105,13 +105,13 @@
<!-- Buttons -->
<div class="ui small compact basic button" v-on:click="openNote">
<i class="file icon"></i>
<i class="file outline icon"></i>
Open Note
</div>
<div class="ui small compact basic button" v-on:click="openEditAttachments"
:class="{ 'disabled':this.searchParams.noteId }">
<i class="folder icon"></i>
Note Attachments
<i class="folder open outline icon"></i>
Note Files
</div>
<div class="ui small compact basic icon button" v-on:click="deleteAttachment">
<i v-if="!working" class="trash alternate outline icon"></i>
@ -192,6 +192,7 @@
this.unfolded = false
setTimeout( () => {
this.visible = false
this.$store.dispatch('fetchAndUpdateUserTotals')
}, 600)
}
})

View File

@ -68,6 +68,7 @@
const imageCode = `<img alt="image" src="/api/static/thumb_${location}">`
this.$bus.$emit('new_file_upload', {noteId: this.noteId, imageCode})
this.$store.dispatch('fetchAndUpdateUserTotals')
}
})
.catch(results => {

View File

@ -153,7 +153,8 @@
<div class="menu-section" v-if="loggedIn">
<router-link exact-active-class="active" class="menu-item menu-button" to="/notes" v-on:click.native="emitReloadEvent()">
<i class="file icon"></i>Notes
<i class="file outline icon"></i>Notes
<!-- <span v-if="$store.getters.totals">{{ $store.getters.totals['totalNotes'] }}</span> -->
</router-link>
<div>
<!-- <div class="menu-item sub">Show Only <i class="caret down icon"></i></div> -->
@ -163,27 +164,16 @@
</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
</div>
</div>
<div class="menu-section" v-if="loggedIn">
<div class="menu-section" v-if="loggedIn && $store.getters.totals && $store.getters.totals['totalFiles']">
<router-link class="menu-item menu-button" exact-active-class="active" to="/attachments">
<i class="folder icon"></i>Files
<i class="folder open outline icon"></i>Files
<!-- <span>{{ $store.getters.totals['totalFiles'] }}</span> -->
</router-link>
</div>
<div class="menu-section" v-if="loggedIn">
<router-link v-if="loggedIn" exact-active-class="active" class="menu-item menu-button" to="/quick">
<i class="coffee icon"></i>Quick
<i class="paper plane outline icon"></i>Quick
</router-link>
</div>
@ -208,7 +198,7 @@
<div class="menu-section" v-if="loggedIn">
<div v-if="loggedIn" v-on:click="destroyLoginToken" class="menu-item menu-button">
<i class="user icon"></i>{{ucWords($store.getters.getUsername)}}
<i class="user outline icon"></i>{{ucWords($store.getters.getUsername)}}
</div>
</div>
@ -242,6 +232,8 @@
this.mobile = this.$store.getters.getIsUserOnMobile
this.collapsed = this.$store.getters.getIsUserOnMobile
// {{ totals['totalNotes'] }}
if(this.mobile){
this.menuOpen = false
}

View File

@ -30,6 +30,7 @@
axios.post('/api/note/delete', {'noteId':this.noteId}).then(response => {
if(response.data == true){
this.$bus.$emit('note_deleted', this.noteId)
this.$store.dispatch('fetchAndUpdateUserTotals')
}
})
},

View File

@ -118,13 +118,13 @@
/>
</side-slide-menu>
<side-slide-menu v-if="showTagSlideMenu" v-on:close="showTagSlideMenu = false" name="tags">
<side-slide-menu v-if="showTagSlideMenu" v-on:close="showTagSlideMenu = false" name="tags" :style-object="styleObject">
<div class="ui basic segment">
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/>
</div>
</side-slide-menu>
<side-slide-menu v-if="showFilesSideMenu" v-on:close="showFilesSideMenu = false" name="images">
<side-slide-menu v-if="showFilesSideMenu" v-on:close="showFilesSideMenu = false" name="images" :style-object="styleObject">
<div class="ui basic segment">
<simple-attachment-note
v-on:close="showFilesSideMenu = false"
@ -134,7 +134,7 @@
</div>
</side-slide-menu>
<side-slide-menu v-if="showNoteOptions" v-on:close="showNoteOptions = false" name="note-options">
<side-slide-menu v-if="showNoteOptions" v-on:close="showNoteOptions = false" name="note-options" :style-object="styleObject">
<div class="ui basic padded segment">
<div class="ui grid">
<div class="sixteen wide column">
@ -202,6 +202,7 @@
loading: true,
loadingMessage: 'Loading Note',
currentNoteId: 0,
modified: false,
noteText: '',
rawTextId: 0,
shareUsername: null,
@ -873,21 +874,16 @@
'archived':this.archived,
}
let vm = this
//Debounce save to prevent spamming
// clearTimeout(this.saveDebounce)
// this.saveDebounce = setTimeout(() => {
//Only notify user if saving - may help with debugging in the future
vm.statusText = 'Saving'
axios.post('/api/note/update', postData).then( response => {
vm.statusText = 'Saved'
vm.updated = Math.round((+new Date)/1000)
this.statusText = 'Saving'
axios.post('/api/note/update', postData).then( response => {
this.statusText = 'Saved'
this.updated = Math.round((+new Date)/1000)
this.modified = true
//Update last saved note hash
vm.lastNoteHash = vm.hashString( currentNoteText )
return resolve(true)
})
// }, 300)
//Update last saved note hash
this.lastNoteHash = this.hashString( currentNoteText )
return resolve(true)
})
})
},
checkForUpdatedNote(){
@ -949,7 +945,9 @@
this.sizeDown = true
//This timeout allows animation to play before closing
setTimeout(() => {
this.$bus.$emit('close_active_note', {position: this.position, noteId: this.noteid})
this.$bus.$emit('close_active_note', {
position: this.position, noteId: this.noteid, modified: this.modified
})
return
}, 300)
})

View File

@ -12,8 +12,9 @@
<div class="sixteen wide column">
<h3>All Tags</h3>
<h4 v-if="allTags.length == 0">No tags yet, add a tag.</h4>
<div v-if="allTags.length > 0">
<div class="ui icon large label" v-for="tag in allTags" :class="{ 'green':isTagOnNote(tag.id) }">
<div class="ui icon large label clickable" v-for="tag in allTags" :class="{ 'green':isTagOnNote(tag.id) }" v-on:click="toggleTag(tag.text, tag.id, tag.entryId)">
{{ ucWords(tag.text) }}
</div>
</div>
@ -42,7 +43,7 @@
v-on:focus="onFocus"
/>
<div class="suggestion-box" v-if="suggestions.length > 0">
<div class="suggestion-item" v-for="(item, index) in suggestions" :class="{ 'active':(index == selection) }" v-on:click="onClickTag(index)">
<div class="suggestion-item" v-for="(item, index) in suggestions" :class="{ 'active':(index == selection) }" v-on:click="onSuggestionClick(index)">
{{ucWords(item.text)}} <span class="suggestion-tip" v-if="index == selection">Press Enter to add</span>
</div>
</div>
@ -98,6 +99,7 @@
return true
}
}
return false
},
getTagTextById(id){
let tag = this.getTagById(id)
@ -171,9 +173,38 @@
}
}, 300)
},
onClickTag(index){
onSuggestionClick(index){
this.newTagInput = this.suggestions[index].text
this.addTag()
},
toggleTag(tagText, id){
//remove tag
if(this.isTagOnNote(id)){
//Find database ID for tag
let entryId = null
this.noteTagIds.forEach(tag => {
if(tag.tagId == id){
entryId = tag.entryId
return
}
})
//Submit database entry to be removed
if(entryId){
this.removeTag(entryId)
}
return
}
//Add Tag
this.newTagInput = tagText
this.addTag()
return
},
addTag(){
@ -228,6 +259,8 @@
},
removeTag(tagId){
console.log(tagId)
let postData = {
'tagId':tagId,
'noteId':this.noteId
@ -255,9 +288,6 @@
/* note tag edit area */
.full-tag-area {
color: var(--text_color);
background-color: var(--background_color);
/*padding: 15px;*/
/*border: 1px solid;*/
border-color: var(--border_color);

View File

@ -1,9 +1,8 @@
<template>
<div class="ui form">
<div class="fields">
<div class="sixteen wide field">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes and Files" />
</div>
<div class="ui left icon fluid input">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes and Files" />
<i class="search icon"></i>
</div>
</div>
</template>

View File

@ -8,13 +8,16 @@
z-index: 400;
overflow: hidden;
height: 100%;
color: var(--text_color);
background-color: var(--background_color);
}
.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);
/*background-color: var(--background_color);*/
overflow-x: scroll;
}
.slide-shadow {
@ -71,7 +74,7 @@
<transition name="modal-fade">
<div>
<div class="slide-container">
<div class="slide-container" :style="{ 'background-color':bgColor, 'color':textColor}">
<!-- content of the editor -->
<div class="slide-content">
@ -94,13 +97,15 @@
<script>
export default {
name: 'SideSlideMenu',
props: [ 'name' ],
props: [ 'name', 'styleObject' ],
components: {
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
},
data () {
return {
items: []
items: [],
bgColor: null,
textColor: null,
}
},
beforeMount(){
@ -115,6 +120,16 @@
beforeDestroy(){
},
mounted(){
//If note style object is set, use that on the slide menu
if(this.styleObject && this.styleObject.noteText){
this.textColor = this.styleObject.noteText
}
if(this.styleObject && this.styleObject.noteBackground){
this.bgColor = this.styleObject.noteBackground
}
//Close all other panels that are not this one
this.$nextTick( () => {

View File

@ -4,7 +4,7 @@
<div class="ui sixteen wide column">
<h2 class="ui header">
<i class="folder icon"></i>
<i class="folder open outline icon"></i>
<div class="content">
Files
<div class="sub header">Uploaded Files and Websites from notes.</div>
@ -18,7 +18,7 @@
Show All Attachments
</div>
<div class="ui green button" v-on:click="openNote">
<i class="file icon"></i>
<i class="file outline icon"></i>
Open Note
</div>
</div>

View File

@ -3,33 +3,48 @@
<div class="ui grid" :class="{ 'mush-it-up':showOneColumn() }" ref="content">
<!-- Note filter options -->
<div class="row" v-if="!$store.getters.getIsUserOnMobile">
<div v-if="!$store.getters.getIsUserOnMobile" class="sixteen wide column">
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
<div :class="{ 'sixteen wide column':showOneColumn(), 'eight wide column':!showOneColumn() }">
<div class="ui form">
<div class="fields">
<div class="ten wide field">
<search-input></search-input>
</div>
<div class="six wide field">
<span class="ui fluid green button"
v-if="showClear"
@click="reset">
<i class="undo icon"></i>Back to All Notes
</span>
<!-- <fast-filters /> -->
</div>
<div class="ui grid">
<div class="eight wide column">
<search-input></search-input>
</div>
<div class="eight wide column">
<div class="ui basic button" v-on:click="updateFastFilters(3)" v-if="$store.getters.totals && $store.getters.totals['sharedToNotes'] > 0" style="position: relative;">
<i class="green mail icon"></i>Inbox
<span class="floating ui green label" v-if="$store.getters.totals['unreadNotes'] > 0">
{{ $store.getters.totals['unreadNotes'] }}
</span>
</div>
<div class="ui basic button" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
<i class="green archive icon"></i>Archived
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
</div>
</div>
<div class="eight wide column" v-if="showClear">
<!-- <fast-filters /> -->
<span class="ui fluid green button"
@click="reset">
<i class="arrow circle left icon"></i>Back to All Notes
</span>
</div>
</div>
</div>
<div v-if="$store.getters.getIsUserOnMobile && showClear" class="row">
<div class="sixteen wide column">
<span class="ui fluid green button"
@click="reset">
<i class="undo icon"></i>Reset Filters
<span class="ui fluid green button" @click="reset">
<i class="arrow circle left icon"></i>Back to All Notes
</span>
</div>
</div>
@ -67,7 +82,7 @@
<!-- pinned notes -->
<div v-if="containsPinnednotes > 0" class="note-card-section">
<!-- ({{containsPinnednotes}}) -->
<h4><i class="green pin icon"></i>Pinned <span v-if="!working"></span></h4>
<h4><i class="green pin icon"></i>Pinned</h4>
<div class="note-card-display-area">
<note-title-display-card
v-for="note in notes"
@ -83,7 +98,7 @@
<!-- normal notes -->
<div v-if="containsNormalNotes > 0" class="note-card-section">
<!-- ({{containsNormalNotes}}) -->
<h4><i class="green file icon"></i>Notes <span v-if="!working"></span></h4>
<h4><i class="green file icon"></i>Notes</h4>
<div class="note-card-display-area">
<note-title-display-card
v-for="note in notes"
@ -118,7 +133,7 @@
<!-- found attachments -->
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
<h4><i class="green folder icon"></i> Found in Files ({{ foundAttachments.length }})</h4>
<h4><i class="folder open outline icon"></i> Found in Files ({{ foundAttachments.length }})</h4>
<attachment-display
v-for="item in foundAttachments"
:item="item"
@ -197,9 +212,16 @@
this.$parent.loginGateway()
this.$bus.$on('close_active_note', ({position, noteId}) => {
//Update totals for app
this.$store.dispatch('fetchAndUpdateUserTotals')
this.$bus.$on('close_active_note', ({position, noteId, modified}) => {
this.closeNote(position)
this.updateSingleNote(noteId)
this.$store.dispatch('fetchAndUpdateUserTotals')
if(modified){
this.updateSingleNote(noteId)
}
})
this.$bus.$on('note_deleted', (noteId) => {
//Remove deleted note from set, its deleted
@ -268,6 +290,7 @@
// this.$bus.$off()
},
mounted() {
//Loads initial batch and tags
this.reset()
},
@ -637,6 +660,31 @@
resolve(data)
})
})
},
updateFastFilters(index){
//clear out tags
this.searchTags = []
//A little hacky, brings user to notes page then filters on click
if(this.$route.name != 'NotesPage'){
this.$router.push('/notes')
setTimeout( () => {
this.updateFastFilters(index)
}, 500 )
}
const options = [
'withLinks', // 'Only Show Notes with Links'
'withTags', // 'Only Show Notes with Tags'
'onlyArchived', //'Only Show Archived Notes'
'onlyShowSharedNotes', //Only show shared notes
]
let filter = {}
filter[options[index]] = 1
this.$bus.$emit('update_fast_filters', filter)
}
}
}

View File

@ -4,7 +4,7 @@
<div class="ui sixteen wide column">
<h2 class="ui header">
<i class="coffee icon"></i>
<i class="paper plane outline icon"></i>
<div class="content">
Quick
<div class="sub header">Add new information with great speed</div>
@ -26,7 +26,14 @@
</div>
<div class="field">
<div v-on:click="appendQuickNote" class="ui green button">Save (CRTL + Enter)</div>
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button">Edit</div>
<div v-if="quickNoteId" class="ui right floated basic button" v-on:click="$router.push('/attachments/note/'+quickNoteId)">
<i class="folder open outline icon"></i>
Quick Files + Links
</div>
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button">
<i class="file outline icon"></i>
Edit
</div>
</div>
</div>
</div>
@ -98,7 +105,7 @@
//Don't submit empty note
if(this.newText.trim() == ''){ return }
axios.post('/api/quick-note/update', { 'pushText':this.newText } )
axios.post('/api/quick-note/update', { 'pushText':this.newText.trim() } )
.then( results => {
this.newText = '' //Clear text area

View File

@ -12,6 +12,7 @@ export default new Vuex.Store({
isUserOnMobile: false,
isNoteSettingsOpen: false, //Little note settings pane
socket: null,
userTotals: null,
},
mutations: {
setLoginToken(state, userData){
@ -88,6 +89,14 @@ export default new Vuex.Store({
//Put socket id in axios headers
axios.defaults.headers.common['socketId'] = socket
state.socket = socket
},
setUserTotals(state, totalsObject){
//Save all the totals for the user
state.userTotals = totalsObject
// Object.keys(totalsObject).forEach( key => {
// console.log(key + ' -- ' + totalsObject[key])
// })
}
},
@ -114,5 +123,16 @@ export default new Vuex.Store({
getSocket: state => {
return state.socket
},
totals: state => {
return state.userTotals
},
},
actions: {
fetchAndUpdateUserTotals ({ commit }) {
axios.post('/api/user/totals')
.then( ({data}) => {
commit('setUserTotals', data)
})
}
}
})

View File

@ -105,7 +105,7 @@ Note.create = (userId, noteText, quickNote = 0) => {
const created = Math.round((+new Date)/1000)
db.promise()
.query(`INSERT INTO note_raw_text (text) VALUE ('')`)
.query(`INSERT INTO note_raw_text (text) VALUE (?)`, [noteText])
.then( (rows, fields) => {
const rawTextId = rows[0].insertId
@ -334,6 +334,10 @@ Note.get = (userId, noteId) => {
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId,noteId])
.then((rows, fields) => {
const created = Math.round((+new Date)/1000)
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [created, noteId])
//Return note data
resolve(rows[0][0])

View File

@ -10,7 +10,9 @@ QuickNote.get = (userId) => {
db.promise()
.query(`
SELECT id, text FROM note WHERE quick_note = 1 AND user_id = ? LIMIT 1
SELECT note.id, text FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE quick_note = 1 AND user_id = ? LIMIT 1
`, [userId])
.then((rows, fields) => {
@ -53,8 +55,10 @@ QuickNote.update = (userId, pushText) => {
db.promise()
.query(`
SELECT id, text, color, pinned, archived
FROM note WHERE quick_note = 1 AND user_id = ? LIMIT 1
SELECT note.id, text, color, pinned, archived
FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE quick_note = 1 AND user_id = ? LIMIT 1
`, [userId])
.then((rows, fields) => {
@ -94,20 +98,4 @@ QuickNote.update = (userId, pushText) => {
//Note.create(userId, 'Quick Note', 1)
}
QuickNote.create = (userId, noteText) => {
return new Promise((resolve, reject) => {
if(userId == null || userId < 10){ reject('User Id required to create note') }
const created = Math.round((+new Date)/1000)
db.promise()
.query('INSERT INTO note (user_id, text, created) VALUES (?,?,?)', [userId, noteText, created])
.then((rows, fields) => {
resolve(rows[0].insertId) //Only return the new note ID when creating a new note
})
.catch(console.log)
})
}

View File

@ -111,5 +111,58 @@ User.create = (username, password) => {
.catch(console.log)
})
}
//Counts notes, pinned notes, archived notes, shared notes, unread notes, total files and types
User.getCounts = (userId) => {
return new Promise((resolve, reject) => {
let countTotals = {}
db.promise().query(
`SELECT
SUM(pinned = 1 && archived = 0 && share_user_id IS NULL) AS pinnedNotes,
SUM(pinned = 0 && archived = 1 && share_user_id IS NULL) AS archivedNotes,
SUM(share_user_id IS NULL) AS totalNotes,
SUM(share_user_id != ?) AS sharedToNotes,
SUM( (share_user_id != ? && opened IS null) || (share_user_id != ? && updated > opened) ) AS unreadNotes
FROM note
WHERE user_id = ?`, [userId, userId, userId, userId])
.then( (rows, fields) => {
Object.assign(countTotals, rows[0][0]) //combine results
return db.promise().query(
`SELECT count(id) AS sharedFromNotes
FROM note WHERE share_user_id = ?`, [userId]
)
})
.then( (rows, fields) => {
Object.assign(countTotals, rows[0][0]) //combine results
return db.promise().query(
`SELECT
SUM(attachment_type = 1) as linkFiles,
SUM(attachment_type != 1) as otherFiles,
COUNT(id) as totalFiles
FROM attachment WHERE visible = 1
AND user_id = ?
`, [userId]
)
}).then( (rows, fields) => {
Object.assign(countTotals, rows[0][0]) //combine results
//Convert everything to an int or 0
Object.keys(countTotals).forEach( key => {
const count = parseInt(countTotals[key])
countTotals[key] = count ? count : 0
})
resolve(countTotals)
})
})
}

View File

@ -46,4 +46,11 @@ router.post('/login', function (req, res) {
})
})
// fetch counts of users notes
router.post('/totals', function (req, res) {
User.getCounts(req.headers.userId)
.then( countsObject => res.send( countsObject ))
})
module.exports = router