I swear, I'm going to start doing regular commits

+ Added a ton of shit
+ About to add socket.io oh god.
This commit is contained in:
Max G 2020-01-03 01:26:55 +00:00
parent 8d07a8e11a
commit 4216c1825e
24 changed files with 3171 additions and 360 deletions

View File

@ -1,8 +1,7 @@
#!/bin/bash
BACKUPDIR="databaseBackupPi"
BACKUPDIR="/home/mab/databaseBackupPi"
cd ..
mkdir -p $BACKUPDIR
cd $BACKUPDIR
@ -11,7 +10,7 @@ ssh mab@avidhabit.com -p 13328 "mysqldump --all-databases --user root -pRootPass
cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/backup-$NOW.sql"
echo "Database Backup Complete"
echo "Database Backup Complete on $NOW"
#Restore DB
# copy file over, run restore

View File

@ -42,7 +42,7 @@ module.exports = {
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(png|jpe?g|gif)(\?.*)?$/,
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
@ -58,7 +58,7 @@ module.exports = {
}
},
{
test: /\.(eot|ttf|otf|woff|woff2|svg)(\?.*)?$/,
test: /\.(eot|ttf|otf|woff|woff2)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,

View File

@ -97,12 +97,16 @@ div.ui.basic.green.label {
/* Styles for public display pages */
.note-status-indicator {
float: right;
position: absolute;
width: 100px;
padding: 9px 0;
padding: 16px 0;
box-sizing: border-box;
text-align: center;
color: var(--text_color);
bottom: 0;
right: 0;
z-index: 100;
cursor: pointer;
}

View File

@ -1,110 +0,0 @@
<style scoped>
.shade {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0,0,0,0.7);
z-index: 1200;
cursor: pointer;
}
.editor {
position: fixed;
top: 10%;
bottom: 10%;
left: 10%;
right: 10%;
z-index: 1300;
overflow-y: scroll;
background-color: var(--background_color);
border: 3px solid;
border-color: var(--border_color);
}
.attachment-image {
width: 100%;
}
</style>
<template>
<div>
<div class="shade" v-on:click="closeAttachmentEditor"></div>
<div class="editor">
<div class="ui basic segment">
<h2>Edit Attachments</h2>
<div class="ui vertically divided grid">
<div v-for="item in attachments" class="row">
<div class="ui sixteen wide column">
<div class="ui form">
<div class="field">
<textarea v-on:blur="saveIt(item)" v-model="item.text" :key="item.id + 'text'"></textarea>
</div>
</div>
</div>
<div class="ui sixteen wide column">
<a v-if="item.url" :href="item.url" target="_blank">{{item.url}}</a>
<!-- Display image -->
<div v-if="item.file_location && item.attachment_type == 2">
<img class="attachment-image" :src="`/api/static/${item.file_location}`">
</div>
<!-- provide link -->
<a v-if="item.file_location" :href="`/api/static/${item.file_location}`" target="_blank">Download</a>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'AttachmentEditor',
props: [ 'noteId' ],
data: function(){
return {
attachments: []
}
},
beforeCreate: function(){
},
mounted: function(){
this.fetchAttachments()
},
methods: {
fetchAttachments(){
axios.post('/api/attachment/get', {'noteId':this.noteId})
.then(results => {
this.attachments = results.data
})
},
closeAttachmentEditor(){
this.$bus.$emit('close_edit_attachment')
},
saveIt(attachment){
const data = {
'attachmentId': attachment.id,
'updatedText': attachment.text,
'noteId': this.noteId
}
axios.post('/api/attachment/update', data)
.then(results => {})
},
}
}
</script>

View File

@ -7,9 +7,12 @@
</style>
<template>
<div class="ui clickable basic button">
<div class="ui small compact basic icon button clickable">
<form>
<label :for="`upfile-${noteId}`" class="clickable">File Yeet {{uploadPercentage}}</label>
<label :for="`upfile-${noteId}`" class="clickable">
<i class="upload icon"></i>
Upload File <span v-if="uploadPercentage != 0">{{uploadPercentage}}%</span>
</label>
<input class="hidden-up" type="file" :id="`upfile-${noteId}`" ref="file" v-on:change="handleFileUpload()" />
</form>
<!-- <button v-if="file" v-on:click="uploadFileToServer()">Submit</button> -->
@ -47,7 +50,7 @@
}
}
).then(results => {
this.uploadPercentage = 'DONE'
this.uploadPercentage = 0
this.file = null
console.log('SUCCESS!!');
@ -76,7 +79,7 @@
})
.catch(results => {
this.uploadPercentage = 'FAIL'
this.uploadPercentage = 0
console.log('FAILURE!!');
})
},

View File

@ -21,10 +21,10 @@
.menu-item {
color: #fff;
padding: 10px 0px 10px 10px;
padding: 0.8em 0px 0.8em 0.8em;
display: inline-block;
width: 100%;
font-size: 1.1rem;
font-size: 1.15em;
box-sizing: border-box;
}
.sub {
@ -60,28 +60,60 @@
cursor: pointer;
}
.top-menu-bar {
color: var(--text_color);
width: calc(100% - 20px);
/*color: var(--text_color);*/
/*width: 100%;*/
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background-color: var(--background_color);
border-bottom: 1px solid;
border-color: var(--border_color);
padding: 5px 1rem 5px;
}
.place-holder {
width: 100%;
height: 50px;
}
.top-menu-bar img {
width: 20px;
height: 20px;
width: 30px;
height: 30px;
}
</style>
<template>
<div>
<div class="place-holder" v-if="collapsed && !menuOpen"></div>
<!-- collapsed menu, appears as a bar -->
<div class="top-menu-bar menu-item" v-if="collapsed || mobile" v-on:click="collapseMenu">
<div class="top-menu-bar" v-if="(collapsed || mobile) && !menuOpen">
<div class="ui grid">
<div class="five wide column">
<i class="bars icon"></i> Menu
<div class="three wide column">
<div class="ui large basic compact icon button" v-on:click="collapseMenu">
<i class="green bars icon"></i>
</div>
<div class="six wide center aligned column">
<img src="/api/static/assets/favicon.ico" alt="logo" />
</div>
<div class="five wide right aligned column"></div>
<div class="ten wide center aligned column">
<img v-if="!loggedIn" src="/api/static/assets/favicon.ico" alt="logo" />
<search-input v-if="loggedIn"></search-input>
</div>
<div class="three wide right aligned column">
<!-- mobile create note button -->
<div v-if="loggedIn">
<div v-if="!disableNewNote" @click="createNote" class="ui large basic compact icon button">
<i class="green plus icon"></i>
</div>
<div v-if="disableNewNote" class="ui large basic compact icon button">
<i class="grey plus icon"></i>
</div>
</div>
</div>
</div>
</div>
@ -94,7 +126,7 @@
<div class="menu-section">
<div class="menu-item menu-button" v-on:click="collapseMenu">
<i class="caret square left icon"></i>
<i class="caret square left outline icon"></i>
</div>
</div>
@ -108,7 +140,7 @@
</div>
<div class="menu-section" v-if="loggedIn">
<router-link exact-active-class="active" class="menu-item menu-button" to="/notes">
<router-link exact-active-class="active" class="menu-item menu-button" to="/notes" v-on:click.native="emitReloadEvent()">
<i class="file icon"></i>Notes
</router-link>
<div>
@ -168,13 +200,16 @@
import axios from 'axios'
export default {
components: {
'search-input': require('@/components/SearchInput.vue').default,
},
data: function(){
return {
username: '',
collapsed: false,
mobile: false,
disableNewNote: false
disableNewNote: false,
menuOpen: true,
}
},
beforeCreate: function(){
@ -182,6 +217,10 @@
mounted: function(){
this.mobile = this.$store.getters.getIsUserOnMobile
this.collapsed = this.$store.getters.getIsUserOnMobile
if(this.mobile){
this.menuOpen = false
}
},
computed: {
loggedIn () {
@ -194,10 +233,18 @@
//Collapse menu when item is clicked in mobile
if(this.mobile && !this.collapsed){
this.collapsed = true
this.menuOpen = false
}
},
collapseMenu(){
this.collapsed = !this.collapsed
if(!this.collapsed){
this.menuOpen = true
} else {
this.menuOpen = false
}
},
createNote(event){
const title = ''
@ -227,6 +274,10 @@
return $1.toUpperCase()
})
},
emitReloadEvent(){
//Reloads note page to initial state
this.$bus.$emit('note_reload')
},
updateFastFilters(index){
//A little hacky, brings user to notes page then filters on click

View File

@ -3,7 +3,7 @@
<span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete" data-inverted="" data-position="top right">
<i class="grey trash alternate icon"></i>
</span>
<span class="clickable" @click="actuallyDelete()" @mouseleave="reset" v-if="click == 1" data-tooltip="Click again to delete." data-position="left center" data-inverted="">
<span class="clickable" @click="actuallyDelete()" @mouseleave="reset" v-if="click == 1" data-tooltip="Click again to delete." data-position="top right" data-inverted="">
<i class="red trash alternate icon"></i>
</span>
</span>
@ -26,9 +26,11 @@
this.click++
},
actuallyDelete(){
axios.post('/api/note/delete', {'noteId':this.noteId}).then(response => {
if(response.data == true){
this.$bus.$emit('note_deleted')
console.log('Lets delete this note!')
this.$bus.$emit('note_deleted', this.noteId)
}
})
},

View File

@ -15,39 +15,11 @@
</div>
</div>
<!-- Menu -->
<div class="note-top-menu">
<div @click="close" class="ui basic icon button" data-tooltip="Close" data-position="right center" data-inverted="">
<i class="close icon"></i>
</div>
<div @click="onTogglePinned" class="ui basic icon button" data-tooltip="Pin to Top" data-position="right center" data-inverted="">
<i class="pin icon" :class="{green:(pinned == 1)}"></i> {{(pinned == 1)?'Pinned':''}}
</div>
<div @click="onToggleArchived" class="ui basic icon button" data-tooltip="Hide on main" data-position="right center" data-inverted="">
<i class="archive icon" :class="{green:(archived == 1)}"></i> {{(archived == 1)?'Archived':''}}
</div>
<!-- <file-upload-button :noteId="noteid" /> -->
<span class="relative" v-on:click="showColorPicker">
<span class="ui basic icon button">
<i class="paint brush icon"></i>
</span>
</span>
<div v-if="attachmentCount > 0" class="ui basic icon button" v-on:click="openEditAttachment" data-tooltip="View Links/Images" data-position="right center" data-inverted="">
<i class="folder icon"></i>
</div>
<span class="note-status-indicator">{{statusText}}</span>
</div>
<span class="note-status-indicator" v-on:click="save()">{{statusText}}</span>
<div class="tinymce-container">
<textarea :id="noteid+'-tinymce-editor'">{{noteText}}</textarea>
<note-tag-edit v-if="!$store.getters.getIsUserOnMobile" :noteId="noteid" :key="'tags-for-note-'+noteid"/>
</div>
<color-picker
v-if="colorPickerVisible"
@ -56,7 +28,51 @@
:style-object="styleObject"
/>
<div v-if="$store.getters.getIsNoteSettingsOpen">
<div class="all-settings">
<div class="ui grid">
<div class="row">
<div class="sixteen wide column">
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/><br>
<!-- close button -->
<div class="ui small compact basic icon button" v-on:click="$store.commit('toggleNoteSettingsPane')">
<i class="close icon"></i> Close Settings
</div>
<!-- Pin Button -->
<div @click="onToggleArchived" class="ui small compact basic icon button">
<i class="archive icon" :class="{green:(archived == 1)}"></i>
{{(archived == 1)?'Archived':'Archive'}}
</div>
<!-- archive button -->
<div @click="onTogglePinned" class="ui small compact basic icon button">
<i class="pin icon" :class="{green:(pinned == 1)}"></i>
{{(pinned == 1)?'Pinned':'Pin'}}
</div>
<!-- colors button -->
<span class="relative" v-on:click="showColorPicker">
<span class="ui small compact basic icon button">
<i class="paint brush icon"></i>
Colors
</span>
</span>
<!-- attachment button -->
<div v-if="attachmentCount > 0" class="ui small compact basic icon button" v-on:click="openEditAttachment">
<i class="folder icon"></i> Attachments
</div>
<!-- file upload button -->
<file-upload-button :noteId="noteid" />
</div>
</div>
</div>
</div>
<!-- <div class="shade" v-on:click="showAllSettings = false"></div> -->
</div>
</div>
</template>
@ -72,6 +88,7 @@
'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,
},
data(){
return {
@ -95,6 +112,10 @@
colorPickerLocation: null,
tinymce: null, //Initialized editor instance
//Settings vars
showAllSettings: true,
lastVisibilityState: null,
}
},
watch: {
@ -114,10 +135,21 @@
}
},
beforeMount(){
//Load tinymce into the page only do it once
if(document.querySelectorAll('[data-mceload]').length == 0){
let tinyMceIncluder = document.createElement('script')
tinyMceIncluder.setAttribute('src', '/api/static/assets/tinymce/tinymce.min.js')
tinyMceIncluder.setAttribute('data-mceload','loaded')
document.head.appendChild(tinyMceIncluder)
}
},
beforeDestroy(){
this.$bus.$off('toggle_night_mode', this.listener)
document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
this.$off() // Remove all event listeners
this.$bus.$off()
//Trash editor instance on close
this.tinymce.remove()
@ -127,6 +159,8 @@
//Change TinyMce styles on nightmored change
this.$bus.$on('toggle_night_mode', this.setEditorTextColor )
document.addEventListener('visibilitychange', this.checkForUpdatedNote)
this.$nextTick(() => {
this.loadNote(this.noteid)
})
@ -139,10 +173,17 @@
// {title: 'My image 2', value: 'http://www.moxiecode.com/my2.gif'}
// ]
//Default plugin options for big browsers
let toolbarOptions = 'customCloseButton | mceTogglePinned | forecolor styleselect | bold italic underline | link | bullist numlist | outdent indent table | h | image'
let pluginOptions = 'paste, link, code, lists, table, hr, image'
//Tweak doc height for mobile
let docHeight = 'calc(100vh - 90px)'
let docHeight = 'calc(100vh - 1px)'
if(this.$store.getters.getIsUserOnMobile){
docHeight = 'calc(100vh - 37px)'
docHeight = 'calc(100vh - 1px)'
toolbarOptions = 'customCloseButton | bullist numlist | mceTogglePinned'
pluginOptions = 'lists'
}
//setup skin as dark if night mode is enabled
@ -152,12 +193,13 @@
}
const editorId = '#'+this.noteid+'-tinymce-editor'
let vm = this
//Globally defined included in index HTML
tinymce.init({
selector: editorId,
toolbar: 'forecolor backcolor styleselect | bold italic underline | link image | code | undo redo | bullist numlist | outdent indent table, hr, searchreplace | removeformat',
plugins: 'paste, link, code, lists, table, hr, searchreplace, image',
toolbar: toolbarOptions,
plugins: pluginOptions,
browser_spellcheck: true,
menubar: false,
branding: false,
@ -167,6 +209,25 @@
contextmenu: false,
init_instance_callback: this.editorInitCallback,
imagetools_toolbar: "imageoptions",
setup: editor => {
//Add custom buttons to tinymce instance
editor.ui.registry.addButton('customCloseButton', {
text: 'Close',
icon: 'close',
onAction: function (_) {
vm.close()
}
})
//Add custom buttons to tinymce instance
editor.ui.registry.addButton('mceTogglePinned', {
text: 'Note Settings',
icon: 'settings',
onAction: function (_) {
vm.$store.commit('toggleNoteSettingsPane')
}
})
}
})
},
editorInitCallback(editor){
@ -197,7 +258,9 @@
}
}
},
setText(inText){
return this.tinymce.setContent(inText)
},
getText(){
//Return text from tinyMce Editor
@ -208,7 +271,6 @@
this.colorPickerLocation = {'x':event.clientX, 'y':event.clientY}
},
openEditAttachment(){
// this.$bus.$emit('open_edit_attachment', this.currentNoteId)
this.$router.push('/attachments/note/'+this.currentNoteId)
},
onTogglePinned(){
@ -300,11 +362,12 @@
save(){
return new Promise((resolve, reject) => {
//Clear other debounced events to prevent double calling of save
clearTimeout(this.editDebounce)
// clearTimeout(this.editDebounce)
//Don't save note if its hash doesn't change
const currentNoteText = this.getText()
if( this.lastNoteHash == this.hashString( currentNoteText )){
this.statusText = 'Saved'
return resolve(true)
}
@ -341,6 +404,34 @@
})
},
checkForUpdatedNote(){
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
console.log('Checking for changes is note data')
const postData = {
noteId:this.currentNoteId,
text:this.getText(),
updated: this.updated
}
axios.post('/api/note/difftext', postData)
.then( ({data}) => {
//Don't do anything if nothing has changed
if(data == ''){ return }
if(data.diffs > 0){
//Update text and last updated time for note
this.setText(data.updatedText)
this.updated = data.updated
}
})
}
//Track visibility state
this.lastVisibilityState = document.visibilityState
},
hashString(text){
var hash = 0;
@ -369,7 +460,7 @@
this.sizeDown = true
//This timeout allows animation to play before closing
setTimeout(() => {
this.$bus.$emit('close_active_note', this.position)
this.$bus.$emit('close_active_note', {position: this.position, noteId: this.noteid})
return
}, 300)
})
@ -381,6 +472,28 @@
<style type="text/css" scoped>
/*Settings manager styles */
.all-settings {
position: absolute;
bottom: -5px;
right: 10px;
left: 10px;
z-index: 99;
border: 1px solid;
background-color: var(--background_color);
border-color: var(--border_color);
box-sizing: border-box;
border-radius: 7px;
box-shadow: 0px 3px 7px 0px rgba(140,140,140,1);
padding: 1em;
}
/*End Settings manager styles */
.tinymce-container {
/* Uncomment this to see the */
/*border-bottom: 2px solid green !important;*/
}
.note-top-menu {
width: 100%;
display: inline-block;

View File

@ -1,8 +1,9 @@
<template>
<div v-on:mouseover="fullTagEdit = true">
<div v-on:click="fullTagEdit = true">
<!-- simple string view -->
<div v-if="!fullTagEdit" class="ui basic segment">
<!-- class="ui basic segment" -->
<div v-if="!fullTagEdit">
<div class="simple-tag-display">
<!-- Show Loading -->
@ -10,11 +11,11 @@
<!-- Default count -->
<span v-if="loaded">
<i class="tags icon"></i> <b>{{tags.length}} Tags</b>
<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 green button">
<span v-if="tags.length == 0 && loaded" class="ui small compact basic green button">
<i class="plus icon"></i> Add a tag
</span>
@ -28,6 +29,13 @@
<!-- hover over view -->
<div v-if="fullTagEdit" class="full-tag-area fade-in-fwd" v-on:mouseleave="fullTagEdit = false; clearSuggestions()">
<!-- 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>
</div>
<!-- tag input and suggestion popup -->
<div class="ui form">
<input
@ -45,13 +53,6 @@
</div>
</div>
<!-- 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>
</div>
</div>
</div>
</template>
@ -233,28 +234,27 @@
/* note tag edit area */
.full-tag-area {
position: absolute;
bottom: 0;
left: 0;
right: 0;
color: var(--text_color);
background-color: var(--background_color);
padding: 15px;
border: 1px solid;
/*padding: 15px;*/
/*border: 1px solid;*/
border-color: var(--border_color);
}
.full-tag-area .delete-tag-display {
margin-top: 15px;
/*margin-top: 15px;*/
}
.full-tag-area .ui.label {
margin-bottom: 5px;
}
.simple-tag-display {
width: 100%;
width: calc(100% - 0px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-height: 35px;
color: var(--text_color);
cursor: pointer;
}
/* tag suggestion box styles */
@ -269,8 +269,8 @@
height: 40px;
padding: 10px 15px;
cursor: pointer;
background: white;
color: black;
background-color: var(--background_color);
color: var(--text_color);
}
.suggestion-item.active {
background: green;

View File

@ -11,33 +11,45 @@
<div class="ui grid max-height">
<!-- Show title and snippet below it -->
<div class="top aligned row" @click.stop="onClick(note.id)">
<div class="top aligned row" @click.self="onClick(note.id)">
<div class="sixteen wide column overflow-hidden" @click="e => onClick(note.id, e)">
<!-- Title display -->
<div v-if="note.title.length > 0"
data-test-id="title"
:class="{ 'big-text':(note.titleLength <= 100), 'small-text-title':(note.titleLength >= 100) }"
v-html="note.title"></div>
<!-- Sub text display -->
<div v-if="note.subtext.length > 0 && !isShowingSearchResults()"
data-test-id="subtext"
:class="{ 'big-text':(note.subtextLength <= 100 && note.titleLength <= 100), 'small-text':(note.subtextLength >= 100) }"
v-html="note.subtext"></div>
<div class="sixteen wide column overflow-hidden">
<h3 class="clickable">{{note.title}}</h3>
<p v-if="!isShowingSearchResults()" class="clickable">{{note.subtext}}</p>
<!-- Display highlights from solr results -->
<div v-if="note.note_highlights.length > 0 && textResults" class="term-usage">
<div class="usage-row" v-for="highlight in note.note_highlights" v-html="cleanHighlight(highlight)"></div>
</div>
<div
class="usage-row"
v-for="highlight in note.note_highlights"
:class="{ 'big-text':(highlight <= 100), 'small-text-title':(highlight >= 100) }"
v-html="cleanHighlight(highlight)"></div>
</div>
<!-- <div class="sixteen wide column overflow-hidden" v-if="isShowingSearchResults()">
</div> -->
</div>
</div>
<!-- Toolbar on the bottom -->
<div class="bottom aligned row icon-bar" @click.self.stop="onClick(note.id)">
<div class="six wide column clickable" @click.stop="onClick(note.id)">
<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)}}
<!-- {{(note.chars.toLocaleString())}} -->
</div>
<div class="ten wide right aligned column split-spans">
<div class="ten wide right aligned column">
<delete-button class="hover-hide" :note-id="note.id" />
<!-- ALways show delete button on mobile -->
<delete-button :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }" :note-id="note.id" />
<span v-if="note.pinned == 1" data-position="top right" data-tooltip="Pinned" data-inverted="">
<i class="green pin icon"></i>
@ -45,7 +57,7 @@
<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">
<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="">
@ -88,7 +100,6 @@
},
openEditAttachment(){
this.$router.push('/attachments/note/'+this.note.id)
// this.$bus.$emit('open_edit_attachment', this.note.id)
},
},
data () {
@ -132,6 +143,20 @@
</script>
<style type="text/css">
/*Strict font sizes for card display*/
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
font-size: 1.0em !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;
}
.big-text > p, .big-text > h1, .big-text > h2 {
margin-bottom: 0.3em;
}
.note-title-display-card h3 {
font-size: 1rem;
font-weight: bold;
@ -166,7 +191,7 @@
border: 1px solid;
border-color: var(--border_color);
width: calc(33.333% - 10px);
transition: box-shadow 0.3s;
/*transition: box-shadow 0.3s;*/
box-sizing: border-box;
cursor: pointer;

View File

@ -0,0 +1,44 @@
<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" />
</div>
</div>
</div>
</template>
<script>
export default {
data: function(){
return {
searchTerm: '',
searchTimeout: null,
searchDebounceDuration: 300,
}
},
beforeCreate: function(){
},
mounted: function(){
//search clear
this.$bus.$on('reset_fast_filters', () => {
this.searchTerm = ''
})
},
methods: {
searchKeyUp(){
clearTimeout(this.searchTimeout)
this.searchTimeout = setTimeout(() => {
this.search()
}, this.searchDebounceDuration)
},
search(){
this.$bus.$emit('update_search_term', this.searchTerm)
},
}
}
</script>

View File

@ -32,7 +32,6 @@ require('./assets/themes/default/assets/fonts/outline-icons.woff2')
// This callback runs before every route change, including on page load.
// Sets the title of the page using vue router
router.beforeEach((to, from, next) => {

View File

@ -62,6 +62,12 @@
100%{background-position:0% 50%}
}
/*safari fix - prevents page from being below the menu */
.dont-pad-me {
margin-right: 0 !important;
margin-left: 0 !important;
}
</style>
<template>

View File

@ -4,15 +4,14 @@
<div class="ui grid" :class="{ 'mush-it-up':showOneColumn() }" ref="content">
<!-- Note filter options -->
<div class="row">
<div class="row" v-if="!$store.getters.getIsUserOnMobile">
<div
:class="{ 'sixteen wide column':showOneColumn(), 'eight 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">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" />
<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"
@ -20,16 +19,19 @@
@click="reset">
<i class="undo icon"></i>Reset Filters
</span>
<!-- <fast-filters /> -->
</div>
</div>
</div>
</div>
<div
:class="{ 'sixteen wide column':showOneColumn(), 'eight wide column':!showOneColumn() }"
>
<h2 class="ui right floated">
<fast-filters />
</h2>
</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>
</div>
</div>
@ -64,7 +66,8 @@
<!-- pinned notes -->
<div v-if="containsPinnednotes > 0" class="note-card-section">
<h4><i class="green pin icon"></i>Pinned <span v-if="!working">({{containsPinnednotes}})</span></h4>
<!-- ({{containsPinnednotes}}) -->
<h4><i class="green pin icon"></i>Pinned <span v-if="!working"></span></h4>
<div class="note-card-display-area">
<note-title-display-card
v-for="note in notes"
@ -79,7 +82,8 @@
<!-- normal notes -->
<div v-if="containsNormalNotes > 0" class="note-card-section">
<h4>Notes <span v-if="!working">({{containsNormalNotes}})</span></h4>
<!-- ({{containsNormalNotes}}) -->
<h4><i class="green file icon"></i>Notes <span v-if="!working"></span></h4>
<div class="note-card-display-area">
<note-title-display-card
v-for="note in notes"
@ -114,12 +118,8 @@
</div>
<input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" />
<input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" />
<div v-if="openAttachmentEdit">
<edit-attachment :note-id="editAttchmentId" :key="editAttchmentId" />
</div>
<input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" ref="note1" />
<input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" ref="note2" />
</div>
</template>
@ -134,7 +134,7 @@
'input-notes': require('@/components/NoteInputPanel.vue').default,
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
'fast-filters': require('@/components/FastFilters.vue').default,
'edit-attachment': require('@/components/AttachmentEditor.vue').default,
'search-input': require('@/components/SearchInput.vue').default,
},
data () {
return {
@ -153,6 +153,7 @@
batchOffset: 0, //Tracks the current batch that has been loaded
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
loadingInProgress: false,
fetchTags: false,
//Clear button is not visible
showClear: false,
@ -162,8 +163,8 @@
containsNormalNotes: 0,
containsPinnednotes: 0,
containsTextResults: 0,
containsTagResults: 0,
containsAttachmentResults: 0,
// containsTagResults: 0,
// containsAttachmentResults: 0,
//Currently open notes in app
activeNoteId1: null,
@ -173,47 +174,14 @@
activeNote1Position: 0,
activeNote2Position: 0,
//Attchment to edit. Only 1 for now
openAttachmentEdit: false,
editAttchmentId: null,
lastVisibilityState: null,
}
},
beforeMount(){
this.$parent.loginGateway()
this.$bus.$on('open_edit_attachment', noteId => {
this.openAttachmentEdit = true
this.editAttchmentId = noteId
})
this.$bus.$on('close_edit_attachment', noteId => {
this.openAttachmentEdit = false
this.editAttchmentId = null
})
this.$bus.$on('close_active_note', position => {
this.closeNote(position)
})
this.$bus.$on('note_deleted', () => {
this.search()
})
this.$bus.$on('update_fast_filters', newFilter => {
this.fastFilters = newFilter
this.search(true, this.batchSize, false)
})
//New note button pushes open note event
this.$bus.$on('open_note', noteId => {
this.openNote(noteId)
})
//Mount notes on load if note ID is set
if(this.$route.params && this.$route.params.id){
const id = this.$route.params.id
this.openNote(id)
}
window.addEventListener('scroll', this.onScroll)
//Load tinymce into the page only do it once
if(document.querySelectorAll('[data-mceload]').length == 0){
let tinyMceIncluder = document.createElement('script')
@ -222,21 +190,75 @@
document.head.appendChild(tinyMceIncluder)
}
this.$bus.$on('close_active_note', ({position, noteId}) => {
this.closeNote(position)
this.updateSingleNote(noteId)
})
this.$bus.$on('note_deleted', (noteId) => {
//Remove deleted note from set, its deleted
this.notes.forEach( (note, index) => {
if(note.id == noteId){
if(note.pinned == 1){
this.containsPinnednotes--
} else {
this.containsNormalNotes--
}
this.notes.splice(index, 1)
}
})
})
this.$bus.$on('update_fast_filters', newFilter => {
this.fastFilters = newFilter
//Fast filters always return all the results and tags
this.search(true, this.batchSize, false).then( () => {
return this.fetchUserTags()
})
})
//Event to update search from other areas
this.$bus.$on('update_search_term', sentInSearchTerm => {
this.searchTerm = sentInSearchTerm
this.search(true, this.batchSize)
.then( () => {
return this.fetchUserTags()
})
})
//New note button pushes open note event
this.$bus.$on('open_note', noteId => {
this.openNote(noteId)
})
//Reload page content
this.$bus.$on('note_reload', () => {
this.reset()
})
//Mount notes on load if note ID is set
if(this.$route.params && this.$route.params.id){
const id = this.$route.params.id
this.openNote(id)
}
window.addEventListener('scroll', this.onScroll)
//Close notes when back button is pressed
window.addEventListener('hashchange', this.hashChangeAction)
document.addEventListener('visibilitychange', this.visibiltyChangeAction);
},
beforeDestroy(){
console.log('Unbinging all events')
window.removeEventListener('scroll', this.onScroll)
window.removeEventListener('hashchange', this.hashChangeAction)
document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
this.$off() // Remove all event listeners
this.$bus.$off()
},
mounted() {
//Load a super fast batch
this.search(true, this.firstLoadBatchSize, 0)
.then( () => {
//Load a larger batch once first batch has loaded
this.search(false, this.batchSize, true).then( () => {
})
})
//Loads initial batch and tags
this.reset()
},
methods: {
showOneColumn(){
@ -244,7 +266,13 @@
return (this.activeNoteId1 != null || this.activeNoteId2 != null) &&
!this.$store.getters.getIsUserOnMobile
},
openNote(id){
openNote(id, event = null){
//Don't open note if a link is clicked in display card
if(event && event.target && event.target.nodeName){
const nodeClick = event.target.nodeName
if(nodeClick == 'A'){ return }
}
//Do not open same note twice
if(this.activeNoteId1 == id || this.activeNoteId2 == id){
@ -287,12 +315,17 @@
this.activeNoteId2 = null
}
//IF two notes get opened, update ID of open note
if(this.activeNoteId1 || this.activeNoteId2){
this.$router.push('/notes/open/'+Math.max(this.activeNoteId1,this.activeNoteId2))
} else {
//No notes are open, just show notes page
this.$router.push('/notes')
}
this.activeNote1Position = 0
this.activeNote2Position = 0
this.search(false)
},
toggleTagFilter(tagId){
@ -302,7 +335,15 @@
this.searchTags.push(tagId)
}
this.search()
//Reset note set and load up notes and tags
if(this.searchTags.length > 0){
this.search(true, this.batchSize)
return
}
//If no tags are selected, reset entire page
this.reset()
},
onScroll(e){
@ -325,7 +366,7 @@
//If greater than 80 of the way down the page, load the next batch
if(percentageDown >= 80){
console.log('loading batch')
console.log('loading next batch')
this.search(false, this.batchSize, true)
}
@ -334,14 +375,120 @@
return
},
//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,'')
let oldPath = event.oldURL.replace(path,'')
//If we go from open note ID to no note ID, close the note
if(newPath == '' && oldPath.indexOf('/open/') != -1){
//Pull note ID out of URL
const noteIdToClose = oldPath.split('/').pop()
if(this.$refs.note1 && this.$refs.note1.currentNoteId == noteIdToClose){
this.$refs.note1.close()
}
if(this.$refs.note2 && this.$refs.note2.currentNoteId == noteIdToClose){
this.$refs.note2.close()
}
}
},
visibiltyChangeAction(event){
//@TODO - set a timeout on this like 2 minutes or just dont do shit and update it via socket.io
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
// console.log('Welcome back. Reloading a batch')
//Load initial batch, then tags, then other batch
this.search(false, this.firstLoadBatchSize)
.then( () => {
return this.fetchUserTags()
})
}
this.lastVisibilityState = document.visibilityState
},
// @TODO Don't even trigger this if the note wasn't changed
updateSingleNote(noteId){
//Lookup one note using passed in ID
const postData = {
searchQuery: this.searchTerm,
searchTags: this.searchTags,
fastFilters:{
noteIdSet:[noteId]
}
}
axios.post('/api/note/search', postData)
.then(results => {
//Pull note data out of note set
let newNote = results.data.notes[0]
let foundNote = false
if(newNote === undefined){
console.log('Note not visible on this page')
return
}
//Go through each note and find the one just updated
this.notes.forEach( (note,index) => {
if(note.id == noteId){
foundNote = true
//Don't move notes that were not changed
if(note.updated == newNote.updated){
return
}
//Compare note tags, if they changed, reload tags
if(newNote.tag_count != note.tag_count){
console.log('Tags changed, update those bitches')
this.fetchUserTags()
}
//go through each prop and update it with new values
Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop]
})
this.notes.splice(index, 1)
this.notes.unshift(note)
}
})
//This note was not found, update it in list
if(foundNote == false){
if(newNote.pinned == 1){
this.containsPinnednotes++
} else {
this.containsNormalNotes++
}
this.notes.unshift(newNote)
}
})
},
search(showLoading = true, notesInNextLoad = null, mergeExisting = false){
return new Promise((resolve, reject) => {
//Don't double load note batches
if(this.loadingInProgress){
return resolve()
}
//Remove all filter limits
//Reset a lot of stuff if we are not merging batches
if(!mergeExisting){
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
// this.commonTags = [] //Don't reset tags, if search returns tags, they will be set
}
//Remove all filter limits from previous queries
delete this.fastFilters.limitSize
delete this.fastFilters.limitOffset
@ -375,40 +522,50 @@
axios.post('/api/note/search', postData).
then(response => {
if(!mergeExisting){
this.containsNormalNotes = 0
this.containsPinnednotes = 0
this.containsTextResults = 0
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
this.commonTags = []
this.notes = []
}
//Save the number of notes just loaded
this.batchOffset += response.data.notes.length
//Mush the two new sets of data together
this.commonTags = this.commonTags.concat(response.data.tags)
this.notes = this.notes.concat(response.data.notes)
//Mush the two new sets of data together (set will be empty is reset is on)
if(response.data.tags.length > 0){
this.commonTags = response.data.tags
}
//Either reload all notes with return data or merge return data
if(!mergeExisting){
this.notes = response.data.notes
} else {
this.notes = this.notes.concat(response.data.notes)
}
//Go through each note and see which section to display
let textResultsCount = 0
let pinnedResultsCount = 0
let normalNotesCount = 0
response.data.notes.forEach(note => {
if(note.note_highlights.length > 0){
this.containsTextResults++
textResultsCount++
return
}
if(note.pinned == 1){
this.containsPinnednotes++
pinnedResultsCount++
return
}
this.containsNormalNotes++
normalNotesCount++
})
if(!mergeExisting){
this.containsNormalNotes = normalNotesCount
this.containsPinnednotes = pinnedResultsCount
this.containsTextResults = textResultsCount
} else {
this.containsNormalNotes += normalNotesCount
this.containsPinnednotes += pinnedResultsCount
this.containsTextResults += textResultsCount
}
this.working = false
this.loadingInProgress = false
@ -420,7 +577,10 @@
let vm = this
clearTimeout(vm.searchDebounce)
vm.searchDebounce = setTimeout(() => {
vm.search()
this.search(true, this.batchSize)
.then( () => {
return this.fetchUserTags()
})
}, 500)
},
ucWords(str){
@ -435,7 +595,28 @@
this.searchTags = []
this.fastFilters = {}
this.$bus.$emit('reset_fast_filters')
this.search(true, this.firstLoadBatchSize, 0)
//Load initial batch, then tags, then other batch
this.search(true, this.firstLoadBatchSize)
.then( () => {
return this.fetchUserTags()
})
.then( () => {
//Load a larger batch once first batch has loaded
return this.search(false, this.batchSize, true)
})
.then( i => {
//Thats how you promise chain
})
},
fetchUserTags(){
return new Promise((resolve, reject) => {
axios.post('/api/tag/usertags')
.then( ({data}) => {
this.commonTags = data
resolve(data)
})
})
}
}
}

View File

@ -11,8 +11,11 @@ const LoginPage = () => import('@/pages/LoginPage')
// import HelpPage from '@/pages/HelpPage'
const HelpPage = () => import('@/pages/HelpPage')
// import SharePage from '@/pages/SharePage'
const SharePage = () => import('@/pages/SharePage')
//These guys can all be loaded as a chunk
import NotesPage from '@/pages/NotesPage'
import SharePage from '@/pages/SharePage'
import QuickPage from '@/pages/QuickPage'
import AttachmentsPage from '@/pages/AttachmentsPage'

View File

@ -10,6 +10,7 @@ export default new Vuex.Store({
username: null,
nightMode: false,
isUserOnMobile: false,
isNoteSettingsOpen: false, //Little note settings pane
},
mutations: {
setLoginToken(state, userData){
@ -77,7 +78,9 @@ export default new Vuex.Store({
state.isUserOnMobile = true
}
})(navigator.userAgent||navigator.vendor||window.opera, state);
},
toggleNoteSettingsPane(state){
state.isNoteSettingsOpen = !state.isNoteSettingsOpen
}
},
getters: {
@ -96,6 +99,9 @@ export default new Vuex.Store({
},
getIsUserOnMobile: state => {
return state.isUserOnMobile
}
},
getIsNoteSettingsOpen: state => {
return state.isNoteSettingsOpen
},
}
})

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,140 @@
let ProcessText = module.exports = {}
ProcessText.removeHtml = (string) => {
if(string == undefined || string == null || string.length == 0){
return ''
}
return string
.replace(/&[#A-Za-z0-9]+;/g,' ') //Rip out all HTML entities
.replace(/<[^>]+>/g, ' ') //Rip out all HTML tags
.replace(/\s+/g, ' ') //Remove all whitespace
.trim()
}
ProcessText.getUrlsFromString = (string) => {
const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm
return string.match(urlPattern)
}
/*
Pulls out title and subtext of note
+ Title is always first line
+ Empty lines are skipped
+ URLs are turned into links
+ All URLs are givent the target="_blank" property
*/
ProcessText.deduceNoteTitle = (inString) => {
let title = '' //Title of note
let sub = '' //sub text below note
if(!inString || inString == null || inString.length == 0){
return {title, sub}
}
let lines = inString.match(/[^\r\n]+/g)
let finalLines = []
const startTags = ['<ol','<li','<ul']
const endTags = ['</o','</l','</u']
let totalLines = Math.min(lines.length, 6)
let charLimit = 250
let listStart = false
for(let i=0; i < totalLines; i++){
//Just in case 'i' gets bigger than array
if(lines[i] === undefined){
continue
}
const cleanLine = ProcessText.removeHtml(lines[i]).trim().replace('&nbsp','')
const lineStart = lines[i].trim().substring(0, 3)
charLimit -= cleanLine.length
//Close out list if char limit is hit
if(charLimit <= 0 && listStart){
finalLines.push(lines[i])
break
}
//Images appear as empty, push em!
if(cleanLine.length == 0 && lines[i].indexOf('<img') != -1){
finalLines.push(lines[i])
continue
}
//Empty line, may be a list open or close
if(cleanLine.length == 0 && (startTags.includes(lineStart) || endTags.includes(lineStart) )){
if(listStart == false){
charLimit = 400 //Double size for list notes
}
finalLines.push(lines[i])
totalLines++
listStart = true
continue
}
//If line is part of a list, up counter, we want the whole list
if(startTags.includes(lineStart)){
totalLines++
}
//Skip empty lines
if(!cleanLine || cleanLine.length == 0 || cleanLine == '&nbsp;'){
totalLines++
continue
}
//turn urls into links, don't process if its already an <a href=
const containsUrls = ProcessText.getUrlsFromString(cleanLine)
if(containsUrls && containsUrls.length == 1 && lines[i].indexOf('</a>') == -1){
const url = containsUrls[0]
lines[i] = lines[i].replace(url, `<a href="${url}">${url}</a>`)
}
//Insert target=_blank into links if set, do it for every link in line
if(lines[i].indexOf('</a>') > 0){
lines[i] = lines[i].replace(/<a /g, '<a target="_blank" ')
}
//Limit output characters
//Check character limit
if(charLimit <= 0 && listStart == false){
//Cut the string down to character limit
const cutString = lines[i].substring(0, lines[i].length+charLimit)
//Find last space and cut off everything after it
let cleanCutString = cutString.substring(0, cutString.lastIndexOf(' '))
//Some strings may not contain a space resulting in no string
if(cleanCutString.length == 0){
cleanCutString = cutString
}
finalLines.push(cleanCutString + '...')
break;
}
finalLines.push(lines[i])
}
//Pull out title if its not an empty string
if(ProcessText.removeHtml(finalLines[0]).trim().replace('&nbsp','').length > 0){
title = finalLines.shift()
}
sub = finalLines.join('')
//Return final display lengths
let titleLength = ProcessText.removeHtml(title).trim().replace('&nbsp','').length
let subtextLength = ProcessText.removeHtml(sub).trim().replace('&nbsp','').length
return { title, sub, titleLength, subtextLength }
}

View File

@ -117,6 +117,7 @@ Attachment.scanTextForWebsites = (userId, noteId, noteText) => {
Attachment.urlForNote(userId, noteId).then(attachments => {
//Find all URLs in text
//@TODO - Use the process text library for this function
const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm
let allUrls = noteText.match(urlPattern)

View File

@ -5,7 +5,10 @@ let Attachment = require('@models/Attachment')
let ProcessText = require('@helpers/ProcessText')
const DiffMatchPatch = require('@helpers/DiffMatchPatch')
var rp = require('request-promise');
const fs = require('fs')
let Note = module.exports = {}
@ -137,19 +140,94 @@ Note.update = (userId, noteId, noteText, color, pinned, archived) => {
})
}
//
// Delete a note and all its remaining parts
//
Note.delete = (userId, noteId) => {
return new Promise((resolve, reject) => {
db.promise().query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId,userId])
.then((rows, fields) => {
db.promise().query('DELETE FROM attachment WHERE attachment.note_id = ? AND attachment.user_id = ?', [noteId,userId])
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) => {
db.promise().query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
//Select all attachments with files
return db.promise().query('SELECT file_location FROM attachment WHERE attachment.note_id = ? AND attachment.user_id = ?', [noteId,userId])
})
.then((attachmentRows, fields) => {
//Go through each selected attachment and delete the files
attachmentRows[0].forEach( location => {
const fileName = location['file_location']
if(fileName != null && fileName.length > 1){
fs.unlink('../staticFiles/'+fileName ,function(err){ //Async, just rip through them.
if(err) return console.log(err);
// console.log('file deleted successfully => ', fileName);
})
}
})
return db.promise().query('DELETE FROM attachment WHERE attachment.note_id = ? AND attachment.user_id = ?', [noteId,userId])
})
.then((rows, fields) => {
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)
})
})
}
//text is the current text for the note that will be compared to the text in the database
Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
return new Promise((resolve, reject) => {
Note.get(userId, noteId)
.then(noteObject => {
let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
let newText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
if(noteObject.updated == lastUpdated){
console.log('No note diff')
resolve(null)
}
if(noteObject.updated > lastUpdated){
newText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
oldText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
}
const dmp = new DiffMatchPatch.diff_match_patch()
const diff = dmp.diff_main(oldText, newText)
dmp.diff_cleanupSemantic(diff)
const patch_list = dmp.patch_make(oldText, newText, diff);
const patch_text = dmp.patch_toText(patch_list);
//Patch text - shows a list of changes
var patches = dmp.patch_fromText(patch_text);
// console.log(patch_text)
//results[1] - contains diagnostic data for patch apply, its possible it can fail
var results = dmp.patch_apply(patches, oldText);
//Compile return data for front end
const returnData = {
updatedText: results[0],
diffs: results[1].length, //Only use length for now
updated: Math.max(noteObject.updated,lastUpdated) //Return most recently updated date
}
//Final change in notes
console.log(returnData)
resolve(returnData)
})
})
}
Note.get = (userId, noteId) => {
@ -184,6 +262,7 @@ Note.getShared = (noteId) => {
})
}
// Searches text index, returns nothing if there is no search query
Note.solrQuery = (userId, searchQuery, searchTags) => {
return new Promise((resolve, reject) => {
@ -243,27 +322,28 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
Note.solrQuery(userId, searchQuery, searchTags).then( (textSearchResults) => {
//Pull out search results from previous query
let textSearchIds = []
let highlights = {}
let returnTagResults = false
if(textSearchResults != null){
textSearchIds = textSearchResults['ids']
highlights = textSearchResults['snippets']
}
//No results, return empty data
if(textSearchIds.length == 0 && searchQuery.length > 0){
return resolve(returnData)
}
//Default note lookup gets all notes
// Base of the query, modified with fastFilters
// Add to query for character counts -> CHAR_LENGTH(note.text) as chars
let noteSearchQuery = `
SELECT note.id,
SUBSTRING(note.text, 1, 400) as text,
updated, color,
SUBSTRING(note.text, 1, 1500) as text,
updated,
color,
count(distinct note_tag.id) as tag_count,
count(distinct attachment.id) as attachment_count,
note.pinned,
@ -274,22 +354,26 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
WHERE note.user_id = ?`
let searchParams = [userId]
//If text search returned results, limit search to those ids
if(textSearchIds.length > 0){
searchParams.push(textSearchIds)
noteSearchQuery += ' AND note.id IN (?)'
}
if(searchTags.length > 0){
if(fastFilters.noteIdSet && fastFilters.noteIdSet.length > 0){
searchParams.push(fastFilters.noteIdSet)
noteSearchQuery += ' AND note.id IN (?)'
}
//If tags are passed, use those tags in search
if(searchTags.length > 0){
searchParams.push(searchTags)
noteSearchQuery += ' AND note_tag.tag_id IN (?)'
}
//Toggle archived, show archived if tags are searched
// - archived will show archived in search results
// - onlyArchive will exclude notes that are not archived
if(fastFilters.archived == 1 || searchTags.length > 0 || fastFilters.onlyArchived == 1){
//Do nothing
//Show archived notes, only if fast filter is set, default to not archived
if(fastFilters.onlyArchived == 1){
noteSearchQuery += ' AND note.archived = 1' //Show Archived
} else {
noteSearchQuery += ' AND note.archived = 0' //Exclude archived
}
@ -299,14 +383,17 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
//Only show notes with Tags
if(fastFilters.withTags == 1){
returnTagResults = true
noteSearchQuery += ' HAVING tag_count > 0'
}
//Only show notes with links
if(fastFilters.withLinks == 1){
returnTagResults = true
noteSearchQuery += ' HAVING attachment_count > 0'
}
//Only show archived notes
if(fastFilters.onlyArchived == 1){
returnTagResults = true
noteSearchQuery += ' HAVING note.archived = 1'
}
@ -336,10 +423,13 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
const limitOffset = parseInt(fastFilters.limitOffset, 10) || 0 //Either parse int, or use zero
console.log(` LIMIT ${limitOffset}, ${limitSize}`)
// console.log(` LIMIT ${limitOffset}, ${limitSize}`)
noteSearchQuery += ` LIMIT ${limitOffset}, ${limitSize}`
}
// console.log('------------- Final Query --------------')
// console.log(noteSearchQuery)
// console.log('------------- ----------- --------------')
db.promise()
.query(noteSearchQuery, searchParams)
@ -357,37 +447,26 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
if(note.text == null){ note.text = '' }
//Attempt to pull string out of first tag in note
let reg = note.text.match(/<([\w]+)[^>]*>(.*?)<\/\1>/g)
//Deduce note title
const textData = ProcessText.deduceNoteTitle(note.text)
//Pull out first html tag contents, that is the title
if(reg != null && reg[0]){
note.title = reg[0] //First line from HTML
} else {
note.title = note.text //Entire note
}
//Clean up html title
note.title = ProcessText.removeHtml(note.title)
//Generate Subtext
note.subtext = ''
if(note.text != '' && note.title != ''){
note.subtext = ProcessText.removeHtml(note.text)
.substring(note.title.length)
}
// console.log(textData)
note.title = textData.title
note.subtext = textData.sub
note.titleLength = textData.titleLength
note.subtextLength = textData.subtextLength
note.note_highlights = []
note.attachment_highlights = []
note.tag_highlights = []
//Push in solr highlights
//Push in search highlights
if(highlights && highlights[note.id]){
note['note_highlights'] = [highlights[note.id]]
}
//Clear out note.text before sending it to front end
//Clear out note.text before sending it to front end, its being used in title and subtext
delete note.text
})
@ -396,6 +475,13 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
return resolve(returnData)
}
//Return all notes, tags are not being searched
// if tags are being searched, continue
// if notes are being filtered, return tags
if(searchTags.length == 0 && returnTagResults == false){
return resolve(returnData)
}
//Only show tags of selected notes
db.promise()
.query(`SELECT tag.id, tag.text, count(tag.id) as usages FROM note_tag

View File

@ -2,6 +2,21 @@ let db = require('@config/database')
let Tag = module.exports = {}
Tag.userTags = (userId) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`
SELECT tag.id, text, COUNT(note_tag.note_id) as usages FROM tag
JOIN note_tag ON tag.id = note_tag.tag_id
WHERE note_tag.user_id = ?
GROUP BY tag.id
ORDER BY usages DESC
`, [userId])
.then( (rows, fields) => {
resolve(rows[0])
})
})
}
Tag.removeTagFromNote = (userId, tagId) => {
return new Promise((resolve, reject) => {

View File

@ -1,7 +1,7 @@
let express = require('express')
var multer = require('multer')
var upload = multer({ dest: '../staticFiles/' })
var upload = multer({ dest: '../staticFiles/' }) //@TODO make this a global value
let router = express.Router()
let Attachment = require('@models/Attachment');

View File

@ -38,6 +38,15 @@ router.post('/search', function (req, res) {
.then( notesAndTags => res.send(notesAndTags))
})
router.post('/difftext', function (req, res) {
Notes.getDiffText(userId, req.body.noteId, req.body.text, req.body.updated)
.then( fullDiffText => {
//Response should be full diff text
res.send(fullDiffText)
})
})
//Reindex all notes. Not a very good function, not public
router.get('/reindex5yu43prchuj903mrc', function (req, res) {

View File

@ -42,4 +42,10 @@ router.post('/get', function (req, res) {
.then( data => res.send(data) )
})
//Get all the tags for this user in order of usage
router.post('/usertags', function (req, res) {
Tags.userTags(userId)
.then( data => res.send(data) )
})
module.exports = router