mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
Merge remote-tracking branch 'origin/stable'
This commit is contained in:
commit
67cce5f817
@ -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": {
|
||||||
|
@ -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() {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
@ -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}) {
|
||||||
|
@ -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;
|
||||||
|
28
src/public/app/utils/mutex.js
Normal file
28
src/public/app/utils/mutex.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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" };
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user