Compare commits

...

19 Commits

Author SHA1 Message Date
zadam b0a3f828fb release 0.38.1-beta 2019-12-10 23:09:02 +01:00
zadam 76f5736255 update demo document 2019-12-10 23:08:50 +01:00
zadam a82066d899 ability to define a keyboard shortcut for paste markdown, closes #761 2019-12-10 23:04:18 +01:00
zadam 45c5287d53 protection against note switching race conditions 2019-12-10 22:35:24 +01:00
zadam dce54c7af3 run consistency checks on demand 2019-12-10 22:03:00 +01:00
zadam ee15db0ae1 for title/content search does not make sense to search for protected notes 2019-12-10 21:40:53 +01:00
zadam c48dbb0913 more debugging info for problems after sync 2019-12-10 21:31:24 +01:00
zadam 6f32d6fabe new mechanism to wait for sync after interaction with backend in Script API using api.waitForMaxKnownSyncId() 2019-12-09 23:07:45 +01:00
zadam 1e123f2390 sql console tweaks 2019-12-09 21:31:38 +01:00
zadam b29155775e Merge branch 'stable'
# Conflicts:
#	package-lock.json
2019-12-09 20:23:22 +01:00
zadam b821ed28fc refactor mention setup 2019-12-09 20:09:55 +01:00
zadam fdb8959aa1 move mime types loading to avoid race conditions 2019-12-09 20:06:36 +01:00
zadam 7554cb057b support unicode characters in filters without quotes, fixes #757 2019-12-09 19:38:48 +01:00
zadam fab959539a ability to run multiple queries (and get multiple result sets) from SQL console 2019-12-08 11:20:44 +01:00
zadam afe44a6fe8 Merge branch 'stable' 2019-12-08 10:37:19 +01:00
zadam 7ed526beb7 fix clipper, closes #752 2019-12-08 09:41:31 +01:00
zadam af695802e3 codeNotesMimeTypes option has not been created for new documents, fixes #755 2019-12-08 09:12:42 +01:00
zadam 156f040880 added "backend log" dialog 2019-12-05 21:25:36 +01:00
zadam cf6f04defb better port error logging 2019-12-04 22:59:26 +01:00
41 changed files with 1082 additions and 798 deletions
BIN
View File
Binary file not shown.
+1
View File
@@ -959,6 +959,7 @@ class Note extends Entity {
delete pojo.isContentAvailable;
delete pojo.__attributeCache;
delete pojo.__ownedAttributeCache;
delete pojo.content;
/** zero references to contentHash, probably can be removed */
delete pojo.contentHash;
+84
View File
@@ -4975,6 +4975,90 @@ Internally this serializes the anonymous function into string and sends it to ba
<h4 class="name" id="waitUntilSynced"><span class="type-signature"></span>waitUntilSynced<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line389">line 389</a>
</li></ul></dd>
</dl>
+1 -1
View File
@@ -47,7 +47,7 @@ class Branch {
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
return this.treeCache.getNote(this.noteId);
}
/** @returns {boolean} true if it's top level, meaning its parent is root note */
@@ -410,6 +410,11 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
* @param {function} handler
*/
this.bindGlobalShortcut = utils.bindGlobalShortcut;
/**
* @method
*/
this.waitUntilSynced = ws.waitForMaxKnownSyncId;
}
export default FrontendScriptApi;</code></pre>
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.37.8",
"version": "0.38.0-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -6996,9 +6996,9 @@
},
"dependencies": {
"glob": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -9307,9 +9307,9 @@
}
},
"sqlite3": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.0.tgz",
"integrity": "sha512-RvqoKxq+8pDHsJo7aXxsFR18i+dU2Wp5o12qAJOV5LNcDt+fgJsc2QKKg3sIRfXrN9ZjzY1T7SNe/DFVqAXjaw==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.1.tgz",
"integrity": "sha512-CvT5XY+MWnn0HkbwVKJAyWEMfzpAPwnTiB3TobA5Mri44SrTovmmh499NPQP+gatkeOipqPlBLel7rn4E/PCQg==",
"requires": {
"nan": "^2.12.1",
"node-pre-gyp": "^0.11.0",
+2 -2
View File
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.38.0-beta",
"version": "0.38.1-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -68,7 +68,7 @@
"session-file-store": "1.3.1",
"simple-node-logger": "18.12.23",
"sqlite": "3.0.3",
"sqlite3": "4.1.0",
"sqlite3": "4.1.1",
"string-similarity": "3.0.0",
"tar-stream": "2.1.0",
"turndown": "5.0.3",
+1
View File
@@ -931,6 +931,7 @@ class Note extends Entity {
delete pojo.isContentAvailable;
delete pojo.__attributeCache;
delete pojo.__ownedAttributeCache;
delete pojo.content;
/** zero references to contentHash, probably can be removed */
delete pojo.contentHash;
@@ -0,0 +1,32 @@
import server from "../services/server.js";
import utils from "../services/utils.js";
const $dialog = $("#backend-log-dialog");
const $backendLogTextArea = $("#backend-log-textarea");
const $refreshBackendLog = $("#refresh-backend-log-button");
export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$dialog.modal();
load();
}
function scrollToBottom() {
$backendLogTextArea.scrollTop($backendLogTextArea[0].scrollHeight);
}
async function load() {
const backendLog = await server.get('backend-log');
$backendLogTextArea.text(backendLog);
scrollToBottom();
}
$refreshBackendLog.on('click', load);
$dialog.on('shown.bs.modal', scrollToBottom);
@@ -26,6 +26,10 @@ async function convertMarkdownToHtml(text) {
}
export async function importMarkdownInline() {
if (noteDetailService.getActiveTabNoteType() !== 'text') {
return;
}
if (utils.isElectron()) {
const {clipboard} = require('electron');
const text = clipboard.readText();
@@ -13,6 +13,10 @@ const TPL = `
<br/>
<br/>
<h4>Consistency checks</h4>
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button><br/><br/>
<h4>Debugging</h4>
<button id="anonymize-button" class="btn">Save anonymized database</button><br/><br/>
@@ -33,9 +37,8 @@ export default class AdvancedOptions {
this.$forceFullSyncButton = $("#force-full-sync-button");
this.$fillSyncRowsButton = $("#fill-sync-rows-button");
this.$anonymizeButton = $("#anonymize-button");
this.$cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button");
this.$cleanupUnusedImagesButton = $("#cleanup-unused-images-button");
this.$vacuumDatabaseButton = $("#vacuum-database-button");
this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button");
this.$forceFullSyncButton.on('click', async () => {
await server.post('sync/force-full-sync');
@@ -55,26 +58,16 @@ export default class AdvancedOptions {
toastService.showMessage("Created anonymized database");
});
this.$cleanupSoftDeletedButton.on('click', async () => {
if (confirm("Do you really want to clean up soft-deleted items?")) {
await server.post('cleanup/cleanup-soft-deleted-items');
toastService.showMessage("Soft deleted items have been cleaned up");
}
});
this.$cleanupUnusedImagesButton.on('click', async () => {
if (confirm("Do you really want to clean up unused images?")) {
await server.post('cleanup/cleanup-unused-images');
toastService.showMessage("Unused images have been cleaned up");
}
});
this.$vacuumDatabaseButton.on('click', async () => {
await server.post('cleanup/vacuum-database');
toastService.showMessage("Database has been vacuumed");
});
this.$findAndFixConsistencyIssuesButton.on('click', async () => {
await server.post('cleanup/find-and-fix-consistency-issues');
toastService.showMessage("Consistency issues should be fixed.");
});
}
}
+40 -26
View File
@@ -6,9 +6,8 @@ import utils from "../services/utils.js";
const $dialog = $("#sql-console-dialog");
const $query = $('#sql-console-query');
const $executeButton = $('#sql-console-execute');
const $resultHead = $('#sql-console-results thead');
const $resultBody = $('#sql-console-results tbody');
const $tables = $("#sql-console-tables");
const $tableSchemas = $("#sql-console-table-schemas");
const $resultContainer = $("#result-container");
let codeEditor;
@@ -19,7 +18,7 @@ export async function showDialog() {
glob.activeDialog = $dialog;
await showTables();
await showTableSchemas();
$dialog.modal();
}
@@ -45,6 +44,10 @@ async function initEditor() {
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
codeEditor.setValue(`SELECT title, isProtected, type, mime FROM notes WHERE noteId = 'root';
---
SELECT noteId, parentNoteId, notePosition, prefix FROM branches WHERE branchId = 'root';`);
}
codeEditor.focus();
@@ -70,55 +73,66 @@ async function execute() {
toastService.showMessage("Query was executed successfully.");
}
const rows = result.rows;
const results = result.results;
$resultHead.empty();
$resultBody.empty();
$resultContainer.empty();
for (const rows of results) {
if (rows.length === 0) {
continue;
}
const $table = $('<table class="table table-striped">');
$resultContainer.append($table);
if (rows.length > 0) {
const result = rows[0];
const rowEl = $("<tr>");
const $row = $("<tr>");
for (const key in result) {
rowEl.append($("<th>").html(key));
$row.append($("<th>").html(key));
}
$resultHead.append(rowEl);
}
$table.append($row);
for (const result of rows) {
const rowEl = $("<tr>");
for (const result of rows) {
const $row = $("<tr>");
for (const key in result) {
rowEl.append($("<td>").html(result[key]));
for (const key in result) {
$row.append($("<td>").html(result[key]));
}
$table.append($row);
}
$resultBody.append(rowEl);
}
}
async function showTables() {
async function showTableSchemas() {
const tables = await server.get('sql/schema');
$tables.empty();
$tableSchemas.empty();
for (const table of tables) {
const $tableLink = $('<button class="btn">').text(table.name);
const $columns = $("<table>");
const $columns = $("<ul>");
for (const column of table.columns) {
$columns.append(
$("<tr>")
.append($("<td>").text(column.name))
.append($("<td>").text(column.type))
$("<li>")
.append($("<span>").text(column.name))
.append($("<span>").text(column.type))
);
}
$tables.append($tableLink).append(" ");
$tableSchemas.append($tableLink).append(" ");
$tableLink
.tooltip({html: true, title: $columns.html()})
.tooltip({
html: true,
placement: 'bottom',
boundary: 'window',
title: $columns[0].outerHTML
})
.on('click', () => codeEditor.setValue("SELECT * FROM " + table.name + " LIMIT 100"));
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ class Branch {
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
return this.treeCache.getNote(this.noteId);
}
/** @returns {boolean} true if it's top level, meaning its parent is root note */
@@ -17,6 +17,7 @@ const JUMP_TO_NOTE = "../dialogs/jump_to_note.js";
const NOTE_SOURCE = "../dialogs/note_source.js";
const RECENT_CHANGES = "../dialogs/recent_changes.js";
const SQL_CONSOLE = "../dialogs/sql_console.js";
const BACKEND_LOG = "../dialogs/backend_log.js";
const ATTRIBUTES = "../dialogs/attributes.js";
const HELP = "../dialogs/help.js";
const NOTE_INFO = "../dialogs/note_info.js";
@@ -95,6 +96,10 @@ function registerEntrypoints() {
$("#open-sql-console-button").on('click', showSqlConsoleDialog);
keyboardActionService.setGlobalActionHandler("ShowSQLConsole", showSqlConsoleDialog);
const showBackendLogDialog = () => import(BACKEND_LOG).then(d => d.showDialog());
$("#show-backend-log-button").on('click', showBackendLogDialog);
keyboardActionService.setGlobalActionHandler("ShowBackendLog", showBackendLogDialog);
$("#show-about-dialog-button").on('click', () => import(ABOUT).then(d => d.showDialog()));
if (utils.isElectron()) {
@@ -127,6 +132,12 @@ function registerEntrypoints() {
$("#open-dev-tools-button").toggle(utils.isElectron());
keyboardActionService.setGlobalActionHandler("PasteMarkdownIntoText", async () => {
const dialog = await import("../dialogs/markdown_import.js");
dialog.importMarkdownInline();
});
if (utils.isElectron()) {
const openDevTools = () => {
require('electron').remote.getCurrentWindow().toggleDevTools();
@@ -382,6 +382,17 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
* @param {function} handler
*/
this.bindGlobalShortcut = utils.bindGlobalShortcut;
/**
* Trilium runs in backend and frontend process, when something is changed on the backend from script,
* frontend will get asynchronously synchronized.
*
* This method returns a promise which resolves once all the backend -> frontend synchronization is finished.
* Typical use case is when new note has been created, we should wait until it is synced into frontend and only then activate it.
*
* @method
*/
this.waitUntilSynced = ws.waitForMaxKnownSyncId;
}
export default FrontendScriptApi;
@@ -55,18 +55,21 @@ class NoteDetailCode {
this.onNoteChange(() => this.ctx.noteChanged());
}
// CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check)
// we provide fallback
this.codeEditor.setValue(this.ctx.note.content || "");
// lazy loading above can take time and tab might have been already switched to another note
if (this.ctx.note && this.ctx.note.type === 'code') {
// CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check)
// we provide fallback
this.codeEditor.setValue(this.ctx.note.content || "");
const info = CodeMirror.findModeByMIME(this.ctx.note.mime);
const info = CodeMirror.findModeByMIME(this.ctx.note.mime);
if (info) {
this.codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(this.codeEditor, info.mode);
if (info) {
this.codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(this.codeEditor, info.mode);
}
this.show();
}
this.show();
}
show() {
@@ -238,14 +238,17 @@ class NoteDetailRelationMap {
await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP);
this.loadMapData();
jsPlumb.ready(() => {
this.initJsPlumbInstance();
// lazy loading above can take time and tab might have been already switched to another note
if (this.ctx.note && this.ctx.note.type === 'relation-map') {
this.loadMapData();
this.initPanZoom();
this.initJsPlumbInstance();
this.loadNotesAndRelations();
this.initPanZoom();
this.loadNotesAndRelations();
}
});
}
@@ -3,6 +3,40 @@ import treeService from './tree.js';
import noteAutocompleteService from './note_autocomplete.js';
import mimeTypesService from './mime_types.js';
const mentionSetup = {
feeds: [
{
marker: '@',
feed: queryText => {
return new Promise((res, rej) => {
noteAutocompleteService.autocompleteSource(queryText, rows => {
if (rows.length === 1 && rows[0].title === 'No results') {
rows = [];
}
for (const row of rows) {
row.text = row.name = row.noteTitle;
row.id = '@' + row.text;
row.link = '#' + row.path;
}
res(rows);
});
});
},
itemRenderer: item => {
const itemElement = document.createElement('span');
itemElement.classList.add('mentions-item');
itemElement.innerHTML = `${item.highlightedTitle} `;
return itemElement;
},
minimumCharacters: 0
}
]
};
class NoteDetailText {
/**
* @param {TabContext} ctx
@@ -34,6 +68,16 @@ class NoteDetailText {
if (!this.textEditor) {
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
const codeBlockLanguages =
(await mimeTypesService.getMimeTypes())
.filter(mt => mt.enabled)
.map(mt => {
return {
language: mt.mime.toLowerCase().replace(/[\W_]+/g,"-"),
label: mt.title
}
});
// CKEditor since version 12 needs the element to be visible before initialization. At the same time
// we want to avoid flicker - i.e. show editor only once everything is ready. That's why we have separate
// display of $component in both branches.
@@ -42,53 +86,9 @@ class NoteDetailText {
// textEditor might have been initialized during previous await so checking again
// looks like double initialization can freeze CKEditor pretty badly
if (!this.textEditor) {
const codeBlockLanguages =
(await mimeTypesService.getMimeTypes())
.filter(mt => mt.enabled)
.map(mt => {
return {
language: mt.mime.toLowerCase().replace(/[\W_]+/g,"-"),
label: mt.title
}
});
this.textEditor = await BalloonEditor.create(this.$editorEl[0], {
placeholder: "Type the content of your note here ...",
mention: {
feeds: [
{
marker: '@',
feed: queryText => {
return new Promise((res, rej) => {
noteAutocompleteService.autocompleteSource(queryText, rows => {
if (rows.length === 1 && rows[0].title === 'No results') {
rows = [];
}
for (const row of rows) {
row.text = row.name = row.noteTitle;
row.id = '@' + row.text;
row.link = '#' + row.path;
}
console.log(rows.slice(0, Math.min(5, rows.length)));
res(rows);
});
});
},
itemRenderer: item => {
const itemElement = document.createElement('span');
itemElement.classList.add('mentions-item');
itemElement.innerHTML = `${item.highlightedTitle} `;
return itemElement;
},
minimumCharacters: 0
}
]
},
mention: mentionSetup,
codeBlock: {
languages: codeBlockLanguages
}
@@ -98,11 +98,14 @@ class NoteDetailText {
}
}
this.textEditor.isReadOnly = await this.isReadOnly();
// lazy loading above can take time and tab might have been already switched to another note
if (this.ctx.note && this.ctx.note.type === 'text') {
this.textEditor.isReadOnly = await this.isReadOnly();
this.$component.show();
this.$component.show();
this.textEditor.setData(this.ctx.note.content);
this.textEditor.setData(this.ctx.note.content);
}
}
getContent() {
+58 -24
View File
@@ -42,12 +42,16 @@ async function remove(url, headers = {}) {
let i = 1;
const reqResolves = {};
let maxKnownSyncId = 0;
async function call(method, url, data, headers = {}) {
let resp;
if (utils.isElectron()) {
const ipc = require('electron').ipcRenderer;
const requestId = i++;
return new Promise((resolve, reject) => {
resp = await new Promise((resolve, reject) => {
reqResolves[requestId] = resolve;
if (REQUEST_LOGGING_ENABLED) {
@@ -64,32 +68,58 @@ async function call(method, url, data, headers = {}) {
});
}
else {
return await ajax(url, method, data, headers);
resp = await ajax(url, method, data, headers);
}
const maxSyncIdStr = resp.headers['trilium-max-sync-id'];
if (maxSyncIdStr && maxSyncIdStr.trim()) {
maxKnownSyncId = Math.max(maxKnownSyncId, parseInt(maxSyncIdStr));
}
return resp.body;
}
async function ajax(url, method, data, headers) {
const options = {
url: baseApiUrl + url,
type: method,
headers: getHeaders(headers),
timeout: 60000
};
function ajax(url, method, data, headers) {
return new Promise((res, rej) => {
const options = {
url: baseApiUrl + url,
type: method,
headers: getHeaders(headers),
timeout: 60000,
success: (body, textStatus, jqXhr) => {
const respHeaders = {};
if (data) {
try {
options.data = JSON.stringify(data);
}
catch (e) {
console.log("Can't stringify data: ", data, " because of error: ", e)
}
options.contentType = "application/json";
}
jqXhr.getAllResponseHeaders().trim().split(/[\r\n]+/).forEach(line => {
const parts = line.split(': ');
const header = parts.shift();
respHeaders[header] = parts.join(': ');
});
return await $.ajax(options).catch(e => {
const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText;
toastService.showError(message);
toastService.throwError(message);
res({
body,
headers: respHeaders
});
},
error: (jqXhr, textStatus, error) => {
const message = "Error when calling " + method + " " + url + ": " + textStatus + " - " + error;
toastService.showError(message);
toastService.throwError(message);
rej(error);
}
};
if (data) {
try {
options.data = JSON.stringify(data);
} catch (e) {
console.log("Can't stringify data: ", data, " because of error: ", e)
}
options.contentType = "application/json";
}
$.ajax(options);
});
}
@@ -101,7 +131,10 @@ if (utils.isElectron()) {
console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode);
}
reqResolves[arg.requestId](arg.body);
reqResolves[arg.requestId]({
body: arg.body,
headers: arg.headers
});
delete reqResolves[arg.requestId];
});
@@ -114,5 +147,6 @@ export default {
remove,
ajax,
// don't remove, used from CKEditor image upload!
getHeaders
getHeaders,
getMaxKnownSyncId: () => maxKnownSyncId
};
@@ -65,6 +65,11 @@ async function getIcon(note) {
async function prepareNode(branch) {
const note = await branch.getNote();
if (!note) {
console.log(`Branch has no note: ${branch}`);
}
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
@@ -161,6 +161,11 @@ class TreeCache {
/** @return {Promise<NoteShort>} */
async getNote(noteId, silentNotFoundError = false) {
if (noteId === 'none') {
console.log(`No 'none' note.`);
return null;
}
else if (!noteId) {
console.log(`Falsy noteId ${noteId}, returning null.`);
return null;
}
+9 -3
View File
@@ -1,5 +1,6 @@
import utils from './utils.js';
import toastService from "./toast.js";
import server from "./server.js";
const $outstandingSyncsCount = $("#outstanding-syncs-count");
@@ -71,8 +72,6 @@ async function handleMessage(event) {
// finish and set to null to signal somebody else can pick it up
consumeQueuePromise = null;
}
checkSyncIdListeners();
}
else if (message.type === 'sync-hash-check-failed') {
toastService.showError("Sync check failed!", 60000);
@@ -98,6 +97,10 @@ function waitForSyncId(desiredSyncId) {
});
}
function waitForMaxKnownSyncId() {
return waitForSyncId(server.getMaxKnownSyncId());
}
function checkSyncIdListeners() {
syncIdReachedListeners
.filter(l => l.desiredSyncId <= lastProcessedSyncId)
@@ -129,6 +132,8 @@ async function consumeSyncData() {
lastProcessedSyncId = Math.max(lastProcessedSyncId, allSyncData[allSyncData.length - 1].id);
}
checkSyncIdListeners();
}
function connectWebSocket() {
@@ -193,5 +198,6 @@ export default {
subscribeToMessages,
subscribeToAllSyncMessages,
subscribeToOutsideSyncMessages,
waitForSyncId
waitForSyncId,
waitForMaxKnownSyncId
};
+1 -1
View File
@@ -794,7 +794,7 @@ div[data-notify="container"] {
padding: 2px;
}
#sql-console-tables button {
#sql-console-table-schemas button {
padding: 0.25rem 0.4rem;
font-size: 0.875rem;
line-height: 0.5;
+15
View File
@@ -0,0 +1,15 @@
"use strict";
const fs = require('fs');
const dateUtils = require('../../services/date_utils');
const {LOG_DIR} = require('../../services/data_dir.js');
async function getBackendLog() {
const file = `${LOG_DIR}/trilium-${dateUtils.localNowDate()}.log`;
return fs.readFileSync(file, 'utf8');
}
module.exports = {
getBackendLog
};
+7 -1
View File
@@ -2,6 +2,7 @@
const sql = require('../../services/sql');
const log = require('../../services/log');
const consistencyChecksService = require('../../services/consistency_checks');
async function vacuumDatabase() {
await sql.execute("VACUUM");
@@ -9,6 +10,11 @@ async function vacuumDatabase() {
log.info("Database has been vacuumed.");
}
async function findAndFixConsistencyIssues() {
await consistencyChecksService.runOnDemandChecks(true);
}
module.exports = {
vacuumDatabase
vacuumDatabase,
findAndFixConsistencyIssues
};
+4 -2
View File
@@ -34,7 +34,8 @@ async function addClipping(req) {
clippingNote = (await noteService.createNewNote({
parentNoteId: todayNote.noteId,
title: title,
content: ''
content: '',
type: 'text'
})).note;
await clippingNote.setLabel('clipType', 'clippings');
@@ -58,7 +59,8 @@ async function createNote(req) {
const {note} = await noteService.createNewNote({
parentNoteId: todayNote.noteId,
title,
content
content,
type: 'text'
});
await note.setLabel('clipType', clipType);
+12 -2
View File
@@ -17,12 +17,22 @@ async function getSchema() {
}
async function execute(req) {
const query = req.body.query;
const queries = req.body.query.split("\n---");
try {
const results = [];
for (const query of queries) {
if (!query.trim()) {
continue;
}
results.push(await sql.getRows(query));
}
return {
success: true,
rows: await sql.getRows(query)
results
};
}
catch (e) {
+7 -2
View File
@@ -12,10 +12,14 @@ function init(app) {
}
};
const respHeaders = {};
const res = {
statusCode: 200,
getHeader: () => {},
setHeader: () => {},
getHeader: name => respHeaders[name],
setHeader: (name, value) => {
respHeaders[name] = value.toString();
},
status: statusCode => {
res.statusCode = statusCode;
return res;
@@ -24,6 +28,7 @@ function init(app) {
event.sender.send('server-response', {
requestId: arg.requestId,
statusCode: res.statusCode,
headers: respHeaders,
body: obj
});
}
+8 -1
View File
@@ -35,6 +35,7 @@ const linkMapRoute = require('./api/link_map');
const clipperRoute = require('./api/clipper');
const similarNotesRoute = require('./api/similar_notes');
const keysRoute = require('./api/keys');
const backendLogRoute = require('./api/backend_log');
const log = require('../services/log');
const express = require('express');
@@ -43,6 +44,7 @@ const auth = require('../services/auth');
const cls = require('../services/cls');
const sql = require('../services/sql');
const protectedSessionService = require('../services/protected_session');
const syncTableService = require('../services/sync_table');
const csurf = require('csurf');
const csrfMiddleware = csurf({
@@ -51,6 +53,8 @@ const csrfMiddleware = csurf({
});
function apiResultHandler(req, res, result) {
res.setHeader('trilium-max-sync-id', syncTableService.getMaxSyncId());
// if it's an array and first element is integer then we consider this to be [statusCode, response] format
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [statusCode, response] = result;
@@ -211,10 +215,11 @@ function register(app) {
apiRoute(POST, '/api/sql/execute', sqlRoute.execute);
apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
apiRoute(POST, '/api/cleanup/cleanup-unused-images', cleanupRoute.cleanupUnusedImages);
// VACUUM requires execution outside of transaction
route(POST, '/api/cleanup/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.vacuumDatabase, apiResultHandler, false);
route(POST, '/api/cleanup/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.findAndFixConsistencyIssues, apiResultHandler, false);
apiRoute(POST, '/api/script/exec', scriptRoute.exec);
apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run);
apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles);
@@ -247,6 +252,8 @@ function register(app) {
apiRoute(GET, '/api/keyboard-actions', keysRoute.getKeyboardActions);
apiRoute(GET, '/api/keyboard-shortcuts-for-notes', keysRoute.getShortcutsForNotes);
apiRoute(GET, '/api/backend-log', backendLogRoute.getBackendLog);
app.use('', router);
}
+1 -1
View File
@@ -1 +1 @@
module.exports = { buildDate:"2019-12-04T22:40:31+01:00", buildRevision: "e0aabe4f9cb37b68dd70bbba6eae5dcc5893f075" };
module.exports = { buildDate:"2019-12-10T23:09:02+01:00", buildRevision: "76f5736255251afe0e94b07236dc5c183384242f" };
+5
View File
@@ -113,6 +113,11 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
condition = "NOT(" + condition + ")";
}
if (['text', 'title', 'content'].includes(filter.name)) {
// for title/content search does not make sense to search for protected notes
condition = `(${condition} AND notes.isProtected = 0)`;
}
where += condition;
}
else if ([">", ">=", "<", "<="].includes(filter.operator)) {
File diff suppressed because it is too large Load Diff
+10
View File
@@ -225,6 +225,11 @@ const DEFAULT_KEYBOARD_ACTIONS = [
defaultShortcuts: ["Alt+O"],
description: "Shows SQL Console dialog"
},
{
actionName: "ShowBackendLog",
defaultShortcuts: [],
description: "Shows Backend Log dialog"
},
{
actionName: "ShowHelp",
defaultShortcuts: ["F1"],
@@ -245,6 +250,11 @@ const DEFAULT_KEYBOARD_ACTIONS = [
actionName: "InsertDateTimeToText",
defaultShortcuts: ["Alt+T"]
},
{
actionName: "PasteMarkdownIntoText",
defaultShortcuts: [],
description: "Pastes Markdown from clipboard into text note"
},
{
separator: "Other"
+2 -1
View File
@@ -82,7 +82,8 @@ const defaultOptions = [
{ name: 'hideTabRowForOneTab', value: 'false', isSynced: false },
{ name: 'imageMaxWidthHeight', value: '1200', isSynced: true },
{ name: 'imageJpegQuality', value: '75', isSynced: true },
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false }
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false },
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-swift","text/xml","text/x-yaml"]', isSynced: true }
];
async function initStartupOptions() {
+1 -1
View File
@@ -1,6 +1,6 @@
const dayjs = require("dayjs");
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\w_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([\w_/-]+|"[^"]+"))?/ig;
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([\p{L}_/-]+|"[^"]+"))?/igu;
const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i;
function calculateSmartValue(v) {
+6 -1
View File
@@ -21,6 +21,10 @@ async function addEntitySync(entityName, entityId, sourceId) {
setTimeout(() => require('./ws').sendPingToAllClients(), 50);
}
function getMaxSyncId() {
return syncs.length === 0 ? 0 : syncs[syncs.length - 1].id;
}
function getEntitySyncsNewerThan(syncId) {
return syncs.filter(s => s.id > syncId);
}
@@ -96,5 +100,6 @@ module.exports = {
addApiTokenSync: async (apiTokenId, sourceId) => await addEntitySync("api_tokens", apiTokenId, sourceId),
addEntitySync,
fillAllSyncRows,
getEntitySyncsNewerThan
getEntitySyncsNewerThan,
getMaxSyncId
};
+7
View File
@@ -81,6 +81,12 @@
<kbd data-kb-action="ShowSQLConsole"></kbd>
</a>
<a class="dropdown-item" id="show-backend-log-button">
<span class="bx bx-empty"></span>
Show backend log
<kbd data-kb-action="ShowBackendLog"></kbd>
</a>
<a class="dropdown-item" id="reload-frontend-button" title="Reload can help with some visual glitches without restarting the whole app.">
<span class="bx bx-empty"></span>
Reload frontend
@@ -187,6 +193,7 @@
<% include dialogs/link_map.ejs %>
<% include dialogs/clone_to.ejs %>
<% include dialogs/move_to.ejs %>
<% include dialogs/backend_log.ejs %>
</div>
<script type="text/javascript">
+18
View File
@@ -0,0 +1,18 @@
<div id="backend-log-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Backend log</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<textarea id="backend-log-textarea" readonly="readonly" style="min-height: 600px; width: 100%;"></textarea>
</div>
<div class="modal-footer">
<button id="refresh-backend-log-button" class="btn btn-primary">Refresh</button>
</div>
</div>
</div>
</div>
+3 -7
View File
@@ -1,5 +1,5 @@
<div id="sql-console-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" style="max-width: 1000px;" role="document">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">SQL console</h5>
@@ -10,7 +10,7 @@
<div class="modal-body">
<div>
Tables:
<span id="sql-console-tables"></span>
<span id="sql-console-table-schemas"></span>
</div>
<div id="sql-console-query"></div>
@@ -19,11 +19,7 @@
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
</div>
<div style="width: 100%; overflow: auto;"> <!-- This is necessary for the table to be scrollable -->
<table id="sql-console-results" class="table table-striped">
<thead></thead>
<tbody></tbody>
</table>
<div id="result-container" style="width: 100%; font-size: smaller; margin-top: 10px;">
</div>
</div>
</div>
+1
View File
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Trilium Notes</title>
<link rel="apple-touch-icon" sizes="180x180" href="images/app-icons/ios/apple-touch-icon.png">
+22 -27
View File
@@ -74,7 +74,28 @@ async function startTrilium() {
httpServer.keepAliveTimeout = 120000 * 5;
httpServer.listen(usedPort, usedHost);
httpServer.on('error', onError);
httpServer.on('error', error => {
if (error.syscall !== 'listen') {
throw error;
}
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(`Port ${usedPort} requires elevated privileges`);
process.exit(1);
break;
case 'EADDRINUSE':
console.error(`Port ${usedPort} is already in use`);
process.exit(1);
break;
default:
throw error;
}
}
);
httpServer.on('listening', () => debug('Listening on port' + httpServer.address().port));
sqlInit.dbReady.then(() => ws.init(httpServer, sessionParser));
@@ -86,29 +107,3 @@ async function startTrilium() {
}
startTrilium();
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error('Port requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error('Port is already in use');
process.exit(1);
break;
default:
throw error;
}
}