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

# Conflicts:
#	package-lock.json
#	src/public/app/services/note_content_renderer.js
#	src/public/stylesheets/style.css
#	src/routes/api/files.js
#	src/routes/routes.js
This commit is contained in:
zadam
2021-04-25 11:14:45 +02:00
55 changed files with 378 additions and 165 deletions

View File

@@ -121,14 +121,14 @@ ws.subscribeToMessages(async message => {
return;
}
if (message.type === 'task-error') {
if (message.type === 'taskError') {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
}
else if (message.type === 'task-progress-count') {
else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, "Export in progress: " + message.progressCount));
}
else if (message.type === 'task-succeeded') {
else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Export finished successfully.");
toast.closeAfter = 5000;

View File

@@ -36,6 +36,7 @@ import SearchResultWidget from "../widgets/search_result.js";
import SyncStatusWidget from "../widgets/sync_status.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import NoteUpdateStatusWidget from "../widgets/note_update_status.js";
const RIGHT_PANE_CSS = `
<style>
@@ -177,6 +178,7 @@ export default class DesktopLayout {
.child(new InheritedAttributesWidget())
)
)
.child(new NoteUpdateStatusWidget())
.child(
new TabCachingWidget(() => new ScrollingContainer()
.child(new SqlTableSchemasWidget())

View File

@@ -1,6 +1,6 @@
import froca from "./froca.js";
import bundleService from "./bundle.js";
import DialogCommandExecutor from "./dialog_command_executor.js";
import RootCommandExecutor from "./root_command_executor.js";
import Entrypoints from "./entrypoints.js";
import options from "./options.js";
import utils from "./utils.js";
@@ -57,7 +57,7 @@ class AppContext extends Component {
this.executors = [
this.tabManager,
new DialogCommandExecutor(),
new RootCommandExecutor(),
new Entrypoints(),
new MainTreeExecutors()
];

View File

@@ -153,12 +153,12 @@ ws.subscribeToMessages(async message => {
return;
}
if (message.type === 'task-error') {
if (message.type === 'taskError') {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'task-progress-count') {
} else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, "Delete notes in progress: " + message.progressCount));
} else if (message.type === 'task-succeeded') {
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Delete finished successfully.");
toast.closeAfter = 5000;
@@ -167,16 +167,16 @@ ws.subscribeToMessages(async message => {
});
ws.subscribeToMessages(async message => {
if (message.taskType !== 'undelete-notes') {
if (message.taskType !== 'undeleteNotes') {
return;
}
if (message.type === 'task-error') {
if (message.type === 'taskError') {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'task-progress-count') {
} else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, "Undeleting notes in progress: " + message.progressCount));
} else if (message.type === 'task-succeeded') {
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Undeleting notes finished successfully.");
toast.closeAfter = 5000;

View File

@@ -64,13 +64,12 @@ async function getWidgetBundlesByParent() {
try {
widget = await executeBundle(bundle);
widgetsByParent.add(widget);
}
catch (e) {
logError("Widget initialization failed: ", e);
continue;
}
widgetsByParent.add(widget);
}
return widgetsByParent;

View File

@@ -0,0 +1,36 @@
import ws from "./ws.js";
import appContext from "./app_context.js";
const fileModificationStatus = {};
function getFileModificationStatus(noteId) {
return fileModificationStatus[noteId];
}
function fileModificationUploaded(noteId) {
delete fileModificationStatus[noteId];
}
function ignoreModification(noteId) {
delete fileModificationStatus[noteId];
}
ws.subscribeToMessages(async message => {
if (message.type !== 'openedFileUpdated') {
return;
}
fileModificationStatus[message.noteId] = message;
appContext.triggerEvent('openedFileUpdated', {
noteId: message.noteId,
lastModifiedMs: message.lastModifiedMs,
filePath: message.filePath
});
});
export default {
getFileModificationStatus,
fileModificationUploaded,
ignoreModification
}

View File

@@ -52,12 +52,12 @@ ws.subscribeToMessages(async message => {
return;
}
if (message.type === 'task-error') {
if (message.type === 'taskError') {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'task-progress-count') {
} else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, "Import in progress: " + message.progressCount));
} else if (message.type === 'task-succeeded') {
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Import finished successfully.");
toast.closeAfter = 5000;

View File

@@ -89,7 +89,11 @@ function updateDisplayedShortcuts($container) {
const action = await getAction(actionName, true);
if (action) {
$(el).text(action.effectiveShortcuts.join(', '));
const keyboardActions = action.effectiveShortcuts.join(', ');
if (keyboardActions || $(el).text() !== "not set") {
$(el).text(keyboardActions);
}
}
});

View File

@@ -39,12 +39,12 @@ async function getRenderedContent(note, options = {}) {
.css("max-width", "100%")
);
}
else if (!options.tooltip && ['file', 'pdf', 'audio', 'video']) {
else if (!options.tooltip && ['file', 'pdf', 'audio', 'video'].includes(type)) {
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
$downloadButton.on('click', () => openService.downloadFileNote(note.noteId));
$openButton.on('click', () => openService.openFileNote(note.noteId));
$openButton.on('click', () => openService.openNoteExternally(note.noteId));
// open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected);
@@ -59,7 +59,7 @@ async function getRenderedContent(note, options = {}) {
}
else if (type === 'audio') {
const $audioPreview = $('<audio controls></audio>')
.attr("src", openService.getUrlForDownload("api/notes/" + note.noteId + "/open"))
.attr("src", openService.getUrlForStreaming("api/notes/" + note.noteId + "/open-partial"))
.attr("type", note.mime)
.css("width", "100%");
@@ -67,7 +67,7 @@ async function getRenderedContent(note, options = {}) {
}
else if (type === 'video') {
const $videoPreview = $('<video controls></video>')
.attr("src", openService.getUrlForDownload("api/notes/" + note.noteId + "/open"))
.attr("src", openService.getUrlForDownload("api/notes/" + note.noteId + "/open-partial"))
.attr("type", note.mime)
.css("width", "100%");

View File

@@ -317,7 +317,9 @@ class NoteListRenderer {
const $expander = $('<span class="note-expander bx bx-chevron-right"></span>');
const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
const notePath = this.parentNote.noteId + '/' + note.noteId;
const notePath = this.parentNote.type === 'search'
? note.noteId // for search note parent we want to display non-search path
: this.parentNote.noteId + '/' + note.noteId;
const $card = $('<div class="note-book-card">')
.attr('data-note-id', note.noteId)

View File

@@ -21,9 +21,9 @@ function downloadFileNote(noteId) {
download(url);
}
async function openFileNote(noteId) {
async function openNoteExternally(noteId) {
if (utils.isElectron()) {
const resp = await server.post("notes/" + noteId + "/saveToTmpDir");
const resp = await server.post("notes/" + noteId + "/save-to-tmp-dir");
const electron = utils.dynamicRequire('electron');
const res = await electron.shell.openPath(resp.tmpFilePath);
@@ -66,7 +66,7 @@ function getHost() {
export default {
download,
downloadFileNote,
openFileNote,
openNoteExternally,
downloadNoteRevision,
getUrlForDownload
}

View File

@@ -89,18 +89,18 @@ function makeToast(message, protectingLabel, text) {
}
ws.subscribeToMessages(async message => {
if (message.taskType !== 'protect-notes') {
if (message.taskType !== 'protectNotes') {
return;
}
const protectingLabel = message.data.protect ? "Protecting" : "Unprotecting";
if (message.type === 'task-error') {
if (message.type === 'taskError') {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'task-progress-count') {
} else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message, protectingLabel,protectingLabel + " in progress: " + message.progressCount));
} else if (message.type === 'task-succeeded') {
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message, protectingLabel, protectingLabel + " finished successfully.");
toast.closeAfter = 3000;

View File

@@ -2,8 +2,9 @@ import Component from "../widgets/component.js";
import appContext from "./app_context.js";
import dateNoteService from "../services/date_notes.js";
import treeService from "../services/tree.js";
import openService from "./open.js";
export default class DialogCommandExecutor extends Component {
export default class RootCommandExecutor extends Component {
jumpToNoteCommand() {
import("../dialogs/jump_to_note.js").then(d => d.showDialog());
}
@@ -84,4 +85,12 @@ export default class DialogCommandExecutor extends Component {
showBackendLogCommand() {
import("../dialogs/backend_log.js").then(d => d.showDialog());
}
openNoteExternallyCommand() {
const noteId = appContext.tabManager.getActiveTabNoteId();
if (noteId) {
openService.openNoteExternally(noteId);
}
}
}

View File

@@ -127,7 +127,7 @@ async function sortAlphabetically(noteId) {
}
ws.subscribeToMessages(message => {
if (message.type === 'open-note') {
if (message.type === 'openNote') {
appContext.tabManager.activateOrOpenNote(message.noteId);
if (utils.isElectron()) {

View File

@@ -59,7 +59,7 @@ async function handleMessage(event) {
}
if (message.type === 'frontend-update') {
let {entityChanges, lastSyncedPush} = message.data;
let {entityChanges} = message.data;
lastPingTs = Date.now();
if (entityChanges.length > 0) {

View File

@@ -22,6 +22,7 @@ const TPL = `
padding-left: 10px;
padding-right: 10px;
position: relative;
top: -2px;
border-radius: 0;
}

View File

@@ -1,11 +1,12 @@
import TabAwareWidget from "./tab_aware_widget.js";
import protectedSessionService from "../services/protected_session.js";
import utils from "../services/utils.js";
const TPL = `
<div class="dropdown note-actions">
<style>
.note-actions .dropdown-menu {
width: 15em;
width: 20em;
}
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
@@ -84,6 +85,7 @@ const TPL = `
<a data-trigger-command="showNoteRevisions" class="dropdown-item show-note-revisions-button">Revisions</a>
<a data-trigger-command="showLinkMap" class="dropdown-item show-link-map-button"><kbd data-command="showLinkMap"></kbd> Link map</a>
<a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a>
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"><kbd data-command="openNoteExternally"></kbd> Open note externally</a>
<a class="dropdown-item import-files-button">Import files</a>
<a class="dropdown-item export-note-button">Export note</a>
<a data-trigger-command="printActiveNote" class="dropdown-item print-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a>
@@ -119,6 +121,8 @@ export default class NoteActionsWidget extends TabAwareWidget {
this.$widget.on('click', '.dropdown-item',
() => this.$widget.find('.dropdown-toggle').dropdown('toggle'));
this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button");
}
refreshWithNote(note) {
@@ -128,6 +132,8 @@ export default class NoteActionsWidget extends TabAwareWidget {
this.$protectButton.toggle(!note.isProtected);
this.$unprotectButton.toggle(!!note.isProtected);
this.$openNoteExternallyButton.toggle(utils.isElectron());
}
toggleDisabled($el, enable) {

View File

@@ -382,8 +382,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
else {
node.setActive();
this.clearSelectedNodes();
}
return false;
@@ -393,6 +391,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
// click event won't propagate so let's close context menu manually
contextMenu.hide();
this.clearSelectedNodes();
const notePath = treeService.getNotePath(data.node);
const activeTabContext = appContext.tabManager.getActiveTabContext();
@@ -1144,11 +1144,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
if (node) {
node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
if (activeNodeFocused) {
node.setFocus(true);
// needed by Firefox: https://github.com/zadam/trilium/issues/1865
this.tree.$container.focus();
}
await node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
}
else {
// this is used when original note has been deleted and we want to move the focus to the note above/below

View File

@@ -0,0 +1,64 @@
import TabAwareWidget from "./tab_aware_widget.js";
import server from "../services/server.js";
import fileWatcher from "../services/file_watcher.js";
const TPL = `
<div class="dropdown note-update-status-widget alert alert-warning">
<style>
.note-update-status-widget {
margin: 10px;
}
</style>
<p>File <code class="file-path"></code> has been last modified on <span class="file-last-modified"></span>.</p>
<div style="display: flex; flex-direction: row; justify-content: space-evenly;">
<button class="btn btn-sm file-upload-button">Upload modified file</button>
<button class="btn btn-sm ignore-this-change-button">Ignore this change</button>
</div>
</div>`;
export default class NoteUpdateStatusWidget extends TabAwareWidget {
isEnabled() {
return super.isEnabled()
&& !!fileWatcher.getFileModificationStatus(this.noteId);
}
doRender() {
this.$widget = $(TPL);
this.overflowing();
this.$filePath = this.$widget.find(".file-path");
this.$fileLastModified = this.$widget.find(".file-last-modified");
this.$fileUploadButton = this.$widget.find(".file-upload-button");
this.$fileUploadButton.on("click", async () => {
await server.post(`notes/${this.noteId}/upload-modified-file`, {
filePath: this.$filePath.text()
});
fileWatcher.fileModificationUploaded(this.noteId);
this.refresh();
});
this.$ignoreThisChangeButton = this.$widget.find(".ignore-this-change-button");
this.$ignoreThisChangeButton.on('click', () => {
fileWatcher.ignoreModification(this.noteId);
this.refresh();
});
}
refreshWithNote(note) {
const status = fileWatcher.getFileModificationStatus(note.noteId);
this.$filePath.text(status.filePath);
this.$fileLastModified.text(dayjs.unix(status.lastModifiedMs / 1000).format("HH:mm:ss"));
}
openedFileUpdatedEvent(data) {
if (data.noteId === this.noteId) {
this.refresh();
}
}
}

View File

@@ -5,7 +5,7 @@ const TPL = `
<td colspan="2">
<span class="bx bx-trash"></span>
Delete matched note
Delete matched notes
</td>
<td class="button-column">
<span class="bx bx-x icon-action action-conf-del"></span>

View File

@@ -21,6 +21,7 @@ const TPL = `
<option value="ownedLabelCount">Number of labels</option>
<option value="ownedRelationCount">Number of relations</option>
<option value="targetRelationCount">Number of relations targeting the note</option>
<option value="random">Random order</option>
</select>
<select name="orderDirection" class="form-control w-auto d-inline">

View File

@@ -82,7 +82,7 @@ export default class FilePropertiesWidget extends TabAwareWidget {
this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input");
this.$downloadButton.on('click', () => openService.downloadFileNote(this.noteId));
this.$openButton.on('click', () => openService.openFileNote(this.noteId));
this.$openButton.on('click', () => openService.openNoteExternally(this.noteId));
this.$uploadNewRevisionButton.on("click", () => {
this.$uploadNewRevisionInput.trigger("click");

View File

@@ -26,6 +26,8 @@ const TPL = `
<div class="no-print" style="display: flex; justify-content: space-evenly; margin: 10px;">
<button class="image-download btn btn-sm btn-primary" type="button">Download</button>
<button class="image-open btn btn-sm btn-primary" type="button">Open</button>
<button class="image-copy-to-clipboard btn btn-sm btn-primary" type="button">Copy to clipboard</button>
<button class="image-upload-new-revision btn btn-sm btn-primary" type="button">Upload new revision</button>
@@ -59,6 +61,9 @@ export default class ImagePropertiesWidget extends TabAwareWidget {
this.$fileType = this.$widget.find(".image-filetype");
this.$fileSize = this.$widget.find(".image-filesize");
this.$openButton = this.$widget.find(".image-open");
this.$openButton.on('click', () => openService.openNoteExternally(this.noteId));
this.$imageDownloadButton = this.$widget.find(".image-download");
this.$imageDownloadButton.on('click', () => openService.downloadFileNote(this.noteId));

View File

@@ -1,6 +1,7 @@
import utils from "../../services/utils.js";
import openService from "../../services/open.js";
import TypeWidget from "./type_widget.js";
import fileWatcher from "../../services/file_watcher.js";
import server from "../../services/server.js";
const TPL = `
<div class="note-detail-file note-detail-printable">
@@ -50,9 +51,6 @@ export default class FileTypeWidget extends TypeWidget {
}
async doRefresh(note) {
const attributes = note.getAttributes();
const attributeMap = utils.toObject(attributes, l => [l.name, l.value]);
this.$widget.show();
const noteComplement = await this.tabContext.getNoteComplement();
@@ -73,14 +71,14 @@ export default class FileTypeWidget extends TypeWidget {
else if (note.mime.startsWith('video/')) {
this.$videoPreview
.show()
.attr("src", openService.getUrlForDownload("api/notes/" + this.noteId + "/open"))
.attr("src", openService.getUrlForDownload("api/notes/" + this.noteId + "/open-partial"))
.attr("type", this.note.mime)
.css("width", this.$widget.width());
}
else if (note.mime.startsWith('audio/')) {
this.$audioPreview
.show()
.attr("src", openService.getUrlForDownload("api/notes/" + this.noteId + "/open"))
.attr("src", openService.getUrlForDownload("api/notes/" + this.noteId + "/open-partial"))
.attr("type", this.note.mime)
.css("width", this.$widget.width());
}

View File

@@ -164,11 +164,6 @@ div.ui-tooltip {
overflow: auto;
}
.alert {
padding: 5px;
width: auto;
}
/*
* .search-inactive is added to search window <webview> when the window
* is inactive.
@@ -761,9 +756,14 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
width: 100%;
}
.alert {
padding: 8px 14px;
width: auto;
}
.alert-warning, .alert-info {
color: var(--main-text-color) !important;
background-color: var(--accented-background-color) !important;
background-color: transparent !important;
border-color: var(--main-border-color) !important;
}

View File

@@ -107,7 +107,11 @@ function processContent(images, note, content) {
const filename = path.basename(src);
if (!dataUrl || !dataUrl.startsWith("data:image")) {
log.info("Image could not be recognized as data URL:", dataUrl.substr(0, Math.min(100, dataUrl.length)));
const excerpt = dataUrl
? dataUrl.substr(0, Math.min(100, dataUrl.length))
: "null";
log.info("Image could not be recognized as data URL: " + excerpt);
continue;
}
@@ -140,7 +144,7 @@ function processContent(images, note, content) {
function openNote(req) {
if (utils.isElectron()) {
ws.sendMessageToAllClients({
type: 'open-note',
type: 'openNote',
noteId: req.params.noteId
});

View File

@@ -3,10 +3,13 @@
const protectedSessionService = require('../../services/protected_session');
const repository = require('../../services/repository');
const utils = require('../../services/utils');
const log = require('../../services/log');
const noteRevisionService = require('../../services/note_revisions');
const tmp = require('tmp');
const fs = require('fs');
const { Readable } = require('stream');
const chokidar = require('chokidar');
const ws = require('../../services/ws');
function updateFile(req) {
const {noteId} = req.params;
@@ -120,6 +123,19 @@ function saveToTmpDir(req) {
fs.writeSync(tmpObj.fd, note.getContent());
fs.closeSync(tmpObj.fd);
log.info(`Saved temporary file for note ${noteId} into ${tmpObj.name}`);
if (utils.isElectron()) {
chokidar.watch(tmpObj.name).on('change', (path, stats) => {
ws.sendMessageToAllClients({
type: 'openedFileUpdated',
noteId: noteId,
lastModifiedMs: stats.atimeMs,
filePath: tmpObj.name
});
});
}
return {
tmpFilePath: tmpObj.name
};

View File

@@ -7,6 +7,8 @@ const sql = require('../../services/sql');
const utils = require('../../services/utils');
const log = require('../../services/log');
const TaskContext = require('../../services/task_context');
const fs = require('fs');
const noteRevisionService = require("../../services/note_revisions.js");
function getNote(req) {
const noteId = req.params.noteId;
@@ -80,7 +82,7 @@ function deleteNote(req) {
function undeleteNote(req) {
const note = repository.getNote(req.params.noteId);
const taskContext = TaskContext.getInstance(utils.randomString(10), 'undelete-notes');
const taskContext = TaskContext.getInstance(utils.randomString(10), 'undeleteNotes');
noteService.undeleteNote(note, note.deleteId, taskContext);
@@ -109,7 +111,7 @@ function protectNote(req) {
const protect = !!parseInt(req.params.isProtected);
const includingSubTree = !!parseInt(req.query.subtree);
const taskContext = new TaskContext(utils.randomString(10), 'protect-notes', {protect});
const taskContext = new TaskContext(utils.randomString(10), 'protectNotes', {protect});
noteService.protectNoteRecursively(note, protect, includingSubTree, taskContext);
@@ -273,6 +275,29 @@ function getDeleteNotesPreview(req) {
};
}
function uploadModifiedFile(req) {
const noteId = req.params.noteId;
const {filePath} = req.body;
const note = repository.getNote(noteId);
if (!note) {
return [404, `Note ${noteId} has not been found`];
}
log.info(`Updating note ${noteId} with content from ${filePath}`);
noteRevisionService.createNoteRevision(note);
const fileContent = fs.readFileSync(filePath);
if (!fileContent) {
return [400, `File ${fileContent} is empty`];
}
note.setContent(fileContent);
}
module.exports = {
getNote,
updateNote,
@@ -286,5 +311,6 @@ module.exports = {
changeTitle,
duplicateSubtree,
eraseDeletedNotesNow,
getDeleteNotesPreview
getDeleteNotesPreview,
uploadModifiedFile
};

View File

@@ -3,6 +3,7 @@
const utils = require('../services/utils');
const optionService = require('../services/options');
const myScryptService = require('../services/my_scrypt');
const log = require('../services/log');
function loginPage(req, res) {
res.render('login', { failedAuth: false });
@@ -28,6 +29,9 @@ function login(req, res) {
});
}
else {
// note that logged IP address is usually meaningless since the traffic should come from a reverse proxy
log.info(`WARNING: Wrong username / password from ${req.ip}, rejecting.`);
res.render('login', {'failedAuth': true});
}
}

View File

@@ -165,6 +165,7 @@ function register(app) {
apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow);
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree);
apiRoute(POST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile);
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
@@ -177,14 +178,15 @@ function register(app) {
route(PUT, '/api/notes/:noteId/file', [auth.checkApiAuthOrElectron, uploadMiddleware, csrfMiddleware],
filesRoute.updateFile, apiResultHandler);
route(GET, '/api/notes/:noteId/open', [auth.checkApiAuthOrElectron],
route(GET, '/api/notes/:noteId/open', [auth.checkApiAuthOrElectron], filesRoute.openFile);
route(GET, '/api/notes/:noteId/open-partial', [auth.checkApiAuthOrElectron],
createPartialContentHandler(filesRoute.fileContentProvider, {
debug: (string, extra) => { console.log(string, extra); }
}));
route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
// this "hacky" path is used for easier referencing of CSS resources
route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
apiRoute(POST, '/api/notes/:noteId/saveToTmpDir', filesRoute.saveToTmpDir);
apiRoute(POST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveToTmpDir);
apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes);
apiRoute(POST, '/api/notes/:noteId/attributes', attributesRoute.addNoteAttribute);

Binary file not shown.

View File

@@ -1 +1 @@
module.exports = { buildDate:"2021-04-11T22:29:56+02:00", buildRevision: "58e4bd4974275a113c50e4ed7a554987921d55fc" };
module.exports = { buildDate:"2021-04-19T22:43:03+02:00", buildRevision: "6136243d6117910b80feafad4fc7121ecc42d794" };

View File

@@ -36,6 +36,12 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
if (opmlVersion === 1) {
title = outline.$.title;
content = toHtml(outline.$.text);
if (!title || !title.trim()) {
// https://github.com/zadam/trilium/issues/1862
title = outline.$.text;
content = '';
}
}
else if (opmlVersion === 2) {
title = outline.$.text;

View File

@@ -352,6 +352,12 @@ const DEFAULT_KEYBOARD_ACTIONS = [
defaultShortcuts: [],
scope: "window"
},
{
actionName: "openNoteExternally",
defaultShortcuts: [],
description: "Open note as a file with default application",
scope: "window"
},
{
actionName: "renderActiveNote",
defaultShortcuts: [],

View File

@@ -1,6 +1,7 @@
"use strict";
const Expression = require('./expression');
const TrueExp = require("./true.js");
class AndExp extends Expression {
static of(subExpressions) {
@@ -10,6 +11,8 @@ class AndExp extends Expression {
return subExpressions[0];
} else if (subExpressions.length > 0) {
return new AndExp(subExpressions);
} else {
return new TrueExp();
}
}

View File

@@ -2,6 +2,7 @@
const Expression = require('./expression');
const NoteSet = require('../note_set');
const TrueExp = require("./true");
class OrExp extends Expression {
static of(subExpressions) {
@@ -13,6 +14,9 @@ class OrExp extends Expression {
else if (subExpressions.length > 0) {
return new OrExp(subExpressions);
}
else {
return new TrueExp();
}
}
constructor(subExpressions) {

View File

@@ -28,17 +28,33 @@ class OrderByAndLimitExp extends Expression {
let valA = valueExtractor.extract(a);
let valB = valueExtractor.extract(b);
if (!isNaN(valA) && !isNaN(valB)) {
if (valA === null && valB === null) {
// neither has attribute at all
continue;
}
else if (valB === null) {
return smaller;
}
else if (valA === null) {
return larger;
}
// if both are numbers then parse them for numerical comparison
// beware that isNaN will return false for empty string and null
if (valA.trim() !== "" && valB.trim() !== "" && !isNaN(valA) && !isNaN(valB)) {
valA = parseFloat(valA);
valB = parseFloat(valB);
}
if (valA < valB) {
if (!valA && !valB) {
// the attribute is not defined in either note so continue to next order definition
continue;
} else if (!valB || valA < valB) {
return smaller;
} else if (valA > valB) {
} else if (!valA || valA > valB) {
return larger;
}
// else go to next order definition
// else the values are equal and continue to next order definition
}
return 0;

View File

@@ -0,0 +1,11 @@
"use strict";
const Expression = require('./expression');
class TrueExp extends Expression {
execute(inputNoteSet, executionContext) {
return inputNoteSet;
}
}
module.exports = TrueExp;

View File

@@ -329,6 +329,9 @@ function getExpression(tokens, searchContext, level = 0) {
else if (op === 'or') {
return OrExp.of(expressions);
}
else {
throw new Error(`Unrecognized op=${op}`);
}
}
for (i = 0; i < tokens.length; i++) {
@@ -358,8 +361,7 @@ function getExpression(tokens, searchContext, level = 0) {
continue;
}
exp.subExpression = getAggregateExpression();
exp.subExpression = getAggregateExpression();console.log(exp);
return exp;
}
else if (token === 'not') {

View File

@@ -69,7 +69,7 @@ class ValueExtractor {
i++;
}
else if (pathEl in PROP_MAPPING) {
else if (pathEl in PROP_MAPPING || pathEl === 'random') {
if (i !== this.propertyPath.length - 1) {
return `${pathEl} is a terminal property specifier and must be at the end`;
}
@@ -113,6 +113,9 @@ class ValueExtractor {
else if (cur() === 'children') {
cursor = cursor.children[0];
}
else if (cur() === 'random') {
return Math.random();
}
else if (cur() in PROP_MAPPING) {
return cursor[PROP_MAPPING[cur()]];
}

View File

@@ -59,6 +59,7 @@ async function sync() {
if (e.message &&
(e.message.includes('ECONNREFUSED') ||
e.message.includes('ERR_CONNECTION_REFUSED') ||
e.message.includes('ERR_ADDRESS_UNREACHABLE') ||
e.message.includes('Bad Gateway'))) {
ws.syncFailed();

View File

@@ -38,7 +38,7 @@ class TaskContext {
this.lastSentCountTs = Date.now();
ws.sendMessageToAllClients({
type: 'task-progress-count',
type: 'taskProgressCount',
taskId: this.taskId,
taskType: this.taskType,
data: this.data,
@@ -49,7 +49,7 @@ class TaskContext {
reportError(message) {
ws.sendMessageToAllClients({
type: 'task-error',
type: 'taskError',
taskId: this.taskId,
taskType: this.taskType,
data: this.data,
@@ -59,7 +59,7 @@ class TaskContext {
taskSucceeded(result) {
ws.sendMessageToAllClients({
type: 'task-succeeded',
type: 'taskSucceeded',
taskId: this.taskId,
taskType: this.taskType,
data: this.data,

View File

@@ -188,6 +188,8 @@ function formatDownloadTitle(filename, type, mime) {
filename = "untitled";
}
filename = sanitize(filename);
if (type === 'text') {
return filename + '.html';
} else if (['relation-map', 'search'].includes(type)) {

View File

@@ -18,12 +18,12 @@
<ul>
<li><kbd>UP</kbd>, <kbd>DOWN</kbd> - go up/down in the list of notes</li>
<li><kbd>LEFT</kbd>, <kbd>RIGHT</kbd> - collapse/expand node</li>
<li><kbd data-command="backInNoteHistory"></kbd>, <kbd data-command="forwardInNoteHistory"></kbd> - go back / forwards in the history</li>
<li><kbd data-command="jumpToNote"></kbd> - show <a class="external" href="https://github.com/zadam/trilium/wiki/Note-navigation#jump-to-note">"Jump to" dialog</a></li>
<li><kbd data-command="scrollToActiveNote"></kbd> - scroll to active note</li>
<li><kbd data-command="backInNoteHistory">not set</kbd>, <kbd data-command="forwardInNoteHistory">not set</kbd> - go back / forwards in the history</li>
<li><kbd data-command="jumpToNote">not set</kbd> - show <a class="external" href="https://github.com/zadam/trilium/wiki/Note-navigation#jump-to-note">"Jump to" dialog</a></li>
<li><kbd data-command="scrollToActiveNote">not set</kbd> - scroll to active note</li>
<li><kbd>Backspace</kbd> - jump to parent note</li>
<li><kbd data-command="collapseTree"></kbd> - collapse whole note tree</li>
<li><kbd data-command="collapseSubtree"></kbd> - collapse sub-tree</li>
<li><kbd data-command="collapseTree">not set</kbd> - collapse whole note tree</li>
<li><kbd data-command="collapseSubtree">not set</kbd> - collapse sub-tree</li>
</ul>
</p>
</div>
@@ -40,10 +40,10 @@
Only in desktop (electron build):
<ul>
<li><kbd data-command="openNewTab"></kbd> open empty tab</li>
<li><kbd data-command="closeActiveTab"></kbd> close active tab</li>
<li><kbd data-command="activateNextTab"></kbd> activate next tab</li>
<li><kbd data-command="activatePreviousTab"></kbd> activate previous tab</li>
<li><kbd data-command="openNewTab">not set</kbd> open empty tab</li>
<li><kbd data-command="closeActiveTab">not set</kbd> close active tab</li>
<li><kbd data-command="activateNextTab">not set</kbd> activate next tab</li>
<li><kbd data-command="activatePreviousTab">not set</kbd> activate previous tab</li>
</ul>
</p>
</div>
@@ -55,9 +55,9 @@
<p class="card-text">
<ul>
<li><kbd data-command="createNoteAfter"></kbd> - create new note after the active note</li>
<li><kbd data-command="createNoteInto"></kbd> - create new sub-note into active note</li>
<li><kbd data-command="editBranchPrefix"></kbd> - edit <a class="external" href="https://github.com/zadam/trilium/wiki/Tree concepts#prefix">prefix</a> of active note clone</li>
<li><kbd data-command="createNoteAfter">not set</kbd> - create new note after the active note</li>
<li><kbd data-command="createNoteInto">not set</kbd> - create new sub-note into active note</li>
<li><kbd data-command="editBranchPrefix">not set</kbd> - edit <a class="external" href="https://github.com/zadam/trilium/wiki/Tree concepts#prefix">prefix</a> of active note clone</li>
</ul>
</p>
</div>
@@ -69,15 +69,15 @@
<p class="card-text">
<ul>
<li><kbd data-command="moveNoteUp"></kbd>, <kbd data-command="moveNoteDown"></kbd> - move note up/down in the note list</li>
<li><kbd data-command="moveNoteUpInHierarchy"></kbd>, <kbd data-command="moveNoteDownInHierarchy"></kbd> - move note up in the hierarchy</li>
<li><kbd data-command="addNoteAboveToSelection"></kbd>, <kbd data-command="addNoteBelowToSelection"></kbd> - multi-select note above/below</li>
<li><kbd data-command="selectAllNotesInParent"></kbd> - select all notes in the current level</li>
<li><kbd data-command="moveNoteUp">not set</kbd>, <kbd data-command="moveNoteDown">not set</kbd> - move note up/down in the note list</li>
<li><kbd data-command="moveNoteUpInHierarchy">not set</kbd>, <kbd data-command="moveNoteDownInHierarchy">not set</kbd> - move note up in the hierarchy</li>
<li><kbd data-command="addNoteAboveToSelection">not set</kbd>, <kbd data-command="addNoteBelowToSelection">not set</kbd> - multi-select note above/below</li>
<li><kbd data-command="selectAllNotesInParent">not set</kbd> - select all notes in the current level</li>
<li><kbd>Shift+click</kbd> - select note</li>
<li><kbd data-command="copyNotesToClipboard"></kbd> - copy active note (or current selection) into clipboard (used for <a class="external" href="https://github.com/zadam/trilium/wiki/Cloning notes">cloning</a>)</li>
<li><kbd data-command="cutNotesToClipboard"></kbd> - cut current (or current selection) note into clipboard (used for moving notes)</li>
<li><kbd data-command="pasteNotesFromClipboard"></kbd> - paste note(s) as sub-note into active note (which is either move or clone depending on whether it was copied or cut into clipboard)</li>
<li><kbd data-command="deleteNotes"></kbd> - delete note / sub-tree</li>
<li><kbd data-command="copyNotesToClipboard">not set</kbd> - copy active note (or current selection) into clipboard (used for <a class="external" href="https://github.com/zadam/trilium/wiki/Cloning notes">cloning</a>)</li>
<li><kbd data-command="cutNotesToClipboard">not set</kbd> - cut current (or current selection) note into clipboard (used for moving notes)</li>
<li><kbd data-command="pasteNotesFromClipboard">not set</kbd> - paste note(s) as sub-note into active note (which is either move or clone depending on whether it was copied or cut into clipboard)</li>
<li><kbd data-command="deleteNotes">not set</kbd> - delete note / sub-tree</li>
</ul>
</p>
</div>
@@ -89,12 +89,12 @@
<p class="card-text">
<ul>
<li><kbd data-command="editNoteTitle"></kbd> in tree pane will switch from tree pane into note title. Enter from note title will switch focus to text editor.
<kbd data-command="scrollToActiveNote"></kbd> will switch back from editor to tree pane.</li>
<li><kbd data-command="editNoteTitle">not set</kbd> in tree pane will switch from tree pane into note title. Enter from note title will switch focus to text editor.
<kbd data-command="scrollToActiveNote">not set</kbd> will switch back from editor to tree pane.</li>
<li><kbd>Ctrl+K</kbd> - create / edit external link</li>
<li><kbd data-command="addLinkToText"></kbd> - create internal link</li>
<li><kbd data-command="insertDateTimeToText"></kbd> - insert current date and time at caret position</li>
<li><kbd data-command="scrollToActiveNote"></kbd> - jump away to the tree pane and scroll to active note</li>
<li><kbd data-command="addLinkToText">not set</kbd> - create internal link</li>
<li><kbd data-command="insertDateTimeToText">not set</kbd> - insert current date and time at caret position</li>
<li><kbd data-command="scrollToActiveNote">not set</kbd> - jump away to the tree pane and scroll to active note</li>
</ul>
</p>
</div>
@@ -121,9 +121,9 @@
<p class="card-text">
<ul>
<li><kbd data-command="reloadFrontendApp"></kbd> - reload Trilium frontend</li>
<li><kbd data-command="openDevTools"></kbd> - show developer tools</li>
<li><kbd data-command="showSQLConsole"></kbd> - show SQL console</li>
<li><kbd data-command="reloadFrontendApp">not set</kbd> - reload Trilium frontend</li>
<li><kbd data-command="openDevTools">not set</kbd> - show developer tools</li>
<li><kbd data-command="showSQLConsole">not set</kbd> - show SQL console</li>
</ul>
</p>
</div>
@@ -135,9 +135,9 @@
<p class="card-text">
<ul>
<li><kbd data-command="toggleZenMode"></kbd> - Zen mode - display only note editor, everything else is hidden</li>
<li><kbd data-command="searchNotes"></kbd> - toggle search form in tree pane</li>
<li><kbd data-command="findInText"></kbd> - in page search</li>
<li><kbd data-command="toggleZenMode">not set</kbd> - Zen mode - display only note editor, everything else is hidden</li>
<li><kbd data-command="quickSearch">not set</kbd> - focus on quick search input</li>
<li><kbd data-command="findInText">not set</kbd> - in page search</li>
</ul>
</p>
</div>

11
src/www
View File

@@ -8,10 +8,13 @@ process.on('unhandledRejection', error => {
require('./services/log').info(error);
});
process.on('SIGINT', function() {
console.log("Caught interrupt signal. Exiting.");
process.exit();
});
function exit() {
console.log("Caught interrupt/termination signal. Exiting.");
process.exit(0);
}
process.on('SIGINT', exit);
process.on('SIGTERM', exit);
const { app, sessionParser } = require('./app');
const fs = require('fs');