Merge branch 'stable'

# Conflicts:
#	package-lock.json
#	src/public/javascripts/services/tree_context_menu.js
#	src/services/import/enex.js
This commit is contained in:
zadam
2019-11-27 18:54:49 +01:00
15 changed files with 3995 additions and 149 deletions

View File

@@ -818,6 +818,7 @@ class Note extends Entity {
delete pojo.isContentAvailable;
delete pojo.__attributeCache;
delete pojo.content;
/** zero references to contentHash, probably can be removed */
delete pojo.contentHash;
}
}

View File

@@ -93,7 +93,7 @@ export default class SidebarOptions {
this.$sidebarMinWidth.val(options.sidebarMinWidth);
this.$sidebarWidthPercent.val(options.sidebarWidthPercent);
if (parseInt(options.showSidebarInNewTab)) {
if (options.showSidebarInNewTab === 'true') {
this.$showSidebarInNewTab.attr("checked", "checked");
}
else {

View File

@@ -274,7 +274,9 @@ async function filterTabs(noteId) {
async function noteDeleted(noteId) {
for (const tc of tabContexts) {
if (tc.notePath && tc.notePath.split("/").includes(noteId)) {
// not removing active even if it contains deleted note since that one will move to another note (handled by deletion logic)
// and we would lose tab context state (e.g. sidebar visibility)
if (!tc.isActive() && tc.notePath && tc.notePath.split("/").includes(noteId)) {
await tabRow.removeTab(tc.$tab[0]);
}
}

View File

@@ -246,11 +246,15 @@ class TabContext {
}
setCurrentNotePathToHash() {
if (this.$tab[0] === this.tabRow.activeTabEl) {
if (this.isActive()) {
document.location.hash = (this.notePath || "") + "-" + this.tabId;
}
}
isActive() {
return this.$tab[0] === this.tabRow.activeTabEl;
}
setupClasses() {
for (const clazz of Array.from(this.$tab[0].classList)) { // create copy to safely iterate over while removing classes
if (clazz !== 'note-tab') {

View File

@@ -42,7 +42,7 @@ class TreeContextMenu {
|| (selNodes.length === 1 && selNodes[0] === this.node);
const notSearch = note.type !== 'search';
const parentNotSearch = parentNote.type !== 'search';
const parentNotSearch = !parentNote || parentNote.type !== 'search';
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
return [
@@ -78,8 +78,8 @@ class TreeContextMenu {
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
{ title: 'Paste after', cmd: "pasteAfter", uiIcon: "paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
{ title: 'Duplicate note here', cmd: "duplicateNote", uiIcon: "empty",
enabled: noSelectedNotes && parentNotSearch && (!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) },
{ title: "Duplicate note here", cmd: "duplicateNote", uiIcon: "empty",
enabled: noSelectedNotes && parentNotSearch && isNotRoot && !isHoisted && (!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) },
{ title: "----" },
{ title: "Export", cmd: "export", uiIcon: "empty",
enabled: notSearch && noSelectedNotes },

View File

@@ -127,11 +127,13 @@ async function consumeSyncData() {
}
function connectWebSocket() {
const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
const loc = window.location;
const webSocketUri = (loc.protocol === "https:" ? "wss:" : "ws:")
+ "//" + loc.host + loc.pathname;
// use wss for secure messaging
const ws = new WebSocket(protocol + "://" + location.host);
ws.onopen = () => console.debug(utils.now(), "Connected to server with WebSocket");
const ws = new WebSocket(webSocketUri);
ws.onopen = () => console.debug(utils.now(), `Connected to server ${webSocketUri} with WebSocket`);
ws.onmessage = handleMessage;
// we're not handling ws.onclose here because reconnection is done in sendPing()

View File

@@ -97,6 +97,11 @@ body {
font-size: inherit;
}
#context-menu-container {
max-height: 100vh;
overflow: auto; /* make it scrollable when exceeding total height of the window */
}
#context-menu-container, #context-menu-container .dropdown-menu {
padding: 3px 0 0;
z-index: 1111;

View File

@@ -1 +1 @@
module.exports = { buildDate:"2019-11-22T22:38:03+01:00", buildRevision: "7a2c7edd7e9b975bf64f732629e711379baecf48" };
module.exports = { buildDate:"2019-11-26T22:50:08+01:00", buildRevision: "5193f073e9e55f5440fe2e71fbd2cdfcdb2d2c6b" };

View File

@@ -3,6 +3,7 @@ const fileType = require('file-type');
const stream = require('stream');
const log = require("../log");
const utils = require("../utils");
const sql = require("../sql");
const noteService = require("../notes");
const imageService = require("../image");
const protectedSessionService = require('../protected_session');
@@ -11,7 +12,7 @@ const protectedSessionService = require('../protected_session');
function parseDate(text) {
// insert - and : to make it ISO format
text = text.substr(0, 4) + "-" + text.substr(4, 2) + "-" + text.substr(6, 2)
+ "T" + text.substr(9, 2) + ":" + text.substr(11, 2) + ":" + text.substr(13, 2) + "Z";
+ " " + text.substr(9, 2) + ":" + text.substr(11, 2) + ":" + text.substr(13, 2) + ".000Z";
return text;
}
@@ -153,7 +154,7 @@ async function importEnex(taskContext, file, parentNote) {
} else if (currentTag === 'created') {
note.utcDateCreated = parseDate(text);
} else if (currentTag === 'updated') {
// updated is currently ignored since utcDateModified is updated automatically with each save
note.utcDateModified = parseDate(text);
} else if (currentTag === 'tag') {
note.attributes.push({
type: 'label',
@@ -190,9 +191,27 @@ async function importEnex(taskContext, file, parentNote) {
}
});
async function updateDates(noteId, utcDateCreated, utcDateModified) {
// it's difficult to force custom dateCreated and dateModified to Note entity so we do it post-creation with SQL
await sql.execute(`
UPDATE notes
SET dateCreated = ?,
utcDateCreated = ?,
dateModified = ?,
utcDateModified = ?
WHERE noteId = ?`,
[utcDateCreated, utcDateCreated, utcDateModified, utcDateModified, noteId]);
await sql.execute(`
UPDATE note_contents
SET utcDateModified = ?
WHERE noteId = ?`,
[utcDateModified, noteId]);
}
async function saveNote() {
// make a copy because stream continues with the next async call and note gets overwritten
let {title, content, attributes, resources, utcDateCreated} = note;
let {title, content, attributes, resources, utcDateCreated, utcDateModified} = note;
content = extractContent(content);
@@ -210,6 +229,10 @@ async function importEnex(taskContext, file, parentNote) {
await noteEntity.addAttribute(attr.type, attr.name, attr.value);
}
utcDateCreated = utcDateCreated || noteEntity.utcDateCreated;
// sometime date modified is not present in ENEX, then use date created
utcDateModified = utcDateModified || utcDateCreated;
taskContext.increaseProgressCount();
let noteContent = await noteEntity.getContent();
@@ -239,6 +262,8 @@ async function importEnex(taskContext, file, parentNote) {
await noteEntity.addAttribute(attr.type, attr.name, attr.value);
}
await updateDates(resourceNote.noteId, utcDateCreated, utcDateModified);
taskContext.increaseProgressCount();
const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
@@ -250,7 +275,9 @@ async function importEnex(taskContext, file, parentNote) {
try {
const originalName = "image." + resource.mime.substr(6);
const {url} = await imageService.saveImage(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages);
const {url, note: imageNote} = await imageService.saveImage(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages);
await updateDates(imageNote.noteId, utcDateCreated, utcDateModified);
const imageLink = `<img src="${url}">`;
@@ -272,6 +299,10 @@ async function importEnex(taskContext, file, parentNote) {
// save updated content with links to files/images
await noteEntity.setContent(noteContent);
await noteService.scanForLinks(noteEntity.noteId);
await updateDates(noteEntity.noteId, utcDateCreated, utcDateModified);
}
saxStream.on("closetag", async tag => {

View File

@@ -37,7 +37,7 @@ function isProtectedSessionAvailable() {
function decryptNotes(notes) {
for (const note of notes) {
if (note.isProtected) {
note.title = decrypt(note.title);
note.title = decryptString(note.title);
}
}
}