Merge remote-tracking branch 'origin/master' into m43

This commit is contained in:
zadam
2020-05-12 21:15:54 +02:00
32 changed files with 205 additions and 124 deletions

View File

@@ -105,7 +105,6 @@ class Attribute extends Entity {
// cannot be static!
updatePojo(pojo) {
delete pojo.isOwned;
delete pojo.__note;
}
@@ -124,4 +123,4 @@ class Attribute extends Entity {
}
}
module.exports = Attribute;
module.exports = Attribute;

View File

@@ -411,10 +411,6 @@ class Note extends Entity {
}
});
for (const attr of filteredAttributes) {
attr.isOwned = attr.noteId === this.noteId;
}
this.__attributeCache = filteredAttributes;
}
@@ -946,4 +942,4 @@ class Note extends Entity {
}
}
module.exports = Note;
module.exports = Note;

View File

@@ -8,6 +8,7 @@ import contextMenu from "./services/context_menu.js";
import DesktopMainWindowLayout from "./layouts/desktop_main_window_layout.js";
import glob from "./services/glob.js";
import DesktopExtraWindowLayout from "./layouts/desktop_extra_window_layout.js";
import zoomService from './services/zoom.js';
glob.setupGlobs();
@@ -133,9 +134,11 @@ if (utils.isElectron()) {
return;
}
const zoomLevel = zoomService.getCurrentZoom();
contextMenu.show({
x: params.x,
y: params.y,
x: params.x / zoomLevel,
y: params.y / zoomLevel,
items,
selectMenuItemHandler: ({command, spellingSuggestion}) => {
if (command === 'replaceMisspelling') {
@@ -144,4 +147,4 @@ if (utils.isElectron()) {
}
});
});
}
}

View File

@@ -59,8 +59,8 @@ function AttributesModel() {
});
};
async function showAttributes(attributes) {
const ownedAttributes = attributes.filter(attr => attr.isOwned);
async function showAttributes(noteId, attributes) {
const ownedAttributes = attributes.filter(attr => attr.noteId === noteId);
for (const attr of ownedAttributes) {
attr.labelValue = attr.type === 'label' ? attr.value : '';
@@ -86,7 +86,7 @@ function AttributesModel() {
addLastEmptyRow();
const inheritedAttributes = attributes.filter(attr => !attr.isOwned);
const inheritedAttributes = attributes.filter(attr => attr.noteId !== noteId);
self.inheritedAttributes(inheritedAttributes);
}
@@ -96,7 +96,7 @@ function AttributesModel() {
const attributes = await server.get('notes/' + noteId + '/attributes');
await showAttributes(attributes);
await showAttributes(noteId, attributes);
// attribute might not be rendered immediatelly so could not focus
setTimeout(() => $(".attribute-type-select:last").trigger('focus'), 1000);
@@ -166,7 +166,7 @@ function AttributesModel() {
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
await showAttributes(attributes);
await showAttributes(noteId, attributes);
toastService.showMessage("Attributes have been saved.");
};
@@ -311,4 +311,4 @@ $dialog.on('focus', '.label-value', function (e) {
$el: $(this),
open: true
})
});
});

View File

@@ -24,7 +24,6 @@ import NoteRevisionsWidget from "../widgets/collapsible_widgets/note_revisions.j
import SimilarNotesWidget from "../widgets/collapsible_widgets/similar_notes.js";
import WhatLinksHereWidget from "../widgets/collapsible_widgets/what_links_here.js";
import SidePaneToggles from "../widgets/side_pane_toggles.js";
import appContext from "../services/app_context.js";
const RIGHT_PANE_CSS = `
<style>
@@ -117,6 +116,7 @@ export default class DesktopMainWindowLayout {
.hideInZenMode())
.child(new FlexContainer('row')
.collapsible()
.filling()
.child(new SidePaneContainer('left')
.hideInZenMode()
.child(new GlobalButtonsWidget())
@@ -153,4 +153,4 @@ export default class DesktopMainWindowLayout {
.child(new SidePaneToggles().hideInZenMode())
);
}
}
}

View File

@@ -4,7 +4,7 @@ import DialogCommandExecutor from "./dialog_command_executor.js";
import Entrypoints from "./entrypoints.js";
import options from "./options.js";
import utils from "./utils.js";
import ZoomService from "./zoom.js";
import zoomService from "./zoom.js";
import TabManager from "./tab_manager.js";
import treeService from "./tree.js";
import Component from "../widgets/component.js";
@@ -73,7 +73,7 @@ class AppContext extends Component {
}
if (utils.isElectron()) {
this.child(new ZoomService());
this.child(zoomService);
}
this.triggerEvent('initialRenderComplete');
@@ -134,4 +134,4 @@ $(window).on('hashchange', function() {
}
});
export default appContext;
export default appContext;

View File

@@ -81,24 +81,29 @@ function goToLink(e) {
}
else if (e.which === 1) {
const activeTabContext = appContext.tabManager.getActiveTabContext();
activeTabContext.setNote(notePath)
activeTabContext.setNote(notePath);
}
else {
return false;
}
}
else {
const address = $link.attr('href');
if (e.which === 1) {
const address = $link.attr('href');
if (address && address.startsWith('http')) {
window.open(address, '_blank');
if (address && address.startsWith('http')) {
window.open(address, '_blank');
}
}
else {
return false;
}
}
return true;
}
function newTabContextMenu(e) {
function linkContextMenu(e) {
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
@@ -113,7 +118,7 @@ function newTabContextMenu(e) {
x: e.pageX,
y: e.pageY,
items: [
{title: "Open note in new tab", command: "openNoteInNewTab", uiIcon: "arrow-up-right"},
{title: "Open note in new tab", command: "openNoteInNewTab", uiIcon: "empty"},
{title: "Open note in new window", command: "openNoteInNewWindow", uiIcon: "window-open"}
],
selectMenuItemHandler: ({command}) => {
@@ -155,21 +160,23 @@ $(document).on('mousedown', '.note-detail-text a', function (e) {
$(document).on('mousedown', '.note-detail-book a', goToLink);
$(document).on('mousedown', '.note-detail-render a', goToLink);
$(document).on('mousedown', '.note-detail-text.ck-read-only a,.note-detail-text a.reference-link', goToLink);
$(document).on('mousedown', '.note-detail-text a.reference-link', goToLink);
$(document).on('mousedown', '.note-detail-readonly-text a', goToLink);
$(document).on('mousedown', 'a.ck-link-actions__preview', goToLink);
$(document).on('click', 'a.ck-link-actions__preview', e => {
e.preventDefault();
e.stopPropagation();
});
$(document).on('contextmenu', 'a.ck-link-actions__preview', newTabContextMenu);
$(document).on('contextmenu', '.note-detail-text a', newTabContextMenu);
$(document).on('contextmenu', "a[data-action='note']", newTabContextMenu);
$(document).on('contextmenu', ".note-detail-render a", newTabContextMenu);
$(document).on('contextmenu', ".note-paths-widget a", newTabContextMenu);
$(document).on('contextmenu', 'a.ck-link-actions__preview', linkContextMenu);
$(document).on('contextmenu', '.note-detail-text a', linkContextMenu);
$(document).on('contextmenu', '.note-detail-readonly-text a', linkContextMenu);
$(document).on('contextmenu', "a[data-action='note']", linkContextMenu);
$(document).on('contextmenu', ".note-detail-render a", linkContextMenu);
$(document).on('contextmenu', ".note-paths-widget a", linkContextMenu);
export default {
getNotePathFromUrl,
createNoteLink,
goToLink
};
};

View File

@@ -37,6 +37,10 @@ function subscribeToMessages(messageHandler) {
// used to serialize sync operations
let consumeQueuePromise = null;
// most sync events are sent twice - once immediatelly after finishing the transaction and once during the scheduled ping
// but we want to process only once
const receivedSyncIds = new Set();
async function handleMessage(event) {
const message = JSON.parse(event.data);
@@ -52,14 +56,19 @@ async function handleMessage(event) {
if (syncRows.length > 0) {
const filteredRows = syncRows.filter(row =>
row.entityName !== 'recent_notes'
!receivedSyncIds.has(row.id)
&& row.entityName !== 'recent_notes'
&& (row.entityName !== 'options' || row.entityId !== 'openTabs'));
if (filteredRows.length > 0) {
console.debug(utils.now(), "Sync data: ", filteredRows);
}
syncDataQueue.push(...syncRows);
for (const row of filteredRows) {
receivedSyncIds.add(row.id);
}
syncDataQueue.push(...filteredRows);
// we set lastAcceptedSyncId even before sync processing and send ping so that backend can start sending more updates
lastAcceptedSyncId = Math.max(lastAcceptedSyncId, syncRows[syncRows.length - 1].id);
@@ -170,7 +179,7 @@ function connectWebSocket() {
async function sendPing() {
if (Date.now() - lastPingTs > 30000) {
console.log(utils.now(), "Lost websocket connection to the backend");
console.log(utils.now(), "Lost websocket connection to the backend. If you keep having this issue repeatedly, you might want to check your reverse proxy (nginx, apache) configuration and allow/unblock WebSocket.");
}
if (ws.readyState === ws.OPEN) {
@@ -374,4 +383,4 @@ export default {
subscribeToMessages,
waitForSyncId,
waitForMaxKnownSyncId
};
};

View File

@@ -5,31 +5,33 @@ import utils from "../services/utils.js";
const MIN_ZOOM = 0.5;
const MAX_ZOOM = 2.0;
export default class ZoomService extends Component {
class ZoomService extends Component {
constructor() {
super();
this.setZoomFactor(options.getFloat('zoomFactor'));
options.initializedPromise.then(() => {
this.setZoomFactor(options.getFloat('zoomFactor'));
});
}
setZoomFactor(zoomFactor) {
zoomFactor = parseFloat(zoomFactor);
const webFrame = utils.dynamicRequire('electron').webFrame;
webFrame.setZoomFactor(zoomFactor);
}
async setZoomFactorAndSave(zoomFactor) {
if (zoomFactor >= MIN_ZOOM && zoomFactor <= MAX_ZOOM) {
this.setZoomFactor(zoomFactor);
await options.save('zoomFactor', zoomFactor);
}
else {
console.log(`Zoom factor ${zoomFactor} outside of the range, ignored.`);
}
}
getCurrentZoom() {
return utils.dynamicRequire('electron').webFrame.getZoomFactor();
}
@@ -45,4 +47,8 @@ export default class ZoomService extends Component {
setZoomFactorAndSaveEvent({zoomFactor}) {
this.setZoomFactorAndSave(zoomFactor);
}
}
}
const zoomService = new ZoomService();
export default zoomService;

View File

@@ -30,6 +30,11 @@ class BasicWidget extends Component {
return this;
}
filling() {
this.css('flex-grow', '1');
return this;
}
hideInZenMode() {
this.class('hide-in-zen-mode');
return this;
@@ -109,4 +114,4 @@ class BasicWidget extends Component {
cleanup() {}
}
export default BasicWidget;
export default BasicWidget;

View File

@@ -251,8 +251,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.triggerCommand('setActiveScreen', {screen:'detail'});
}
},
expand: (event, data) => this.setExpandedToServer(data.node.data.branchId, true),
collapse: (event, data) => this.setExpandedToServer(data.node.data.branchId, false),
expand: (event, data) => this.setExpanded(data.node.data.branchId, true),
collapse: (event, data) => this.setExpanded(data.node.data.branchId, false),
hotkeys: utils.isMobile() ? undefined : { keydown: await this.getHotKeys() },
dnd5: {
autoExpandMS: 600,
@@ -807,7 +807,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
async entitiesReloadedEvent({loadResults}) {
const activeNode = this.getActiveNode();
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
const activeNoteId = activeNode ? activeNode.data.noteId : null;
const noteIdsToUpdate = new Set();
@@ -929,15 +931,27 @@ export default class NoteTreeWidget extends TabAwareWidget {
if (node) {
node.setActive(true, {noEvents: true});
}
else {
// this is used when original note has been deleted and we want to move the focus to the note above/below
node = await this.expandToNote(nextNotePath);
if (node) {
this.tree.setFocus();
node.setFocus(true);
await appContext.tabManager.getActiveTabContext().setNote(nextNotePath);
}
}
}
}
async setExpandedToServer(branchId, isExpanded) {
async setExpanded(branchId, isExpanded) {
utils.assertArguments(branchId);
const expandedNum = isExpanded ? 1 : 0;
const branch = treeCache.getBranch(branchId);
branch.isExpanded = isExpanded;
await server.put('branches/' + branchId + '/expanded/' + expandedNum);
await server.put(`branches/${branchId}/expanded/${isExpanded ? 1 : 0}`);
}
async reloadTreeFromCache() {
@@ -997,7 +1011,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
return false;
}
};
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts) {
hotKeyMap[utils.normalizeShortcut(shortcut)] = node => {
@@ -1022,83 +1036,83 @@ export default class NoteTreeWidget extends TabAwareWidget {
async deleteNotesCommand({node}) {
const branchIds = this.getSelectedOrActiveBranchIds(node);
await branchService.deleteNotes(branchIds);
this.clearSelectedNodes();
}
moveNoteUpCommand({node}) {
const beforeNode = node.getPrevSibling();
if (beforeNode !== null) {
branchService.moveBeforeBranch([node.data.branchId], beforeNode.data.branchId);
}
}
moveNoteDownCommand({node}) {
const afterNode = node.getNextSibling();
if (afterNode !== null) {
branchService.moveAfterBranch([node.data.branchId], afterNode.data.branchId);
}
}
moveNoteUpInHierarchyCommand({node}) {
branchService.moveNodeUpInHierarchy(node);
}
moveNoteDownInHierarchyCommand({node}) {
const toNode = node.getPrevSibling();
if (toNode !== null) {
branchService.moveToParentNote([node.data.branchId], toNode.data.noteId);
}
}
addNoteAboveToSelectionCommand() {
const node = this.getFocusedNode();
if (!node) {
return;
}
if (node.isActive()) {
node.setSelected(true);
}
const prevSibling = node.getPrevSibling();
if (prevSibling) {
prevSibling.setActive(true, {noEvents: true});
if (prevSibling.isSelected()) {
node.setSelected(false);
}
prevSibling.setSelected(true);
}
}
addNoteBelowToSelectionCommand() {
const node = this.getFocusedNode();
if (!node) {
return;
}
if (node.isActive()) {
node.setSelected(true);
}
const nextSibling = node.getNextSibling();
if (nextSibling) {
nextSibling.setActive(true, {noEvents: true});
if (nextSibling.isSelected()) {
node.setSelected(false);
}
nextSibling.setSelected(true);
}
}
@@ -1182,4 +1196,4 @@ export default class NoteTreeWidget extends TabAwareWidget {
noteCreateService.duplicateNote(node.data.noteId, branch.parentNoteId);
}
}
}

View File

@@ -19,6 +19,7 @@ const TPL = `
.promoted-attributes td, .promoted-attributes th {
padding: 5px;
min-width: 50px; /* otherwise checkboxes can collapse into 0 width (if there are only checkboxes) */
}
</style>
@@ -98,7 +99,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
const $labelCell = $("<th>").append(valueAttr.name);
const $input = $("<input>")
.prop("tabindex", definitionAttr.position)
.prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
.prop("attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
.prop("attribute-type", valueAttr.type)
.prop("attribute-name", valueAttr.name)
.prop("value", valueAttr.value)
@@ -266,4 +267,4 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
$attr.prop("attribute-id", result.attributeId);
}
}
}

View File

@@ -602,18 +602,23 @@ export default class TabRowWidget extends BasicWidget {
}
updateTab($tab, note) {
if (!note || !$tab.length) {
if (!$tab.length) {
return;
}
this.updateTitle($tab, note.title);
for (const clazz of Array.from($tab[0].classList)) { // create copy to safely iterate over while removing classes
if (clazz !== 'note-tab') {
$tab.removeClass(clazz);
}
}
if (!note) {
this.updateTitle($tab, 'New tab');
return;
}
this.updateTitle($tab, note.title);
$tab.addClass(note.getCssClass());
$tab.addClass(utils.getNoteTypeClass(note.type));
$tab.addClass(utils.getMimeTypeClass(note.mime));
@@ -636,4 +641,4 @@ export default class TabRowWidget extends BasicWidget {
this.updateTab($tab, tabContext.note);
}
}
}
}

View File

@@ -22,6 +22,10 @@ const TPL = `
.note-detail-readonly-text p:first-child, .note-detail-text::before {
margin-top: 0;
}
.note-detail-readonly-text img {
max-width: 100%;
}
</style>
<div class="alert alert-warning no-print">
@@ -77,4 +81,4 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
this.loadIncludedNote(noteId, $(el));
});
}
}
}

View File

@@ -199,4 +199,4 @@ module.exports = {
getEffectiveNoteAttributes,
createRelation,
deleteRelation
};
};

View File

@@ -1,6 +1,5 @@
"use strict";
const noteService = require('../../services/notes');
const protectedSessionService = require('../../services/protected_session');
const repository = require('../../services/repository');
const utils = require('../../services/utils');
@@ -45,7 +44,9 @@ async function downloadNoteFile(noteId, res, contentDisposition = true) {
if (contentDisposition) {
// (one) reason we're not using the originFileName (available as label) is that it's not
// available for older note revisions and thus would be inconsistent
res.setHeader('Content-Disposition', utils.getContentDisposition(note.title || "untitled"));
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
}
res.setHeader('Content-Type', note.mime);
@@ -70,4 +71,4 @@ module.exports = {
openFile,
downloadFile,
downloadNoteFile
};
};

View File

@@ -38,13 +38,7 @@ async function getNoteRevision(req) {
* @return {string}
*/
function getRevisionFilename(noteRevision) {
let filename = noteRevision.title || "untitled";
if (noteRevision.type === 'text') {
filename += '.html';
} else if (['relation-map', 'search'].includes(noteRevision.type)) {
filename += '.json';
}
let filename = utils.formatDownloadTitle(noteRevision.title, noteRevision.type, noteRevision.mime);
const extension = path.extname(filename);
const date = noteRevision.dateCreated
@@ -158,4 +152,4 @@ module.exports = {
eraseAllNoteRevisions,
eraseNoteRevision,
restoreNoteRevision
};
};

View File

@@ -1 +1 @@
module.exports = { buildDate:"2020-05-06T23:24:13+02:00", buildRevision: "54ecd2ee75d1177cedadf9fee10319687feee5f0" };
module.exports = { buildDate:"2020-05-12T16:46:45+02:00", buildRevision: "4f50864ec8346a12d7845cb4c91a3de3b1043d34" };

View File

@@ -13,6 +13,7 @@ const Attribute = require('../entities/attribute');
const hoistedNoteService = require('../services/hoisted_note');
const protectedSessionService = require('../services/protected_session');
const log = require('../services/log');
const utils = require('../services/utils');
const noteRevisionService = require('../services/note_revisions');
const attributeService = require('../services/attributes');
const request = require('./request');
@@ -276,9 +277,9 @@ async function downloadImage(noteId, imageUrl) {
const downloadImagePromises = {};
function replaceUrl(content, url, imageNote) {
const quoted = url.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
const quotedUrl = utils.quoteRegex(url);
return content.replace(new RegExp(`\\s+src=[\"']${quoted}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`);
return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`);
}
async function downloadImages(noteId, content) {

View File

@@ -1,6 +1,6 @@
const dayjs = require("dayjs");
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}\p{Number}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([^\s=*]+|"[^"]+"))?/igu;
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}\p{Number}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([^\s=*"]+|"[^"]+"))?/igu;
const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i;
function calculateSmartValue(v) {

View File

@@ -221,6 +221,7 @@ async function transactional(func) {
await commit();
// note that sync rows sent from this action will be sent again by scheduled periodic ping
require('./ws.js').sendPingToAllClients();
transactionActive = false;
@@ -267,4 +268,4 @@ module.exports = {
executeScript,
transactional,
upsert
};
};

View File

@@ -5,6 +5,7 @@ const randtoken = require('rand-token').generator({source: 'crypto'});
const unescape = require('unescape');
const escape = require('escape-html');
const sanitize = require("sanitize-filename");
const mimeTypes = require('mime-types');
function newEntityId() {
return randomString(12);
@@ -166,10 +167,46 @@ function isStringNote(type, mime) {
|| STRING_MIME_TYPES.includes(mime);
}
function replaceAll(string, replaceWhat, replaceWith) {
const escapedWhat = replaceWhat.replace(/([\/,!\\^${}\[\]().*+?|<>\-&])/g, "\\$&");
function quoteRegex(url) {
return url.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}
return string.replace(new RegExp(escapedWhat, "g"), replaceWith);
function replaceAll(string, replaceWhat, replaceWith) {
const quotedReplaceWhat = quoteRegex(replaceWhat);
return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith);
}
function formatDownloadTitle(filename, type, mime) {
if (!filename) {
filename = "untitled";
}
if (type === 'text') {
return filename + '.html';
} else if (['relation-map', 'search'].includes(type)) {
return filename + '.json';
} else {
if (!mime) {
return filename;
}
mime = mime.toLowerCase();
const filenameLc = filename.toLowerCase();
const extensions = mimeTypes.extensions[mime];
if (!extensions || extensions.length === 0) {
return filename;
}
for (const ext of extensions) {
if (filenameLc.endsWith('.' + ext)) {
return filename;
}
}
return filename + '.' + extensions[0];
}
}
module.exports = {
@@ -198,5 +235,7 @@ module.exports = {
sanitizeFilenameForHeader,
getContentDisposition,
isStringNote,
replaceAll
};
quoteRegex,
replaceAll,
formatDownloadTitle
};

View File

@@ -5,7 +5,7 @@
<link rel="shortcut icon" href="favicon.ico">
<title>Trilium Notes</title>
</head>
<body class="desktop theme-<%= theme %>" style="display: none; --main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;">
<body class="desktop theme-<%= theme %>" style="--main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;">
<noscript>Trilium requires JavaScript to be enabled.</noscript>
<div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div>
@@ -82,9 +82,5 @@
<link rel="stylesheet" type="text/css" href="libraries/boxicons/css/boxicons.min.css">
<script>
$("body").show();
</script>
</body>
</html>