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:
parent
6fe39406b7
commit
abb4e20ec3
@ -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 -p***REMOV
|
||||
|
||||
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
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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>
|
@ -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!!');
|
||||
})
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
44
client/src/components/SearchInput.vue
Normal file
44
client/src/components/SearchInput.vue
Normal 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>
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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
|
||||
},
|
||||
}
|
||||
})
|
2236
server/helpers/DiffMatchPatch.js
Normal file
2236
server/helpers/DiffMatchPatch.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -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(' ','')
|
||||
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 == ' '){
|
||||
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(' ','').length > 0){
|
||||
title = finalLines.shift()
|
||||
}
|
||||
|
||||
sub = finalLines.join('')
|
||||
|
||||
//Return final display lengths
|
||||
let titleLength = ProcessText.removeHtml(title).trim().replace(' ','').length
|
||||
let subtextLength = ProcessText.removeHtml(sub).trim().replace(' ','').length
|
||||
|
||||
|
||||
return { title, sub, titleLength, subtextLength }
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
@ -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');
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user