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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
libraries/jquery.min.map Normal file

File diff suppressed because one or more lines are too long

6
package-lock.json generated
View File

@ -3345,9 +3345,9 @@
} }
}, },
"electron": { "electron": {
"version": "9.0.0-beta.22", "version": "9.0.0-beta.24",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0-beta.22.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0-beta.24.tgz",
"integrity": "sha512-dfqAf+CXXTKcNDj7DU7mYsmx+oZQcXOvJnZ8ZsgAHjrE9Tv8zsYUgCP3JlO4Z8CIazgleKXYmgh6H2stdK7fEA==", "integrity": "sha512-25L3XMqm/1CCaV5CgU5ZkhKXw9830WeipJrTW0+VC5XTKp/3xHwhxyQ5G1kQnOTJd7IGwOamvw237D6e1YKnng==",
"dev": true, "dev": true,
"requires": { "requires": {
"@electron/get": "^1.0.1", "@electron/get": "^1.0.1",

View File

@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.42.1", "version": "0.42.2",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@ -78,7 +78,7 @@
"yazl": "^2.5.1" "yazl": "^2.5.1"
}, },
"devDependencies": { "devDependencies": {
"electron": "9.0.0-beta.22", "electron": "9.0.0-beta.24",
"electron-builder": "22.6.0", "electron-builder": "22.6.0",
"electron-packager": "14.2.1", "electron-packager": "14.2.1",
"electron-rebuild": "1.10.1", "electron-rebuild": "1.10.1",

View File

@ -105,7 +105,6 @@ class Attribute extends Entity {
// cannot be static! // cannot be static!
updatePojo(pojo) { updatePojo(pojo) {
delete pojo.isOwned;
delete pojo.__note; 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; 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 DesktopMainWindowLayout from "./layouts/desktop_main_window_layout.js";
import glob from "./services/glob.js"; import glob from "./services/glob.js";
import DesktopExtraWindowLayout from "./layouts/desktop_extra_window_layout.js"; import DesktopExtraWindowLayout from "./layouts/desktop_extra_window_layout.js";
import zoomService from './services/zoom.js';
glob.setupGlobs(); glob.setupGlobs();
@ -133,9 +134,11 @@ if (utils.isElectron()) {
return; return;
} }
const zoomLevel = zoomService.getCurrentZoom();
contextMenu.show({ contextMenu.show({
x: params.x, x: params.x / zoomLevel,
y: params.y, y: params.y / zoomLevel,
items, items,
selectMenuItemHandler: ({command, spellingSuggestion}) => { selectMenuItemHandler: ({command, spellingSuggestion}) => {
if (command === 'replaceMisspelling') { if (command === 'replaceMisspelling') {
@ -144,4 +147,4 @@ if (utils.isElectron()) {
} }
}); });
}); });
} }

View File

@ -59,8 +59,8 @@ function AttributesModel() {
}); });
}; };
async function showAttributes(attributes) { async function showAttributes(noteId, attributes) {
const ownedAttributes = attributes.filter(attr => attr.isOwned); const ownedAttributes = attributes.filter(attr => attr.noteId === noteId);
for (const attr of ownedAttributes) { for (const attr of ownedAttributes) {
attr.labelValue = attr.type === 'label' ? attr.value : ''; attr.labelValue = attr.type === 'label' ? attr.value : '';
@ -86,7 +86,7 @@ function AttributesModel() {
addLastEmptyRow(); addLastEmptyRow();
const inheritedAttributes = attributes.filter(attr => !attr.isOwned); const inheritedAttributes = attributes.filter(attr => attr.noteId !== noteId);
self.inheritedAttributes(inheritedAttributes); self.inheritedAttributes(inheritedAttributes);
} }
@ -96,7 +96,7 @@ function AttributesModel() {
const attributes = await server.get('notes/' + noteId + '/attributes'); 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 // attribute might not be rendered immediatelly so could not focus
setTimeout(() => $(".attribute-type-select:last").trigger('focus'), 1000); setTimeout(() => $(".attribute-type-select:last").trigger('focus'), 1000);
@ -166,7 +166,7 @@ function AttributesModel() {
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave); const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
await showAttributes(attributes); await showAttributes(noteId, attributes);
toastService.showMessage("Attributes have been saved."); toastService.showMessage("Attributes have been saved.");
}; };
@ -311,4 +311,4 @@ $dialog.on('focus', '.label-value', function (e) {
$el: $(this), $el: $(this),
open: true 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 SimilarNotesWidget from "../widgets/collapsible_widgets/similar_notes.js";
import WhatLinksHereWidget from "../widgets/collapsible_widgets/what_links_here.js"; import WhatLinksHereWidget from "../widgets/collapsible_widgets/what_links_here.js";
import SidePaneToggles from "../widgets/side_pane_toggles.js"; import SidePaneToggles from "../widgets/side_pane_toggles.js";
import appContext from "../services/app_context.js";
const RIGHT_PANE_CSS = ` const RIGHT_PANE_CSS = `
<style> <style>
@ -117,6 +116,7 @@ export default class DesktopMainWindowLayout {
.hideInZenMode()) .hideInZenMode())
.child(new FlexContainer('row') .child(new FlexContainer('row')
.collapsible() .collapsible()
.filling()
.child(new SidePaneContainer('left') .child(new SidePaneContainer('left')
.hideInZenMode() .hideInZenMode()
.child(new GlobalButtonsWidget()) .child(new GlobalButtonsWidget())
@ -153,4 +153,4 @@ export default class DesktopMainWindowLayout {
.child(new SidePaneToggles().hideInZenMode()) .child(new SidePaneToggles().hideInZenMode())
); );
} }
} }

View File

@ -4,7 +4,7 @@ import DialogCommandExecutor from "./dialog_command_executor.js";
import Entrypoints from "./entrypoints.js"; import Entrypoints from "./entrypoints.js";
import options from "./options.js"; import options from "./options.js";
import utils from "./utils.js"; import utils from "./utils.js";
import ZoomService from "./zoom.js"; import zoomService from "./zoom.js";
import TabManager from "./tab_manager.js"; import TabManager from "./tab_manager.js";
import treeService from "./tree.js"; import treeService from "./tree.js";
import Component from "../widgets/component.js"; import Component from "../widgets/component.js";
@ -73,7 +73,7 @@ class AppContext extends Component {
} }
if (utils.isElectron()) { if (utils.isElectron()) {
this.child(new ZoomService()); this.child(zoomService);
} }
this.triggerEvent('initialRenderComplete'); 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) { else if (e.which === 1) {
const activeTabContext = appContext.tabManager.getActiveTabContext(); const activeTabContext = appContext.tabManager.getActiveTabContext();
activeTabContext.setNote(notePath) activeTabContext.setNote(notePath);
} }
else { else {
return false; return false;
} }
} }
else { else {
const address = $link.attr('href'); if (e.which === 1) {
const address = $link.attr('href');
if (address && address.startsWith('http')) { if (address && address.startsWith('http')) {
window.open(address, '_blank'); window.open(address, '_blank');
}
}
else {
return false;
} }
} }
return true; return true;
} }
function newTabContextMenu(e) { function linkContextMenu(e) {
const $link = $(e.target).closest("a"); const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link); const notePath = getNotePathFromLink($link);
@ -113,7 +118,7 @@ function newTabContextMenu(e) {
x: e.pageX, x: e.pageX,
y: e.pageY, y: e.pageY,
items: [ 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"} {title: "Open note in new window", command: "openNoteInNewWindow", uiIcon: "window-open"}
], ],
selectMenuItemHandler: ({command}) => { 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-book a', goToLink);
$(document).on('mousedown', '.note-detail-render 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('mousedown', 'a.ck-link-actions__preview', goToLink);
$(document).on('click', 'a.ck-link-actions__preview', e => { $(document).on('click', 'a.ck-link-actions__preview', e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}); });
$(document).on('contextmenu', 'a.ck-link-actions__preview', newTabContextMenu); $(document).on('contextmenu', 'a.ck-link-actions__preview', linkContextMenu);
$(document).on('contextmenu', '.note-detail-text a', newTabContextMenu); $(document).on('contextmenu', '.note-detail-text a', linkContextMenu);
$(document).on('contextmenu', "a[data-action='note']", newTabContextMenu); $(document).on('contextmenu', '.note-detail-readonly-text a', linkContextMenu);
$(document).on('contextmenu', ".note-detail-render a", newTabContextMenu); $(document).on('contextmenu', "a[data-action='note']", linkContextMenu);
$(document).on('contextmenu', ".note-paths-widget a", newTabContextMenu); $(document).on('contextmenu', ".note-detail-render a", linkContextMenu);
$(document).on('contextmenu', ".note-paths-widget a", linkContextMenu);
export default { export default {
getNotePathFromUrl, getNotePathFromUrl,
createNoteLink, createNoteLink,
goToLink goToLink
}; };

View File

@ -37,6 +37,10 @@ function subscribeToMessages(messageHandler) {
// used to serialize sync operations // used to serialize sync operations
let consumeQueuePromise = null; 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) { async function handleMessage(event) {
const message = JSON.parse(event.data); const message = JSON.parse(event.data);
@ -52,14 +56,19 @@ async function handleMessage(event) {
if (syncRows.length > 0) { if (syncRows.length > 0) {
const filteredRows = syncRows.filter(row => const filteredRows = syncRows.filter(row =>
row.entityName !== 'recent_notes' !receivedSyncIds.has(row.id)
&& row.entityName !== 'recent_notes'
&& (row.entityName !== 'options' || row.entityId !== 'openTabs')); && (row.entityName !== 'options' || row.entityId !== 'openTabs'));
if (filteredRows.length > 0) { if (filteredRows.length > 0) {
console.debug(utils.now(), "Sync data: ", filteredRows); 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 // 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); lastAcceptedSyncId = Math.max(lastAcceptedSyncId, syncRows[syncRows.length - 1].id);
@ -170,7 +179,7 @@ function connectWebSocket() {
async function sendPing() { async function sendPing() {
if (Date.now() - lastPingTs > 30000) { 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) { if (ws.readyState === ws.OPEN) {
@ -374,4 +383,4 @@ export default {
subscribeToMessages, subscribeToMessages,
waitForSyncId, waitForSyncId,
waitForMaxKnownSyncId waitForMaxKnownSyncId
}; };

View File

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

View File

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

View File

@ -251,8 +251,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.triggerCommand('setActiveScreen', {screen:'detail'}); this.triggerCommand('setActiveScreen', {screen:'detail'});
} }
}, },
expand: (event, data) => this.setExpandedToServer(data.node.data.branchId, true), expand: (event, data) => this.setExpanded(data.node.data.branchId, true),
collapse: (event, data) => this.setExpandedToServer(data.node.data.branchId, false), collapse: (event, data) => this.setExpanded(data.node.data.branchId, false),
hotkeys: utils.isMobile() ? undefined : { keydown: await this.getHotKeys() }, hotkeys: utils.isMobile() ? undefined : { keydown: await this.getHotKeys() },
dnd5: { dnd5: {
autoExpandMS: 600, autoExpandMS: 600,
@ -807,7 +807,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
async entitiesReloadedEvent({loadResults}) { async entitiesReloadedEvent({loadResults}) {
const activeNode = this.getActiveNode(); const activeNode = this.getActiveNode();
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null; const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
const activeNoteId = activeNode ? activeNode.data.noteId : null; const activeNoteId = activeNode ? activeNode.data.noteId : null;
const noteIdsToUpdate = new Set(); const noteIdsToUpdate = new Set();
@ -929,15 +931,27 @@ export default class NoteTreeWidget extends TabAwareWidget {
if (node) { if (node) {
node.setActive(true, {noEvents: true}); 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); 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() { async reloadTreeFromCache() {
@ -997,7 +1011,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
return false; return false;
} }
}; };
for (const action of actions) { for (const action of actions) {
for (const shortcut of action.effectiveShortcuts) { for (const shortcut of action.effectiveShortcuts) {
hotKeyMap[utils.normalizeShortcut(shortcut)] = node => { hotKeyMap[utils.normalizeShortcut(shortcut)] = node => {
@ -1022,83 +1036,83 @@ export default class NoteTreeWidget extends TabAwareWidget {
async deleteNotesCommand({node}) { async deleteNotesCommand({node}) {
const branchIds = this.getSelectedOrActiveBranchIds(node); const branchIds = this.getSelectedOrActiveBranchIds(node);
await branchService.deleteNotes(branchIds); await branchService.deleteNotes(branchIds);
this.clearSelectedNodes(); this.clearSelectedNodes();
} }
moveNoteUpCommand({node}) { moveNoteUpCommand({node}) {
const beforeNode = node.getPrevSibling(); const beforeNode = node.getPrevSibling();
if (beforeNode !== null) { if (beforeNode !== null) {
branchService.moveBeforeBranch([node.data.branchId], beforeNode.data.branchId); branchService.moveBeforeBranch([node.data.branchId], beforeNode.data.branchId);
} }
} }
moveNoteDownCommand({node}) { moveNoteDownCommand({node}) {
const afterNode = node.getNextSibling(); const afterNode = node.getNextSibling();
if (afterNode !== null) { if (afterNode !== null) {
branchService.moveAfterBranch([node.data.branchId], afterNode.data.branchId); branchService.moveAfterBranch([node.data.branchId], afterNode.data.branchId);
} }
} }
moveNoteUpInHierarchyCommand({node}) { moveNoteUpInHierarchyCommand({node}) {
branchService.moveNodeUpInHierarchy(node); branchService.moveNodeUpInHierarchy(node);
} }
moveNoteDownInHierarchyCommand({node}) { moveNoteDownInHierarchyCommand({node}) {
const toNode = node.getPrevSibling(); const toNode = node.getPrevSibling();
if (toNode !== null) { if (toNode !== null) {
branchService.moveToParentNote([node.data.branchId], toNode.data.noteId); branchService.moveToParentNote([node.data.branchId], toNode.data.noteId);
} }
} }
addNoteAboveToSelectionCommand() { addNoteAboveToSelectionCommand() {
const node = this.getFocusedNode(); const node = this.getFocusedNode();
if (!node) { if (!node) {
return; return;
} }
if (node.isActive()) { if (node.isActive()) {
node.setSelected(true); node.setSelected(true);
} }
const prevSibling = node.getPrevSibling(); const prevSibling = node.getPrevSibling();
if (prevSibling) { if (prevSibling) {
prevSibling.setActive(true, {noEvents: true}); prevSibling.setActive(true, {noEvents: true});
if (prevSibling.isSelected()) { if (prevSibling.isSelected()) {
node.setSelected(false); node.setSelected(false);
} }
prevSibling.setSelected(true); prevSibling.setSelected(true);
} }
} }
addNoteBelowToSelectionCommand() { addNoteBelowToSelectionCommand() {
const node = this.getFocusedNode(); const node = this.getFocusedNode();
if (!node) { if (!node) {
return; return;
} }
if (node.isActive()) { if (node.isActive()) {
node.setSelected(true); node.setSelected(true);
} }
const nextSibling = node.getNextSibling(); const nextSibling = node.getNextSibling();
if (nextSibling) { if (nextSibling) {
nextSibling.setActive(true, {noEvents: true}); nextSibling.setActive(true, {noEvents: true});
if (nextSibling.isSelected()) { if (nextSibling.isSelected()) {
node.setSelected(false); node.setSelected(false);
} }
nextSibling.setSelected(true); nextSibling.setSelected(true);
} }
} }
@ -1182,4 +1196,4 @@ export default class NoteTreeWidget extends TabAwareWidget {
noteCreateService.duplicateNote(node.data.noteId, branch.parentNoteId); noteCreateService.duplicateNote(node.data.noteId, branch.parentNoteId);
} }
} }

View File

@ -19,6 +19,7 @@ const TPL = `
.promoted-attributes td, .promoted-attributes th { .promoted-attributes td, .promoted-attributes th {
padding: 5px; padding: 5px;
min-width: 50px; /* otherwise checkboxes can collapse into 0 width (if there are only checkboxes) */
} }
</style> </style>
@ -98,7 +99,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
const $labelCell = $("<th>").append(valueAttr.name); const $labelCell = $("<th>").append(valueAttr.name);
const $input = $("<input>") const $input = $("<input>")
.prop("tabindex", definitionAttr.position) .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-type", valueAttr.type)
.prop("attribute-name", valueAttr.name) .prop("attribute-name", valueAttr.name)
.prop("value", valueAttr.value) .prop("value", valueAttr.value)
@ -266,4 +267,4 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
$attr.prop("attribute-id", result.attributeId); $attr.prop("attribute-id", result.attributeId);
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
"use strict"; "use strict";
const noteService = require('../../services/notes');
const protectedSessionService = require('../../services/protected_session'); const protectedSessionService = require('../../services/protected_session');
const repository = require('../../services/repository'); const repository = require('../../services/repository');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
@ -45,7 +44,9 @@ async function downloadNoteFile(noteId, res, contentDisposition = true) {
if (contentDisposition) { if (contentDisposition) {
// (one) reason we're not using the originFileName (available as label) is that it's not // (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 // 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); res.setHeader('Content-Type', note.mime);
@ -70,4 +71,4 @@ module.exports = {
openFile, openFile,
downloadFile, downloadFile,
downloadNoteFile downloadNoteFile
}; };

View File

@ -38,13 +38,7 @@ async function getNoteRevision(req) {
* @return {string} * @return {string}
*/ */
function getRevisionFilename(noteRevision) { function getRevisionFilename(noteRevision) {
let filename = noteRevision.title || "untitled"; let filename = utils.formatDownloadTitle(noteRevision.title, noteRevision.type, noteRevision.mime);
if (noteRevision.type === 'text') {
filename += '.html';
} else if (['relation-map', 'search'].includes(noteRevision.type)) {
filename += '.json';
}
const extension = path.extname(filename); const extension = path.extname(filename);
const date = noteRevision.dateCreated const date = noteRevision.dateCreated
@ -158,4 +152,4 @@ module.exports = {
eraseAllNoteRevisions, eraseAllNoteRevisions,
eraseNoteRevision, eraseNoteRevision,
restoreNoteRevision 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 hoistedNoteService = require('../services/hoisted_note');
const protectedSessionService = require('../services/protected_session'); const protectedSessionService = require('../services/protected_session');
const log = require('../services/log'); const log = require('../services/log');
const utils = require('../services/utils');
const noteRevisionService = require('../services/note_revisions'); const noteRevisionService = require('../services/note_revisions');
const attributeService = require('../services/attributes'); const attributeService = require('../services/attributes');
const request = require('./request'); const request = require('./request');
@ -276,9 +277,9 @@ async function downloadImage(noteId, imageUrl) {
const downloadImagePromises = {}; const downloadImagePromises = {};
function replaceUrl(content, url, imageNote) { 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) { async function downloadImages(noteId, content) {

View File

@ -1,6 +1,6 @@
const dayjs = require("dayjs"); 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; const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i;
function calculateSmartValue(v) { function calculateSmartValue(v) {

View File

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

View File

@ -5,6 +5,7 @@ const randtoken = require('rand-token').generator({source: 'crypto'});
const unescape = require('unescape'); const unescape = require('unescape');
const escape = require('escape-html'); const escape = require('escape-html');
const sanitize = require("sanitize-filename"); const sanitize = require("sanitize-filename");
const mimeTypes = require('mime-types');
function newEntityId() { function newEntityId() {
return randomString(12); return randomString(12);
@ -166,10 +167,46 @@ function isStringNote(type, mime) {
|| STRING_MIME_TYPES.includes(mime); || STRING_MIME_TYPES.includes(mime);
} }
function replaceAll(string, replaceWhat, replaceWith) { function quoteRegex(url) {
const escapedWhat = replaceWhat.replace(/([\/,!\\^${}\[\]().*+?|<>\-&])/g, "\\$&"); 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 = { module.exports = {
@ -198,5 +235,7 @@ module.exports = {
sanitizeFilenameForHeader, sanitizeFilenameForHeader,
getContentDisposition, getContentDisposition,
isStringNote, isStringNote,
replaceAll quoteRegex,
}; replaceAll,
formatDownloadTitle
};

View File

@ -5,7 +5,7 @@
<link rel="shortcut icon" href="favicon.ico"> <link rel="shortcut icon" href="favicon.ico">
<title>Trilium Notes</title> <title>Trilium Notes</title>
</head> </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> <noscript>Trilium requires JavaScript to be enabled.</noscript>
<div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div> <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"> <link rel="stylesheet" type="text/css" href="libraries/boxicons/css/boxicons.min.css">
<script>
$("body").show();
</script>
</body> </body>
</html> </html>