Merge remote-tracking branch 'origin/stable'

This commit is contained in:
zadam 2022-02-10 23:40:18 +01:00
commit 67cce5f817
14 changed files with 127 additions and 82 deletions

View File

@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.50.1", "version": "0.50.2",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {

View File

@ -42,7 +42,9 @@ class EtapiToken extends AbstractEntity {
/** @type {boolean} */ /** @type {boolean} */
this.isDeleted = !!row.isDeleted; this.isDeleted = !!row.isDeleted;
this.becca.etapiTokens[this.etapiTokenId] = this; if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
} }
init() { init() {

View File

@ -48,46 +48,46 @@ const TPL = `
export default class EtapiOptions { export default class EtapiOptions {
constructor() { constructor() {
$("#options-etapi").html(TPL); $("#options-etapi").html(TPL);
$("#create-etapi-token").on("click", async () => { $("#create-etapi-token").on("click", async () => {
const promptDialog = await import('../../dialogs/prompt.js'); const promptDialog = await import('../../dialogs/prompt.js');
const tokenName = await promptDialog.ask({ const tokenName = await promptDialog.ask({
title: "New ETAPI token", title: "New ETAPI token",
message: "Please enter new token's name", message: "Please enter new token's name",
defaultValue: "new token" defaultValue: "new token"
}); });
if (!tokenName.trim()) { if (!tokenName.trim()) {
alert("Token name can't be empty"); alert("Token name can't be empty");
return; return;
} }
const {token} = await server.post('etapi-tokens', {tokenName});
await promptDialog.ask({ const {authToken} = await server.post('etapi-tokens', {tokenName});
await promptDialog.ask({
title: "ETAPI token created", title: "ETAPI token created",
message: 'Copy the created token into clipboard. Trilium stores the token hashed and this is the last time you see it.', message: 'Copy the created token into clipboard. Trilium stores the token hashed and this is the last time you see it.',
defaultValue: token defaultValue: authToken
}); });
this.refreshTokens(); this.refreshTokens();
}); });
this.refreshTokens(); this.refreshTokens();
} }
async refreshTokens() { async refreshTokens() {
const $noTokensYet = $("#no-tokens-yet"); const $noTokensYet = $("#no-tokens-yet");
const $tokensTable = $("#tokens-table"); const $tokensTable = $("#tokens-table");
const tokens = await server.get('etapi-tokens'); const tokens = await server.get('etapi-tokens');
$noTokensYet.toggle(tokens.length === 0); $noTokensYet.toggle(tokens.length === 0);
$tokensTable.toggle(tokens.length > 0); $tokensTable.toggle(tokens.length > 0);
const $tokensTableBody = $tokensTable.find("tbody"); const $tokensTableBody = $tokensTable.find("tbody");
$tokensTableBody.empty(); $tokensTableBody.empty();
for (const token of tokens) { for (const token of tokens) {
$tokensTableBody.append( $tokensTableBody.append(
$("<tr>") $("<tr>")
@ -112,7 +112,7 @@ export default class EtapiOptions {
}); });
await server.patch(`etapi-tokens/${etapiTokenId}`, {name: tokenName}); await server.patch(`etapi-tokens/${etapiTokenId}`, {name: tokenName});
this.refreshTokens(); this.refreshTokens();
} }

View File

@ -13,9 +13,9 @@ let shownCb;
export function ask({ title, message, defaultValue, shown }) { export function ask({ title, message, defaultValue, shown }) {
shownCb = shown; shownCb = shown;
$("#prompt-title").text(title || "Prompt"); $("#prompt-title").text(title || "Prompt");
$question = $("<label>") $question = $("<label>")
.prop("for", "prompt-dialog-answer") .prop("for", "prompt-dialog-answer")
.text(message); .text(message);
@ -51,7 +51,8 @@ $dialog.on("hidden.bs.modal", () => {
} }
}); });
$form.on('submit', () => { $form.on('submit', e => {
e.preventDefault();
resolve($answer.val()); resolve($answer.val());
$dialog.modal('hide'); $dialog.modal('hide');

View File

@ -7,11 +7,14 @@ import treeService from "./tree.js";
import utils from "./utils.js"; import utils from "./utils.js";
import NoteContext from "./note_context.js"; import NoteContext from "./note_context.js";
import appContext from "./app_context.js"; import appContext from "./app_context.js";
import Mutex from "../utils/mutex.js";
export default class TabManager extends Component { export default class TabManager extends Component {
constructor() { constructor() {
super(); super();
this.mutex = new Mutex();
this.activeNtxId = null; this.activeNtxId = null;
// elements are arrays of note contexts for each tab (one main context + subcontexts [splits]) // elements are arrays of note contexts for each tab (one main context + subcontexts [splits])
@ -292,51 +295,55 @@ export default class TabManager extends Component {
} }
async removeNoteContext(ntxId) { async removeNoteContext(ntxId) {
const noteContextToRemove = this.getNoteContextById(ntxId); // removing note context is async process which can take some time, if users presses CTRL-W quickly, two
// close events could interleave which would then lead to attempting to activate already removed context.
await this.mutex.runExclusively(async () => {
const noteContextToRemove = this.getNoteContextById(ntxId);
if (noteContextToRemove.isMainContext()) { if (noteContextToRemove.isMainContext()) {
// forbid removing last main note context // forbid removing last main note context
// this was previously allowed (was replaced with empty tab) but this proved to be prone to race conditions // this was previously allowed (was replaced with empty tab) but this proved to be prone to race conditions
const mainNoteContexts = this.getNoteContexts().filter(nc => nc.isMainContext()); const mainNoteContexts = this.getNoteContexts().filter(nc => nc.isMainContext());
if (mainNoteContexts.length === 1) { if (mainNoteContexts.length === 1) {
mainNoteContexts[0].setEmpty(); mainNoteContexts[0].setEmpty();
return; return;
}
} }
}
// close dangling autocompletes after closing the tab // close dangling autocompletes after closing the tab
$(".aa-input").autocomplete("close"); $(".aa-input").autocomplete("close");
const noteContextsToRemove = noteContextToRemove.getSubContexts(); const noteContextsToRemove = noteContextToRemove.getSubContexts();
const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId); const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId);
await this.triggerEvent('beforeTabRemove', { ntxIds: ntxIdsToRemove }); await this.triggerEvent('beforeTabRemove', { ntxIds: ntxIdsToRemove });
if (!noteContextToRemove.isMainContext()) { if (!noteContextToRemove.isMainContext()) {
await this.activateNoteContext(noteContextToRemove.getMainContext().ntxId); await this.activateNoteContext(noteContextToRemove.getMainContext().ntxId);
}
else if (this.mainNoteContexts.length <= 1) {
await this.openAndActivateEmptyTab();
}
else if (ntxIdsToRemove.includes(this.activeNtxId)) {
const idx = this.mainNoteContexts.findIndex(nc => nc.ntxId === noteContextToRemove.ntxId);
if (idx === this.mainNoteContexts.length - 1) {
await this.activatePreviousTabCommand();
} }
else { else if (this.mainNoteContexts.length <= 1) {
await this.activateNextTabCommand(); await this.openAndActivateEmptyTab();
} }
} else if (ntxIdsToRemove.includes(this.activeNtxId)) {
const idx = this.mainNoteContexts.findIndex(nc => nc.ntxId === noteContextToRemove.ntxId);
this.children = this.children.filter(nc => !ntxIdsToRemove.includes(nc.ntxId)); if (idx === this.mainNoteContexts.length - 1) {
await this.activatePreviousTabCommand();
}
else {
await this.activateNextTabCommand();
}
}
this.recentlyClosedTabs.push(noteContextsToRemove); this.children = this.children.filter(nc => !ntxIdsToRemove.includes(nc.ntxId));
this.triggerEvent('noteContextRemoved', {ntxIds: ntxIdsToRemove}); this.recentlyClosedTabs.push(noteContextsToRemove);
this.tabsUpdate.scheduleUpdate(); this.triggerEvent('noteContextRemoved', {ntxIds: ntxIdsToRemove});
this.tabsUpdate.scheduleUpdate();
});
} }
tabReorderEvent({ntxIdsInOrder}) { tabReorderEvent({ntxIdsInOrder}) {

View File

@ -65,7 +65,7 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
if (!parents.length) { if (!parents.length) {
if (logErrors) { if (logErrors) {
ws.logError(`No parents found for ${childNoteId} (${child.title}) for path ${notePath}`); ws.logError(`No parents found for note ${childNoteId} (${child.title}) for path ${notePath}`);
} }
return; return;

View File

@ -0,0 +1,28 @@
export default class Mutex {
constructor() {
this.current = Promise.resolve();
}
/** @returns {Promise} */
lock() {
let resolveFun;
const subPromise = new Promise(resolve => resolveFun = () => resolve());
// Caller gets a promise that resolves when the current outstanding lock resolves
const newPromise = this.current.then(() => resolveFun);
// Don't allow the next request until the new promise is done
this.current = subPromise;
// Return the new promise
return newPromise;
};
async runExclusively(cb) {
const unlock = await this.lock();
try {
await cb();
}
finally {
unlock();
}
}
}

View File

@ -91,6 +91,8 @@ body {
--ck-color-table-focused-cell-background: var(--more-accented-background-color); --ck-color-table-focused-cell-background: var(--more-accented-background-color);
--ck-color-labeled-field-label-background: var(--accented-background-color);
/* todo lists */ /* todo lists */
--ck-color-todo-list-checkmark-border: var(--main-border-color); --ck-color-todo-list-checkmark-border: var(--main-border-color);

View File

@ -2,16 +2,14 @@ const etapiTokenService = require("../../services/etapi_tokens");
function getTokens() { function getTokens() {
const tokens = etapiTokenService.getTokens(); const tokens = etapiTokenService.getTokens();
tokens.sort((a, b) => a.utcDateCreated < b.utcDateCreated ? -1 : 1); tokens.sort((a, b) => a.utcDateCreated < b.utcDateCreated ? -1 : 1);
return tokens; return tokens;
} }
function createToken(req) { function createToken(req) {
return { return etapiTokenService.createToken(req.body.tokenName);
authToken: etapiTokenService.createToken(req.body.tokenName)
};
} }
function patchToken(req) { function patchToken(req) {
@ -27,4 +25,4 @@ module.exports = {
createToken, createToken,
patchToken, patchToken,
deleteToken deleteToken
}; };

View File

@ -1 +1 @@
module.exports = { buildDate:"2022-02-02T21:38:21+01:00", buildRevision: "0917fc8be171253449219cf29c0e603ac29eb26e" }; module.exports = { buildDate:"2022-02-09T22:52:36+01:00", buildRevision: "23daaa2387a0655685377f0a541d154aeec2aae8" };

View File

@ -19,7 +19,7 @@ function createToken(tokenName) {
name: tokenName, name: tokenName,
tokenHash tokenHash
}).save(); }).save();
return { return {
authToken: `${etapiToken.etapiTokenId}_${token}` authToken: `${etapiToken.etapiTokenId}_${token}`
}; };
@ -29,14 +29,14 @@ function parseAuthToken(auth) {
if (!auth) { if (!auth) {
return null; return null;
} }
const chunks = auth.split("_"); const chunks = auth.split("_");
if (chunks.length === 1) { if (chunks.length === 1) {
return { token: auth }; // legacy format without etapiTokenId return { token: auth }; // legacy format without etapiTokenId
} }
else if (chunks.length === 2) { else if (chunks.length === 2) {
return { return {
etapiTokenId: chunks[0], etapiTokenId: chunks[0],
token: chunks[1] token: chunks[1]
} }
@ -48,20 +48,20 @@ function parseAuthToken(auth) {
function isValidAuthHeader(auth) { function isValidAuthHeader(auth) {
const parsed = parseAuthToken(auth); const parsed = parseAuthToken(auth);
if (!parsed) { if (!parsed) {
return false; return false;
} }
const authTokenHash = getTokenHash(parsed.token); const authTokenHash = getTokenHash(parsed.token);
if (parsed.etapiTokenId) { if (parsed.etapiTokenId) {
const etapiToken = becca.getEtapiToken(parsed.etapiTokenId); const etapiToken = becca.getEtapiToken(parsed.etapiTokenId);
if (!etapiToken) { if (!etapiToken) {
return false; return false;
} }
return etapiToken.tokenHash === authTokenHash; return etapiToken.tokenHash === authTokenHash;
} }
else { else {
@ -70,31 +70,30 @@ function isValidAuthHeader(auth) {
return true; return true;
} }
} }
return false; return false;
} }
} }
function renameToken(etapiTokenId, newName) { function renameToken(etapiTokenId, newName) {
const etapiToken = becca.getEtapiToken(etapiTokenId); const etapiToken = becca.getEtapiToken(etapiTokenId);
if (!etapiToken) { if (!etapiToken) {
throw new Error(`Token ${etapiTokenId} does not exist`); throw new Error(`Token ${etapiTokenId} does not exist`);
} }
etapiToken.name = newName; etapiToken.name = newName;
etapiToken.save(); etapiToken.save();
} }
function deleteToken(etapiTokenId) { function deleteToken(etapiTokenId) {
const etapiToken = becca.getEtapiToken(etapiTokenId); const etapiToken = becca.getEtapiToken(etapiTokenId);
if (!etapiToken) { if (!etapiToken) {
return; // ok, already deleted return; // ok, already deleted
} }
etapiToken.isDeleted = true; etapiToken.markAsDeletedSimple();
etapiToken.save();
} }
module.exports = { module.exports = {
@ -104,4 +103,4 @@ module.exports = {
deleteToken, deleteToken,
parseAuthToken, parseAuthToken,
isValidAuthHeader isValidAuthHeader
}; };

View File

@ -13,6 +13,7 @@ const fs = require("fs");
const becca = require("../../becca/becca"); const becca = require("../../becca/becca");
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
const archiver = require('archiver'); const archiver = require('archiver');
const log = require("../log");
/** /**
* @param {TaskContext} taskContext * @param {TaskContext} taskContext
@ -54,7 +55,7 @@ function exportToZip(taskContext, branch, format, res) {
let existingExtension = path.extname(fileName).toLowerCase(); let existingExtension = path.extname(fileName).toLowerCase();
let newExtension; let newExtension;
if (fileName.length > 30) { if (fileName.length > 30) {
fileName = fileName.substr(0, 30); fileName = fileName.substr(0, 30);
} }
@ -254,7 +255,9 @@ ${content}
</html>`; </html>`;
} }
return html.prettyPrint(content, {indent_size: 2}); return content.length < 100000
? html.prettyPrint(content, {indent_size: 2})
: content;
} }
else if (noteMeta.format === 'markdown') { else if (noteMeta.format === 'markdown') {
let markdownContent = mdService.toMarkdown(content); let markdownContent = mdService.toMarkdown(content);
@ -274,6 +277,8 @@ ${content}
const notePaths = {}; const notePaths = {};
function saveNote(noteMeta, filePathPrefix) { function saveNote(noteMeta, filePathPrefix) {
log.info(`Exporting note ${noteMeta.noteId}`);
if (noteMeta.isClone) { if (noteMeta.isClone) {
const targetUrl = getTargetUrl(noteMeta.noteId, noteMeta); const targetUrl = getTargetUrl(noteMeta.noteId, noteMeta);

View File

@ -634,7 +634,10 @@ function undeleteBranch(branchId, deleteId, taskContext) {
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
if (note.isDeleted && note.deleteId === deleteId) { if (note.isDeleted && note.deleteId === deleteId) {
new Note(note).save(); // becca entity was already created as skeleton in "new Branch()" above
const noteEntity = becca.getNote(note.noteId);
noteEntity.updateFromRow(note);
noteEntity.save();
const attributes = sql.getRows(` const attributes = sql.getRows(`
SELECT * FROM attributes SELECT * FROM attributes

View File

@ -1,5 +1,5 @@
<div id="prompt-dialog" class="modal mx-auto" tabindex="-1" role="dialog"> <div id="prompt-dialog" class="modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<form id="prompt-dialog-form"> <form id="prompt-dialog-form">
<div class="modal-header"> <div class="modal-header">