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
8d07a8e11a
commit
4216c1825e
@ -1,8 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
BACKUPDIR="databaseBackupPi"
|
BACKUPDIR="/home/mab/databaseBackupPi"
|
||||||
|
|
||||||
cd ..
|
|
||||||
mkdir -p $BACKUPDIR
|
mkdir -p $BACKUPDIR
|
||||||
cd $BACKUPDIR
|
cd $BACKUPDIR
|
||||||
|
|
||||||
@ -11,9 +10,9 @@ ssh mab@avidhabit.com -p 13328 "mysqldump --all-databases --user root -pRootPass
|
|||||||
|
|
||||||
cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/backup-$NOW.sql"
|
cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/backup-$NOW.sql"
|
||||||
|
|
||||||
echo "Database Backup Complete"
|
echo "Database Backup Complete on $NOW"
|
||||||
|
|
||||||
#Restore DB
|
#Restore DB
|
||||||
# copy file over, run restore
|
# copy file over, run restore
|
||||||
# scp -P 13328 backup-2019-12-04_03-00.sql mab@avidhabit.com:/home/mab
|
# scp -P 13328 backup-2019-12-04_03-00.sql mab@avidhabit.com:/home/mab
|
||||||
# mysql -u root -p < backup-2019-12-04_03-00.sql
|
# mysql -u root -p < backup-2019-12-04_03-00.sql
|
||||||
|
@ -42,7 +42,7 @@ module.exports = {
|
|||||||
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
|
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',
|
loader: 'url-loader',
|
||||||
options: {
|
options: {
|
||||||
limit: 10000,
|
limit: 10000,
|
||||||
@ -58,7 +58,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(eot|ttf|otf|woff|woff2|svg)(\?.*)?$/,
|
test: /\.(eot|ttf|otf|woff|woff2)(\?.*)?$/,
|
||||||
loader: 'url-loader',
|
loader: 'url-loader',
|
||||||
options: {
|
options: {
|
||||||
limit: 10000,
|
limit: 10000,
|
||||||
|
@ -97,12 +97,16 @@ div.ui.basic.green.label {
|
|||||||
/* Styles for public display pages */
|
/* Styles for public display pages */
|
||||||
|
|
||||||
.note-status-indicator {
|
.note-status-indicator {
|
||||||
float: right;
|
position: absolute;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
padding: 9px 0;
|
padding: 16px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text_color);
|
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>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="ui clickable basic button">
|
<div class="ui small compact basic icon button clickable">
|
||||||
<form>
|
<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()" />
|
<input class="hidden-up" type="file" :id="`upfile-${noteId}`" ref="file" v-on:change="handleFileUpload()" />
|
||||||
</form>
|
</form>
|
||||||
<!-- <button v-if="file" v-on:click="uploadFileToServer()">Submit</button> -->
|
<!-- <button v-if="file" v-on:click="uploadFileToServer()">Submit</button> -->
|
||||||
@ -47,7 +50,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
).then(results => {
|
).then(results => {
|
||||||
this.uploadPercentage = 'DONE'
|
this.uploadPercentage = 0
|
||||||
this.file = null
|
this.file = null
|
||||||
console.log('SUCCESS!!');
|
console.log('SUCCESS!!');
|
||||||
|
|
||||||
@ -76,7 +79,7 @@
|
|||||||
|
|
||||||
})
|
})
|
||||||
.catch(results => {
|
.catch(results => {
|
||||||
this.uploadPercentage = 'FAIL'
|
this.uploadPercentage = 0
|
||||||
console.log('FAILURE!!');
|
console.log('FAILURE!!');
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -21,10 +21,10 @@
|
|||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 10px 0px 10px 10px;
|
padding: 0.8em 0px 0.8em 0.8em;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 1.1rem;
|
font-size: 1.15em;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.sub {
|
.sub {
|
||||||
@ -60,28 +60,60 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.top-menu-bar {
|
.top-menu-bar {
|
||||||
color: var(--text_color);
|
/*color: var(--text_color);*/
|
||||||
width: calc(100% - 20px);
|
/*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 {
|
.top-menu-bar img {
|
||||||
width: 20px;
|
width: 30px;
|
||||||
height: 20px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
|
<div class="place-holder" v-if="collapsed && !menuOpen"></div>
|
||||||
|
|
||||||
<!-- collapsed menu, appears as a bar -->
|
<!-- 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="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>
|
</div>
|
||||||
<div class="six wide center aligned column">
|
<div class="ten wide center aligned column">
|
||||||
<img src="/api/static/assets/favicon.ico" alt="logo" />
|
<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 class="five wide right aligned column"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -94,7 +126,7 @@
|
|||||||
|
|
||||||
<div class="menu-section">
|
<div class="menu-section">
|
||||||
<div class="menu-item menu-button" v-on:click="collapseMenu">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -108,7 +140,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-section" v-if="loggedIn">
|
<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
|
<i class="file icon"></i>Notes
|
||||||
</router-link>
|
</router-link>
|
||||||
<div>
|
<div>
|
||||||
@ -168,13 +200,16 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
'search-input': require('@/components/SearchInput.vue').default,
|
||||||
|
},
|
||||||
data: function(){
|
data: function(){
|
||||||
return {
|
return {
|
||||||
username: '',
|
username: '',
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
mobile: false,
|
mobile: false,
|
||||||
disableNewNote: false
|
disableNewNote: false,
|
||||||
|
menuOpen: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeCreate: function(){
|
beforeCreate: function(){
|
||||||
@ -182,6 +217,10 @@
|
|||||||
mounted: function(){
|
mounted: function(){
|
||||||
this.mobile = this.$store.getters.getIsUserOnMobile
|
this.mobile = this.$store.getters.getIsUserOnMobile
|
||||||
this.collapsed = this.$store.getters.getIsUserOnMobile
|
this.collapsed = this.$store.getters.getIsUserOnMobile
|
||||||
|
|
||||||
|
if(this.mobile){
|
||||||
|
this.menuOpen = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
loggedIn () {
|
loggedIn () {
|
||||||
@ -194,10 +233,18 @@
|
|||||||
//Collapse menu when item is clicked in mobile
|
//Collapse menu when item is clicked in mobile
|
||||||
if(this.mobile && !this.collapsed){
|
if(this.mobile && !this.collapsed){
|
||||||
this.collapsed = true
|
this.collapsed = true
|
||||||
|
this.menuOpen = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
collapseMenu(){
|
collapseMenu(){
|
||||||
this.collapsed = !this.collapsed
|
this.collapsed = !this.collapsed
|
||||||
|
|
||||||
|
if(!this.collapsed){
|
||||||
|
this.menuOpen = true
|
||||||
|
} else {
|
||||||
|
this.menuOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
createNote(event){
|
createNote(event){
|
||||||
const title = ''
|
const title = ''
|
||||||
@ -227,6 +274,10 @@
|
|||||||
return $1.toUpperCase()
|
return $1.toUpperCase()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
emitReloadEvent(){
|
||||||
|
//Reloads note page to initial state
|
||||||
|
this.$bus.$emit('note_reload')
|
||||||
|
},
|
||||||
updateFastFilters(index){
|
updateFastFilters(index){
|
||||||
|
|
||||||
//A little hacky, brings user to notes page then filters on click
|
//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">
|
<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>
|
<i class="grey trash alternate icon"></i>
|
||||||
</span>
|
</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>
|
<i class="red trash alternate icon"></i>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@ -26,9 +26,11 @@
|
|||||||
this.click++
|
this.click++
|
||||||
},
|
},
|
||||||
actuallyDelete(){
|
actuallyDelete(){
|
||||||
|
|
||||||
axios.post('/api/note/delete', {'noteId':this.noteId}).then(response => {
|
axios.post('/api/note/delete', {'noteId':this.noteId}).then(response => {
|
||||||
if(response.data == true){
|
if(response.data == true){
|
||||||
this.$bus.$emit('note_deleted')
|
console.log('Lets delete this note!')
|
||||||
|
this.$bus.$emit('note_deleted', this.noteId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -15,40 +15,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Menu -->
|
<span class="note-status-indicator" v-on:click="save()">{{statusText}}</span>
|
||||||
<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="">
|
<div class="tinymce-container">
|
||||||
<i class="pin icon" :class="{green:(pinned == 1)}"></i> {{(pinned == 1)?'Pinned':''}}
|
<textarea :id="noteid+'-tinymce-editor'">{{noteText}}</textarea>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<textarea :id="noteid+'-tinymce-editor'">{{noteText}}</textarea>
|
|
||||||
|
|
||||||
<note-tag-edit v-if="!$store.getters.getIsUserOnMobile" :noteId="noteid" :key="'tags-for-note-'+noteid"/>
|
|
||||||
|
|
||||||
<color-picker
|
<color-picker
|
||||||
v-if="colorPickerVisible"
|
v-if="colorPickerVisible"
|
||||||
:location="colorPickerLocation"
|
:location="colorPickerLocation"
|
||||||
@ -56,7 +28,51 @@
|
|||||||
:style-object="styleObject"
|
: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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -72,6 +88,7 @@
|
|||||||
'note-tag-edit': require('@/components/NoteTagEdit.vue').default,
|
'note-tag-edit': require('@/components/NoteTagEdit.vue').default,
|
||||||
'color-picker': require('@/components/ColorPicker.vue').default,
|
'color-picker': require('@/components/ColorPicker.vue').default,
|
||||||
'file-upload-button': require('@/components/FileUploadButton.vue').default,
|
'file-upload-button': require('@/components/FileUploadButton.vue').default,
|
||||||
|
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
|
||||||
},
|
},
|
||||||
data(){
|
data(){
|
||||||
return {
|
return {
|
||||||
@ -95,6 +112,10 @@
|
|||||||
colorPickerLocation: null,
|
colorPickerLocation: null,
|
||||||
|
|
||||||
tinymce: null, //Initialized editor instance
|
tinymce: null, //Initialized editor instance
|
||||||
|
|
||||||
|
//Settings vars
|
||||||
|
showAllSettings: true,
|
||||||
|
lastVisibilityState: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -114,10 +135,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount(){
|
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(){
|
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
|
//Trash editor instance on close
|
||||||
this.tinymce.remove()
|
this.tinymce.remove()
|
||||||
|
|
||||||
@ -127,6 +159,8 @@
|
|||||||
//Change TinyMce styles on nightmored change
|
//Change TinyMce styles on nightmored change
|
||||||
this.$bus.$on('toggle_night_mode', this.setEditorTextColor )
|
this.$bus.$on('toggle_night_mode', this.setEditorTextColor )
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', this.checkForUpdatedNote)
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.loadNote(this.noteid)
|
this.loadNote(this.noteid)
|
||||||
})
|
})
|
||||||
@ -139,10 +173,17 @@
|
|||||||
// {title: 'My image 2', value: 'http://www.moxiecode.com/my2.gif'}
|
// {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
|
//Tweak doc height for mobile
|
||||||
let docHeight = 'calc(100vh - 90px)'
|
let docHeight = 'calc(100vh - 1px)'
|
||||||
if(this.$store.getters.getIsUserOnMobile){
|
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
|
//setup skin as dark if night mode is enabled
|
||||||
@ -152,12 +193,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const editorId = '#'+this.noteid+'-tinymce-editor'
|
const editorId = '#'+this.noteid+'-tinymce-editor'
|
||||||
|
let vm = this
|
||||||
|
|
||||||
//Globally defined included in index HTML
|
//Globally defined included in index HTML
|
||||||
tinymce.init({
|
tinymce.init({
|
||||||
selector: editorId,
|
selector: editorId,
|
||||||
toolbar: 'forecolor backcolor styleselect | bold italic underline | link image | code | undo redo | bullist numlist | outdent indent table, hr, searchreplace | removeformat',
|
toolbar: toolbarOptions,
|
||||||
plugins: 'paste, link, code, lists, table, hr, searchreplace, image',
|
plugins: pluginOptions,
|
||||||
browser_spellcheck: true,
|
browser_spellcheck: true,
|
||||||
menubar: false,
|
menubar: false,
|
||||||
branding: false,
|
branding: false,
|
||||||
@ -167,6 +209,25 @@
|
|||||||
contextmenu: false,
|
contextmenu: false,
|
||||||
init_instance_callback: this.editorInitCallback,
|
init_instance_callback: this.editorInitCallback,
|
||||||
imagetools_toolbar: "imageoptions",
|
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){
|
editorInitCallback(editor){
|
||||||
@ -197,7 +258,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
setText(inText){
|
||||||
|
return this.tinymce.setContent(inText)
|
||||||
},
|
},
|
||||||
getText(){
|
getText(){
|
||||||
//Return text from tinyMce Editor
|
//Return text from tinyMce Editor
|
||||||
@ -208,7 +271,6 @@
|
|||||||
this.colorPickerLocation = {'x':event.clientX, 'y':event.clientY}
|
this.colorPickerLocation = {'x':event.clientX, 'y':event.clientY}
|
||||||
},
|
},
|
||||||
openEditAttachment(){
|
openEditAttachment(){
|
||||||
// this.$bus.$emit('open_edit_attachment', this.currentNoteId)
|
|
||||||
this.$router.push('/attachments/note/'+this.currentNoteId)
|
this.$router.push('/attachments/note/'+this.currentNoteId)
|
||||||
},
|
},
|
||||||
onTogglePinned(){
|
onTogglePinned(){
|
||||||
@ -300,11 +362,12 @@
|
|||||||
save(){
|
save(){
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
//Clear other debounced events to prevent double calling of save
|
//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
|
//Don't save note if its hash doesn't change
|
||||||
const currentNoteText = this.getText()
|
const currentNoteText = this.getText()
|
||||||
if( this.lastNoteHash == this.hashString( currentNoteText )){
|
if( this.lastNoteHash == this.hashString( currentNoteText )){
|
||||||
|
this.statusText = 'Saved'
|
||||||
return resolve(true)
|
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){
|
hashString(text){
|
||||||
|
|
||||||
var hash = 0;
|
var hash = 0;
|
||||||
@ -369,7 +460,7 @@
|
|||||||
this.sizeDown = true
|
this.sizeDown = true
|
||||||
//This timeout allows animation to play before closing
|
//This timeout allows animation to play before closing
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$bus.$emit('close_active_note', this.position)
|
this.$bus.$emit('close_active_note', {position: this.position, noteId: this.noteid})
|
||||||
return
|
return
|
||||||
}, 300)
|
}, 300)
|
||||||
})
|
})
|
||||||
@ -381,6 +472,28 @@
|
|||||||
|
|
||||||
<style type="text/css" scoped>
|
<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 {
|
.note-top-menu {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-on:mouseover="fullTagEdit = true">
|
<div v-on:click="fullTagEdit = true">
|
||||||
|
|
||||||
<!-- simple string view -->
|
<!-- simple string view -->
|
||||||
<div v-if="!fullTagEdit" class="ui basic segment">
|
<!-- class="ui basic segment" -->
|
||||||
|
<div v-if="!fullTagEdit">
|
||||||
<div class="simple-tag-display">
|
<div class="simple-tag-display">
|
||||||
|
|
||||||
<!-- Show Loading -->
|
<!-- Show Loading -->
|
||||||
@ -10,11 +11,11 @@
|
|||||||
|
|
||||||
<!-- Default count -->
|
<!-- Default count -->
|
||||||
<span v-if="loaded">
|
<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>
|
</span>
|
||||||
|
|
||||||
<!-- No tags default text -->
|
<!-- 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
|
<i class="plus icon"></i> Add a tag
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@ -28,6 +29,13 @@
|
|||||||
<!-- hover over view -->
|
<!-- hover over view -->
|
||||||
<div v-if="fullTagEdit" class="full-tag-area fade-in-fwd" v-on:mouseleave="fullTagEdit = false; clearSuggestions()">
|
<div v-if="fullTagEdit" class="full-tag-area fade-in-fwd" 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 -->
|
<!-- tag input and suggestion popup -->
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<input
|
<input
|
||||||
@ -45,13 +53,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -233,28 +234,27 @@
|
|||||||
|
|
||||||
/* note tag edit area */
|
/* note tag edit area */
|
||||||
.full-tag-area {
|
.full-tag-area {
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
color: var(--text_color);
|
color: var(--text_color);
|
||||||
background-color: var(--background_color);
|
background-color: var(--background_color);
|
||||||
padding: 15px;
|
/*padding: 15px;*/
|
||||||
border: 1px solid;
|
/*border: 1px solid;*/
|
||||||
border-color: var(--border_color);
|
border-color: var(--border_color);
|
||||||
}
|
}
|
||||||
.full-tag-area .delete-tag-display {
|
.full-tag-area .delete-tag-display {
|
||||||
margin-top: 15px;
|
/*margin-top: 15px;*/
|
||||||
}
|
}
|
||||||
.full-tag-area .ui.label {
|
.full-tag-area .ui.label {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
.simple-tag-display {
|
.simple-tag-display {
|
||||||
width: 100%;
|
width: calc(100% - 0px);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
max-height: 35px;
|
max-height: 35px;
|
||||||
|
color: var(--text_color);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tag suggestion box styles */
|
/* tag suggestion box styles */
|
||||||
@ -269,8 +269,8 @@
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: white;
|
background-color: var(--background_color);
|
||||||
color: black;
|
color: var(--text_color);
|
||||||
}
|
}
|
||||||
.suggestion-item.active {
|
.suggestion-item.active {
|
||||||
background: green;
|
background: green;
|
||||||
|
@ -11,33 +11,45 @@
|
|||||||
<div class="ui grid max-height">
|
<div class="ui grid max-height">
|
||||||
|
|
||||||
<!-- Show title and snippet below it -->
|
<!-- 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 -->
|
<!-- Display highlights from solr results -->
|
||||||
<div v-if="note.note_highlights.length > 0 && textResults" class="term-usage">
|
<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
|
||||||
|
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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="sixteen wide column overflow-hidden" v-if="isShowingSearchResults()">
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toolbar on the bottom -->
|
<!-- Toolbar on the bottom -->
|
||||||
<div class="bottom aligned row icon-bar" @click.self.stop="onClick(note.id)">
|
<div class="bottom aligned row icon-bar" @click.self="onClick(note.id)">
|
||||||
<div class="six wide column clickable" @click.stop="onClick(note.id)">
|
<div class="six wide column clickable" @click="onClick(note.id)">
|
||||||
{{$helpers.timeAgo(note.updated)}}
|
{{$helpers.timeAgo(note.updated)}}
|
||||||
<!-- {{(note.chars.toLocaleString())}} -->
|
|
||||||
</div>
|
</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="">
|
<span v-if="note.pinned == 1" data-position="top right" data-tooltip="Pinned" data-inverted="">
|
||||||
<i class="green pin icon"></i>
|
<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="">
|
<span v-if="note.archived == 1" data-position="top right" data-tooltip="Archived" data-inverted="">
|
||||||
<i class="green archive icon"></i>
|
<i class="green archive icon"></i>
|
||||||
</span>
|
</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}}
|
<i class="linkify icon"></i> {{note.attachment_count}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="note.tag_count == 1" data-position="top right" data-tooltip="Note has 1 tag" data-inverted="">
|
<span v-if="note.tag_count == 1" data-position="top right" data-tooltip="Note has 1 tag" data-inverted="">
|
||||||
@ -88,7 +100,6 @@
|
|||||||
},
|
},
|
||||||
openEditAttachment(){
|
openEditAttachment(){
|
||||||
this.$router.push('/attachments/note/'+this.note.id)
|
this.$router.push('/attachments/note/'+this.note.id)
|
||||||
// this.$bus.$emit('open_edit_attachment', this.note.id)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@ -132,6 +143,20 @@
|
|||||||
</script>
|
</script>
|
||||||
<style type="text/css">
|
<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 {
|
.note-title-display-card h3 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -166,7 +191,7 @@
|
|||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-color: var(--border_color);
|
border-color: var(--border_color);
|
||||||
width: calc(33.333% - 10px);
|
width: calc(33.333% - 10px);
|
||||||
transition: box-shadow 0.3s;
|
/*transition: box-shadow 0.3s;*/
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
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.
|
// This callback runs before every route change, including on page load.
|
||||||
// Sets the title of the page using vue router
|
// Sets the title of the page using vue router
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
|
@ -62,6 +62,12 @@
|
|||||||
100%{background-position:0% 50%}
|
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>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -4,15 +4,14 @@
|
|||||||
<div class="ui grid" :class="{ 'mush-it-up':showOneColumn() }" ref="content">
|
<div class="ui grid" :class="{ 'mush-it-up':showOneColumn() }" ref="content">
|
||||||
|
|
||||||
<!-- Note filter options -->
|
<!-- Note filter options -->
|
||||||
<div class="row">
|
<div class="row" v-if="!$store.getters.getIsUserOnMobile">
|
||||||
|
|
||||||
<div
|
<div :class="{ 'sixteen wide column':showOneColumn(), 'eight wide column':!showOneColumn() }">
|
||||||
:class="{ 'sixteen wide column':showOneColumn(), 'eight wide column':!showOneColumn() }"
|
|
||||||
>
|
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="ten wide field">
|
<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>
|
||||||
<div class="six wide field">
|
<div class="six wide field">
|
||||||
<span class="ui fluid green button"
|
<span class="ui fluid green button"
|
||||||
@ -20,16 +19,19 @@
|
|||||||
@click="reset">
|
@click="reset">
|
||||||
<i class="undo icon"></i>Reset Filters
|
<i class="undo icon"></i>Reset Filters
|
||||||
</span>
|
</span>
|
||||||
|
<!-- <fast-filters /> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
:class="{ 'sixteen wide column':showOneColumn(), 'eight wide column':!showOneColumn() }"
|
|
||||||
>
|
<div v-if="$store.getters.getIsUserOnMobile && showClear" class="row">
|
||||||
<h2 class="ui right floated">
|
<div class="sixteen wide column">
|
||||||
<fast-filters />
|
<span class="ui fluid green button"
|
||||||
</h2>
|
@click="reset">
|
||||||
|
<i class="undo icon"></i>Reset Filters
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -64,7 +66,8 @@
|
|||||||
|
|
||||||
<!-- pinned notes -->
|
<!-- pinned notes -->
|
||||||
<div v-if="containsPinnednotes > 0" class="note-card-section">
|
<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">
|
<div class="note-card-display-area">
|
||||||
<note-title-display-card
|
<note-title-display-card
|
||||||
v-for="note in notes"
|
v-for="note in notes"
|
||||||
@ -79,7 +82,8 @@
|
|||||||
|
|
||||||
<!-- normal notes -->
|
<!-- normal notes -->
|
||||||
<div v-if="containsNormalNotes > 0" class="note-card-section">
|
<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">
|
<div class="note-card-display-area">
|
||||||
<note-title-display-card
|
<note-title-display-card
|
||||||
v-for="note in notes"
|
v-for="note in notes"
|
||||||
@ -114,12 +118,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" />
|
<input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" ref="note1" />
|
||||||
<input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" />
|
<input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" ref="note2" />
|
||||||
|
|
||||||
<div v-if="openAttachmentEdit">
|
|
||||||
<edit-attachment :note-id="editAttchmentId" :key="editAttchmentId" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -134,7 +134,7 @@
|
|||||||
'input-notes': require('@/components/NoteInputPanel.vue').default,
|
'input-notes': require('@/components/NoteInputPanel.vue').default,
|
||||||
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
|
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
|
||||||
'fast-filters': require('@/components/FastFilters.vue').default,
|
'fast-filters': require('@/components/FastFilters.vue').default,
|
||||||
'edit-attachment': require('@/components/AttachmentEditor.vue').default,
|
'search-input': require('@/components/SearchInput.vue').default,
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -153,6 +153,7 @@
|
|||||||
batchOffset: 0, //Tracks the current batch that has been loaded
|
batchOffset: 0, //Tracks the current batch that has been loaded
|
||||||
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
|
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
|
||||||
loadingInProgress: false,
|
loadingInProgress: false,
|
||||||
|
fetchTags: false,
|
||||||
|
|
||||||
//Clear button is not visible
|
//Clear button is not visible
|
||||||
showClear: false,
|
showClear: false,
|
||||||
@ -162,8 +163,8 @@
|
|||||||
containsNormalNotes: 0,
|
containsNormalNotes: 0,
|
||||||
containsPinnednotes: 0,
|
containsPinnednotes: 0,
|
||||||
containsTextResults: 0,
|
containsTextResults: 0,
|
||||||
containsTagResults: 0,
|
// containsTagResults: 0,
|
||||||
containsAttachmentResults: 0,
|
// containsAttachmentResults: 0,
|
||||||
|
|
||||||
//Currently open notes in app
|
//Currently open notes in app
|
||||||
activeNoteId1: null,
|
activeNoteId1: null,
|
||||||
@ -173,47 +174,14 @@
|
|||||||
activeNote1Position: 0,
|
activeNote1Position: 0,
|
||||||
activeNote2Position: 0,
|
activeNote2Position: 0,
|
||||||
|
|
||||||
//Attchment to edit. Only 1 for now
|
lastVisibilityState: null,
|
||||||
openAttachmentEdit: false,
|
|
||||||
editAttchmentId: null,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount(){
|
beforeMount(){
|
||||||
|
|
||||||
this.$parent.loginGateway()
|
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
|
//Load tinymce into the page only do it once
|
||||||
if(document.querySelectorAll('[data-mceload]').length == 0){
|
if(document.querySelectorAll('[data-mceload]').length == 0){
|
||||||
let tinyMceIncluder = document.createElement('script')
|
let tinyMceIncluder = document.createElement('script')
|
||||||
@ -222,21 +190,75 @@
|
|||||||
document.head.appendChild(tinyMceIncluder)
|
document.head.appendChild(tinyMceIncluder)
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
this.$bus.$on('close_active_note', ({position, noteId}) => {
|
||||||
beforeDestroy(){
|
this.closeNote(position)
|
||||||
window.removeEventListener('scroll', this.onScroll)
|
this.updateSingleNote(noteId)
|
||||||
},
|
})
|
||||||
mounted() {
|
this.$bus.$on('note_deleted', (noteId) => {
|
||||||
|
//Remove deleted note from set, its deleted
|
||||||
//Load a super fast batch
|
this.notes.forEach( (note, index) => {
|
||||||
this.search(true, this.firstLoadBatchSize, 0)
|
if(note.id == noteId){
|
||||||
.then( () => {
|
if(note.pinned == 1){
|
||||||
//Load a larger batch once first batch has loaded
|
this.containsPinnednotes--
|
||||||
this.search(false, this.batchSize, true).then( () => {
|
} 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() {
|
||||||
|
//Loads initial batch and tags
|
||||||
|
this.reset()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showOneColumn(){
|
showOneColumn(){
|
||||||
@ -244,7 +266,13 @@
|
|||||||
return (this.activeNoteId1 != null || this.activeNoteId2 != null) &&
|
return (this.activeNoteId1 != null || this.activeNoteId2 != null) &&
|
||||||
!this.$store.getters.getIsUserOnMobile
|
!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
|
//Do not open same note twice
|
||||||
if(this.activeNoteId1 == id || this.activeNoteId2 == id){
|
if(this.activeNoteId1 == id || this.activeNoteId2 == id){
|
||||||
@ -287,12 +315,17 @@
|
|||||||
this.activeNoteId2 = null
|
this.activeNoteId2 = null
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$router.push('/notes')
|
//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.activeNote1Position = 0
|
||||||
this.activeNote2Position = 0
|
this.activeNote2Position = 0
|
||||||
|
|
||||||
this.search(false)
|
|
||||||
},
|
},
|
||||||
toggleTagFilter(tagId){
|
toggleTagFilter(tagId){
|
||||||
|
|
||||||
@ -302,7 +335,15 @@
|
|||||||
this.searchTags.push(tagId)
|
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){
|
onScroll(e){
|
||||||
|
|
||||||
@ -325,7 +366,7 @@
|
|||||||
//If greater than 80 of the way down the page, load the next batch
|
//If greater than 80 of the way down the page, load the next batch
|
||||||
if(percentageDown >= 80){
|
if(percentageDown >= 80){
|
||||||
|
|
||||||
console.log('loading batch')
|
console.log('loading next batch')
|
||||||
this.search(false, this.batchSize, true)
|
this.search(false, this.batchSize, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,14 +375,120 @@
|
|||||||
|
|
||||||
return
|
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){
|
search(showLoading = true, notesInNextLoad = null, mergeExisting = false){
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
//Don't double load note batches
|
||||||
if(this.loadingInProgress){
|
if(this.loadingInProgress){
|
||||||
return resolve()
|
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.limitSize
|
||||||
delete this.fastFilters.limitOffset
|
delete this.fastFilters.limitOffset
|
||||||
|
|
||||||
@ -375,39 +522,49 @@
|
|||||||
axios.post('/api/note/search', postData).
|
axios.post('/api/note/search', postData).
|
||||||
then(response => {
|
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
|
//Save the number of notes just loaded
|
||||||
this.batchOffset += response.data.notes.length
|
this.batchOffset += response.data.notes.length
|
||||||
|
|
||||||
//Mush the two new sets of data together
|
//Mush the two new sets of data together (set will be empty is reset is on)
|
||||||
this.commonTags = this.commonTags.concat(response.data.tags)
|
if(response.data.tags.length > 0){
|
||||||
this.notes = this.notes.concat(response.data.notes)
|
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
|
//Go through each note and see which section to display
|
||||||
|
let textResultsCount = 0
|
||||||
|
let pinnedResultsCount = 0
|
||||||
|
let normalNotesCount = 0
|
||||||
response.data.notes.forEach(note => {
|
response.data.notes.forEach(note => {
|
||||||
|
|
||||||
if(note.note_highlights.length > 0){
|
if(note.note_highlights.length > 0){
|
||||||
this.containsTextResults++
|
textResultsCount++
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if(note.pinned == 1){
|
if(note.pinned == 1){
|
||||||
this.containsPinnednotes++
|
pinnedResultsCount++
|
||||||
return
|
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.working = false
|
||||||
this.loadingInProgress = false
|
this.loadingInProgress = false
|
||||||
@ -420,7 +577,10 @@
|
|||||||
let vm = this
|
let vm = this
|
||||||
clearTimeout(vm.searchDebounce)
|
clearTimeout(vm.searchDebounce)
|
||||||
vm.searchDebounce = setTimeout(() => {
|
vm.searchDebounce = setTimeout(() => {
|
||||||
vm.search()
|
this.search(true, this.batchSize)
|
||||||
|
.then( () => {
|
||||||
|
return this.fetchUserTags()
|
||||||
|
})
|
||||||
}, 500)
|
}, 500)
|
||||||
},
|
},
|
||||||
ucWords(str){
|
ucWords(str){
|
||||||
@ -435,7 +595,28 @@
|
|||||||
this.searchTags = []
|
this.searchTags = []
|
||||||
this.fastFilters = {}
|
this.fastFilters = {}
|
||||||
this.$bus.$emit('reset_fast_filters')
|
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'
|
// import HelpPage from '@/pages/HelpPage'
|
||||||
const HelpPage = () => import('@/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 NotesPage from '@/pages/NotesPage'
|
||||||
import SharePage from '@/pages/SharePage'
|
|
||||||
import QuickPage from '@/pages/QuickPage'
|
import QuickPage from '@/pages/QuickPage'
|
||||||
import AttachmentsPage from '@/pages/AttachmentsPage'
|
import AttachmentsPage from '@/pages/AttachmentsPage'
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ export default new Vuex.Store({
|
|||||||
username: null,
|
username: null,
|
||||||
nightMode: false,
|
nightMode: false,
|
||||||
isUserOnMobile: false,
|
isUserOnMobile: false,
|
||||||
|
isNoteSettingsOpen: false, //Little note settings pane
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setLoginToken(state, userData){
|
setLoginToken(state, userData){
|
||||||
@ -77,7 +78,9 @@ export default new Vuex.Store({
|
|||||||
state.isUserOnMobile = true
|
state.isUserOnMobile = true
|
||||||
}
|
}
|
||||||
})(navigator.userAgent||navigator.vendor||window.opera, state);
|
})(navigator.userAgent||navigator.vendor||window.opera, state);
|
||||||
|
},
|
||||||
|
toggleNoteSettingsPane(state){
|
||||||
|
state.isNoteSettingsOpen = !state.isNoteSettingsOpen
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@ -96,6 +99,9 @@ export default new Vuex.Store({
|
|||||||
},
|
},
|
||||||
getIsUserOnMobile: state => {
|
getIsUserOnMobile: state => {
|
||||||
return state.isUserOnMobile
|
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 = {}
|
let ProcessText = module.exports = {}
|
||||||
|
|
||||||
ProcessText.removeHtml = (string) => {
|
ProcessText.removeHtml = (string) => {
|
||||||
|
|
||||||
|
if(string == undefined || string == null || string.length == 0){
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
return string
|
return string
|
||||||
.replace(/&[#A-Za-z0-9]+;/g,' ') //Rip out all HTML entities
|
.replace(/&[#A-Za-z0-9]+;/g,' ') //Rip out all HTML entities
|
||||||
.replace(/<[^>]+>/g, ' ') //Rip out all HTML tags
|
.replace(/<[^>]+>/g, ' ') //Rip out all HTML tags
|
||||||
.replace(/\s+/g, ' ') //Remove all whitespace
|
.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 => {
|
Attachment.urlForNote(userId, noteId).then(attachments => {
|
||||||
|
|
||||||
//Find all URLs in text
|
//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
|
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)
|
let allUrls = noteText.match(urlPattern)
|
||||||
|
|
||||||
|
@ -5,7 +5,10 @@ let Attachment = require('@models/Attachment')
|
|||||||
|
|
||||||
let ProcessText = require('@helpers/ProcessText')
|
let ProcessText = require('@helpers/ProcessText')
|
||||||
|
|
||||||
|
const DiffMatchPatch = require('@helpers/DiffMatchPatch')
|
||||||
|
|
||||||
var rp = require('request-promise');
|
var rp = require('request-promise');
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
let Note = module.exports = {}
|
let Note = module.exports = {}
|
||||||
|
|
||||||
@ -137,21 +140,96 @@ Note.update = (userId, noteId, noteText, color, pinned, archived) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Delete a note and all its remaining parts
|
||||||
|
//
|
||||||
Note.delete = (userId, noteId) => {
|
Note.delete = (userId, noteId) => {
|
||||||
return new Promise((resolve, reject) => {
|
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 note WHERE note.id = ? AND note.user_id = ?', [noteId,userId])
|
||||||
db.promise().query('DELETE FROM attachment WHERE attachment.note_id = ? AND attachment.user_id = ?', [noteId,userId])
|
.then((rows, fields) => {
|
||||||
.then((rows, fields)=> {
|
return db.promise().query('DELETE FROM note_text_index WHERE note_text_index.note_id = ? AND note_text_index.user_id = ?', [noteId,userId])
|
||||||
db.promise().query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
|
})
|
||||||
.then((rows, fields)=> {
|
.then((rows, fields) => {
|
||||||
resolve(true)
|
//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) => {
|
Note.get = (userId, noteId) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.promise()
|
db.promise()
|
||||||
@ -184,6 +262,7 @@ Note.getShared = (noteId) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Searches text index, returns nothing if there is no search query
|
||||||
Note.solrQuery = (userId, searchQuery, searchTags) => {
|
Note.solrQuery = (userId, searchQuery, searchTags) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
@ -243,27 +322,28 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
|
|||||||
|
|
||||||
Note.solrQuery(userId, searchQuery, searchTags).then( (textSearchResults) => {
|
Note.solrQuery(userId, searchQuery, searchTags).then( (textSearchResults) => {
|
||||||
|
|
||||||
|
//Pull out search results from previous query
|
||||||
let textSearchIds = []
|
let textSearchIds = []
|
||||||
let highlights = {}
|
let highlights = {}
|
||||||
|
let returnTagResults = false
|
||||||
|
|
||||||
if(textSearchResults != null){
|
if(textSearchResults != null){
|
||||||
textSearchIds = textSearchResults['ids']
|
textSearchIds = textSearchResults['ids']
|
||||||
highlights = textSearchResults['snippets']
|
highlights = textSearchResults['snippets']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//No results, return empty data
|
//No results, return empty data
|
||||||
if(textSearchIds.length == 0 && searchQuery.length > 0){
|
if(textSearchIds.length == 0 && searchQuery.length > 0){
|
||||||
return resolve(returnData)
|
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
|
// Add to query for character counts -> CHAR_LENGTH(note.text) as chars
|
||||||
let noteSearchQuery = `
|
let noteSearchQuery = `
|
||||||
SELECT note.id,
|
SELECT note.id,
|
||||||
SUBSTRING(note.text, 1, 400) as text,
|
SUBSTRING(note.text, 1, 1500) as text,
|
||||||
updated, color,
|
updated,
|
||||||
|
color,
|
||||||
count(distinct note_tag.id) as tag_count,
|
count(distinct note_tag.id) as tag_count,
|
||||||
count(distinct attachment.id) as attachment_count,
|
count(distinct attachment.id) as attachment_count,
|
||||||
note.pinned,
|
note.pinned,
|
||||||
@ -274,22 +354,26 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
|
|||||||
WHERE note.user_id = ?`
|
WHERE note.user_id = ?`
|
||||||
let searchParams = [userId]
|
let searchParams = [userId]
|
||||||
|
|
||||||
|
//If text search returned results, limit search to those ids
|
||||||
if(textSearchIds.length > 0){
|
if(textSearchIds.length > 0){
|
||||||
searchParams.push(textSearchIds)
|
searchParams.push(textSearchIds)
|
||||||
noteSearchQuery += ' AND note.id IN (?)'
|
noteSearchQuery += ' AND note.id IN (?)'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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){
|
if(searchTags.length > 0){
|
||||||
//If tags are passed, use those tags in search
|
|
||||||
searchParams.push(searchTags)
|
searchParams.push(searchTags)
|
||||||
noteSearchQuery += ' AND note_tag.tag_id IN (?)'
|
noteSearchQuery += ' AND note_tag.tag_id IN (?)'
|
||||||
}
|
}
|
||||||
|
|
||||||
//Toggle archived, show archived if tags are searched
|
//Show archived notes, only if fast filter is set, default to not archived
|
||||||
// - archived will show archived in search results
|
if(fastFilters.onlyArchived == 1){
|
||||||
// - onlyArchive will exclude notes that are not archived
|
noteSearchQuery += ' AND note.archived = 1' //Show Archived
|
||||||
if(fastFilters.archived == 1 || searchTags.length > 0 || fastFilters.onlyArchived == 1){
|
|
||||||
//Do nothing
|
|
||||||
} else {
|
} else {
|
||||||
noteSearchQuery += ' AND note.archived = 0' //Exclude archived
|
noteSearchQuery += ' AND note.archived = 0' //Exclude archived
|
||||||
}
|
}
|
||||||
@ -299,14 +383,17 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
|
|||||||
|
|
||||||
//Only show notes with Tags
|
//Only show notes with Tags
|
||||||
if(fastFilters.withTags == 1){
|
if(fastFilters.withTags == 1){
|
||||||
|
returnTagResults = true
|
||||||
noteSearchQuery += ' HAVING tag_count > 0'
|
noteSearchQuery += ' HAVING tag_count > 0'
|
||||||
}
|
}
|
||||||
//Only show notes with links
|
//Only show notes with links
|
||||||
if(fastFilters.withLinks == 1){
|
if(fastFilters.withLinks == 1){
|
||||||
|
returnTagResults = true
|
||||||
noteSearchQuery += ' HAVING attachment_count > 0'
|
noteSearchQuery += ' HAVING attachment_count > 0'
|
||||||
}
|
}
|
||||||
//Only show archived notes
|
//Only show archived notes
|
||||||
if(fastFilters.onlyArchived == 1){
|
if(fastFilters.onlyArchived == 1){
|
||||||
|
returnTagResults = true
|
||||||
noteSearchQuery += ' HAVING note.archived = 1'
|
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
|
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}`
|
noteSearchQuery += ` LIMIT ${limitOffset}, ${limitSize}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log('------------- Final Query --------------')
|
||||||
|
// console.log(noteSearchQuery)
|
||||||
|
// console.log('------------- ----------- --------------')
|
||||||
|
|
||||||
db.promise()
|
db.promise()
|
||||||
.query(noteSearchQuery, searchParams)
|
.query(noteSearchQuery, searchParams)
|
||||||
@ -356,38 +446,27 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
|
|||||||
noteIds.push(note.id)
|
noteIds.push(note.id)
|
||||||
|
|
||||||
if(note.text == null){ note.text = '' }
|
if(note.text == null){ note.text = '' }
|
||||||
|
|
||||||
|
//Deduce note title
|
||||||
|
const textData = ProcessText.deduceNoteTitle(note.text)
|
||||||
|
|
||||||
|
// console.log(textData)
|
||||||
|
|
||||||
//Attempt to pull string out of first tag in note
|
note.title = textData.title
|
||||||
let reg = note.text.match(/<([\w]+)[^>]*>(.*?)<\/\1>/g)
|
note.subtext = textData.sub
|
||||||
|
note.titleLength = textData.titleLength
|
||||||
//Pull out first html tag contents, that is the title
|
note.subtextLength = textData.subtextLength
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
note.note_highlights = []
|
note.note_highlights = []
|
||||||
note.attachment_highlights = []
|
note.attachment_highlights = []
|
||||||
note.tag_highlights = []
|
note.tag_highlights = []
|
||||||
|
|
||||||
//Push in solr highlights
|
//Push in search highlights
|
||||||
if(highlights && highlights[note.id]){
|
if(highlights && highlights[note.id]){
|
||||||
note['note_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
|
delete note.text
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -396,6 +475,13 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
|
|||||||
return resolve(returnData)
|
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
|
//Only show tags of selected notes
|
||||||
db.promise()
|
db.promise()
|
||||||
.query(`SELECT tag.id, tag.text, count(tag.id) as usages FROM note_tag
|
.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 = {}
|
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) => {
|
Tag.removeTagFromNote = (userId, tagId) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
let express = require('express')
|
let express = require('express')
|
||||||
|
|
||||||
var multer = require('multer')
|
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 router = express.Router()
|
||||||
|
|
||||||
let Attachment = require('@models/Attachment');
|
let Attachment = require('@models/Attachment');
|
||||||
|
@ -38,6 +38,15 @@ router.post('/search', function (req, res) {
|
|||||||
.then( notesAndTags => res.send(notesAndTags))
|
.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
|
//Reindex all notes. Not a very good function, not public
|
||||||
router.get('/reindex5yu43prchuj903mrc', function (req, res) {
|
router.get('/reindex5yu43prchuj903mrc', function (req, res) {
|
||||||
|
|
||||||
|
@ -42,4 +42,10 @@ router.post('/get', function (req, res) {
|
|||||||
.then( data => res.send(data) )
|
.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
|
module.exports = router
|
Loading…
Reference in New Issue
Block a user