Compare commits

...

22 Commits

Author SHA1 Message Date
zadam
1f3d73b9fd release 0.45.7 2020-12-22 20:21:15 +01:00
zadam
bdfd760b9d fixed some encryption issues 2020-12-21 23:19:03 +01:00
zadam
7133e60267 make encryption more robust in face of null values, #1483 2020-12-21 22:08:55 +01:00
zadam
fc4edf4aa7 better comment for instanceName 2020-12-21 21:05:34 +01:00
zadam
eaf93a70cd fix inverse relation creation, closes #1498 2020-12-21 20:55:01 +01:00
zadam
b093569ec5 increase toast size limit 2020-12-18 21:23:51 +01:00
zadam
4633c68a0c avoid resorting children on every child add, fixes #1480 2020-12-10 16:10:10 +01:00
zadam
33571e0ef3 better logging for un/protect errors 2020-12-09 22:49:55 +01:00
zadam
31876d2cf9 fix automatically scheduled note deletion 2020-12-09 22:45:34 +01:00
zadam
81c6043cb6 fix printing notes with math, closes #1484 2020-12-09 21:59:30 +01:00
zadam
1982d054ef inherit also note type and mime from template note, closes #1475 2020-12-07 09:35:39 +01:00
zadam
e56979c482 add button to erase deleted notes now into the options 2020-12-06 22:11:49 +01:00
zadam
58555b3660 release 0.45.6 2020-12-04 22:08:24 +01:00
zadam
b7b1324dd0 fixed disabled prefix in case of unsafe import to conform to new attribute name pattern constraints 2020-12-04 22:05:29 +01:00
zadam
e318acc977 fix incorrectly set isInheritable on inherited attrs 2020-11-27 22:33:33 +01:00
zadam
8ae82f5b69 fix "open in new tab" in tree context menu 2020-11-24 23:18:53 +01:00
zadam
26442f418a fix "open in new window" link context menu 2020-11-24 23:06:37 +01:00
zadam
23a432e7d8 don't show imageLinks in link map when they are connecting parent (text note) and child (image), closes #1461 2020-11-24 20:12:49 +01:00
zadam
984ecaf99c show again the table handle and type around 2020-11-23 20:56:14 +01:00
zadam
21b73a86b2 show also keyboard shortcut for duplicateSubtree in context menu 2020-11-23 20:17:53 +01:00
zadam
7d8277699c add keyboard shortcut for duplicate subtree, #1451 2020-11-23 19:44:49 +01:00
zadam
928ed7a034 add keyboard shortcut for include note, closes #1410 2020-11-22 22:44:06 +01:00
28 changed files with 181 additions and 63 deletions

View File

@@ -1,5 +1,5 @@
[General]
# Instance name can be used to distinguish between different instances
# Instance name can be used to distinguish between different instances using backend api.getInstanceName()
instanceName=
# set to true to allow using Trilium without authentication (makes sense for server build only, desktop build doesn't need password)

View File

@@ -1,6 +1,6 @@
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
.ck-widget__selection-handle, .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
.printed-content .ck-widget__selection-handle, .printed-content .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
display: none;
}

8
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.45.4",
"version": "0.45.6",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -2654,9 +2654,9 @@
}
},
"electron": {
"version": "9.3.4",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.4.tgz",
"integrity": "sha512-OHP8qMKgW8D8GtH+altB22WJw/lBOyyVdoz5e8D0/iPBmJU3Jm93vO4z4Eh/9DvdSXlH8bMHUCMLL9PVW6f+tw==",
"version": "9.3.5",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.5.tgz",
"integrity": "sha512-EPmDsp7sO0UPtw7nLD1ufse/nBskP+ifXzBgUg9psCUlapkzuwYi6pmLAzKLW/bVjwgyUKwh1OKWILWfOeLGcQ==",
"dev": true,
"requires": {
"@electron/get": "^1.0.1",

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.45.5",
"version": "0.45.7",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -77,7 +77,7 @@
},
"devDependencies": {
"cross-env": "7.0.2",
"electron": "9.3.4",
"electron": "9.3.5",
"electron-builder": "22.9.1",
"electron-packager": "15.1.0",
"electron-rebuild": "2.3.2",

View File

@@ -51,6 +51,12 @@ const TPL = `
<label for="erase-notes-after-time-in-seconds">Erase notes after X seconds</label>
<input class="form-control" id="erase-notes-after-time-in-seconds" type="number" min="0">
</div>
<p>You can also trigger erasing manually:</p>
<button id="erase-deleted-notes-now-button" class="btn">Erase deleted notes now</button>
<br/><br/>
</div>
<div>
@@ -117,6 +123,13 @@ export default class ProtectedSessionOptions {
return false;
});
this.$eraseDeletedNotesButton = $("#erase-deleted-notes-now-button");
this.$eraseDeletedNotesButton.on('click', () => {
server.post('notes/erase-deleted-notes-now').then(() => {
toastService.showMessage("Deleted notes have been erased.");
});
});
this.$protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
this.$protectedSessionTimeout.on('change', () => {

View File

@@ -75,14 +75,16 @@ class NoteShort {
this.parentToBranch[parentNoteId] = branchId;
}
addChild(childNoteId, branchId) {
addChild(childNoteId, branchId, sort = true) {
if (!this.children.includes(childNoteId)) {
this.children.push(childNoteId);
}
this.childToBranch[childNoteId] = branchId;
this.sortChildren();
if (sort) {
this.sortChildren();
}
}
sortChildren() {

View File

@@ -130,7 +130,7 @@ function linkContextMenu(e) {
appContext.tabManager.openTabWithNote(notePath);
}
else if (command === 'openNoteInNewWindow') {
appContext.openInNewWindow(notePath);
appContext.triggerCommand('openInWindow', {notePath});
}
}
});

View File

@@ -8,8 +8,8 @@ async function syncNow() {
toastService.showMessage("Sync finished successfully.");
}
else {
if (result.message.length > 100) {
result.message = result.message.substr(0, 100);
if (result.message.length > 200) {
result.message = result.message.substr(0, 200) + "...";
}
toastService.showError("Sync failed: " + result.message);

View File

@@ -87,6 +87,8 @@ class TreeCache {
const branchRows = resp.branches;
const attributeRows = resp.attributes;
const noteIdsToSort = new Set();
for (const noteRow of noteRows) {
const {noteId} = noteRow;
@@ -153,7 +155,9 @@ class TreeCache {
const parentNote = this.notes[branch.parentNoteId];
if (parentNote) {
parentNote.addChild(branch.noteId, branch.branchId);
parentNote.addChild(branch.noteId, branch.branchId, false);
noteIdsToSort.add(parentNote.noteId);
}
}
@@ -178,6 +182,11 @@ class TreeCache {
}
}
}
// sort all of them at once, this avoids repeated sorts (#1480)
for (const noteId of noteIdsToSort) {
this.notes[noteId].sortChildren();
}
}
async reloadNotes(noteIds) {

View File

@@ -2,9 +2,9 @@ import treeService from './tree.js';
import treeCache from "./tree_cache.js";
import hoistedNoteService from './hoisted_note.js';
import clipboard from './clipboard.js';
import protectedSessionHolder from "./protected_session_holder.js";
import noteCreateService from "./note_create.js";
import contextMenu from "./context_menu.js";
import appContext from "./app_context.js";
class TreeContextMenu {
/**
@@ -95,7 +95,7 @@ class TreeContextMenu {
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
{ title: 'Paste after', command: "pasteNotesAfterFromClipboard", uiIcon: "paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
{ title: "Duplicate subtree(s) here", command: "duplicateSubtree", uiIcon: "empty",
{ title: `Duplicate subtree <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "empty",
enabled: parentNotSearch && isNotRoot && !isHoisted },
{ title: "----" },
{ title: "Export", command: "exportNote", uiIcon: "empty",
@@ -110,14 +110,7 @@ class TreeContextMenu {
const notePath = treeService.getNotePath(this.node);
if (command === 'openInTab') {
const start = Date.now();
await this.node.load(true);
console.log("Reload took", Date.now() - start, "ms");
// appContext.tabManager.openTabWithNote(notePath);
appContext.tabManager.openTabWithNote(notePath);
}
else if (command === "insertNoteAfter") {
const parentNoteId = this.node.data.parentNoteId;

View File

@@ -214,7 +214,8 @@ export default class AttributeListWidget extends TabAwareWidget {
noteId: attribute.noteId,
type: attribute.type,
name: attribute.name,
value: attribute.value
value: attribute.value,
isInheritable: attribute.isInheritable
},
isOwned: false,
x: e.pageX,

View File

@@ -248,13 +248,22 @@ export default class NoteDetailWidget extends TabAwareWidget {
this.$widget.find('.note-detail-printable:visible').printThis({
header: $("<h2>").text(this.note && this.note.title).prop('outerHTML'),
footer: "<script>document.body.className += ' ck-content';</script>",
footer: `
<script src="libraries/katex/katex.min.js"></script>
<script src="libraries/katex/auto-render.min.js"></script>
<script>
document.body.className += ' ck-content printed-content';
renderMathInElement(document.body, {});
</script>
`,
importCSS: false,
loadCSS: [
"libraries/codemirror/codemirror.css",
"libraries/ckeditor/ckeditor-content.css",
"libraries/ckeditor/ckeditor-content.css",
"libraries/bootstrap/css/bootstrap.min.css",
"libraries/katex/katex.min.css",
"stylesheets/print.css",
"stylesheets/relation_map.css",
"stylesheets/themes.css"

View File

@@ -143,6 +143,11 @@ body {
--ck-color-dropdown-panel-background: var(--accented-background-color);
--ck-color-dropdown-panel-border: var(--main-border-color);
/* -- Overrides the default .ck-splitbutton class colors. ----------------------------------- */
--ck-color-split-button-hover-background: var(--ck-color-button-default-hover-background);
--ck-color-split-button-hover-border: var(--main-border-color);
/* -- Overrides the default .ck-input class colors. ----------------------------------------- */
--ck-color-input-background: var(--accented-background-color);
@@ -199,6 +204,9 @@ body {
--ck-color-engine-placeholder-text: var(--muted-text-color);
--ck-z-modal: 10000;
--ck-color-widget-type-around-button: var(--main-border-color);
--ck-color-widget-type-around-button-hover: var(--main-border-color);
}
body {

View File

@@ -3,15 +3,33 @@
const sql = require('../../services/sql');
function getRelations(noteIds) {
return (sql.getManyRows(`
SELECT noteId, name, value AS targetNoteId
FROM attributes
WHERE (noteId IN (???) OR value IN (???))
AND type = 'relation'
AND isDeleted = 0
AND noteId != ''
AND value != ''
`, Array.from(noteIds)));
noteIds = Array.from(noteIds);
return [
// first read all non-image relations
...sql.getManyRows(`
SELECT noteId, name, value AS targetNoteId
FROM attributes
WHERE (noteId IN (???) OR value IN (???))
AND type = 'relation'
AND name != 'imageLink'
AND isDeleted = 0
AND noteId != ''
AND value != ''`, noteIds),
// ... then read only imageLink relations which are not connecting parent and child
// this is done to not show image links in the trivial case where they are direct children of the note to which they are included. Same heuristic as in note tree
...sql.getManyRows(`
SELECT rel.noteId, rel.name, rel.value AS targetNoteId
FROM attributes AS rel
LEFT JOIN branches ON branches.parentNoteId = rel.noteId AND branches.noteId = rel.value AND branches.isDeleted = 0
WHERE (rel.noteId IN (???) OR rel.value IN (???))
AND rel.type = 'relation'
AND rel.name = 'imageLink'
AND rel.isDeleted = 0
AND rel.noteId != ''
AND rel.value != ''
AND branches.branchId IS NULL`, noteIds)
];
}
function getLinkMap(req) {

View File

@@ -193,6 +193,10 @@ function duplicateSubtree(req) {
return noteService.duplicateSubtree(noteId, parentNoteId);
}
function eraseDeletedNotesNow() {
noteService.eraseDeletedNotesNow();
}
module.exports = {
getNote,
updateNote,
@@ -204,5 +208,6 @@ module.exports = {
setNoteTypeMime,
getRelationMap,
changeTitle,
duplicateSubtree
duplicateSubtree,
eraseDeletedNotesNow
};

View File

@@ -38,6 +38,8 @@ function saveSyncSeed(req) {
}]
}
log.info("Saved sync seed.");
sqlInit.createDatabaseForSync(options);
}

View File

@@ -57,7 +57,7 @@ function getTree(req) {
const noteIds = sql.getColumn(`
WITH RECURSIVE
treeWithDescendants(noteId, isExpanded) AS (
SELECT noteId, 1 FROM branches WHERE parentNoteId = ? AND isDeleted = 0
SELECT noteId, isExpanded FROM branches WHERE parentNoteId = ? AND isDeleted = 0
UNION
SELECT branches.noteId, branches.isExpanded FROM branches
JOIN treeWithDescendants ON branches.parentNoteId = treeWithDescendants.noteId

View File

@@ -153,6 +153,7 @@ function register(app) {
route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision);
apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision);
apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap);
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);

View File

@@ -1 +1 @@
module.exports = { buildDate:"2020-11-20T22:50:10+01:00", buildRevision: "e5fa1e0ed555c1c2cb4a14c426d7091d62b5beea" };
module.exports = { buildDate:"2020-12-22T20:21:15+01:00", buildRevision: "bdfd760b9d428dc29c45a0e016d12a25af220043" };

View File

@@ -650,7 +650,7 @@ class ConsistencyChecks {
// root branch should always be expanded
sql.execute("UPDATE branches SET isExpanded = 1 WHERE branchId = 'root'");
if (this.unrecoveredConsistencyErrors) {
if (!this.unrecoveredConsistencyErrors) {
// we run this only if basic checks passed since this assumes basic data consistency
this.checkTreeCycles();

View File

@@ -52,6 +52,10 @@ function encrypt(key, plainText, ivLength = 13) {
}
function decrypt(key, cipherText, ivLength = 13) {
if (cipherText === null) {
return null;
}
if (!key) {
return "[protected]";
}
@@ -93,6 +97,10 @@ function decrypt(key, cipherText, ivLength = 13) {
function decryptString(dataKey, cipherText) {
const buffer = decrypt(dataKey, cipherText);
if (buffer === null) {
return null;
}
const str = buffer.toString('utf-8');
if (str === 'false') {
@@ -108,4 +116,4 @@ module.exports = {
encrypt,
decrypt,
decryptString
};
};

View File

@@ -70,6 +70,10 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
if (templateNoteContent) {
note.setContent(templateNoteContent);
}
note.type = templateNote.type;
note.mime = templateNote.mime;
note.save();
}
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
@@ -90,10 +94,10 @@ eventService.subscribe(eventService.CHILD_NOTE_CREATED, ({ parentNote, childNote
function processInverseRelations(entityName, entity, handler) {
if (entityName === 'attributes' && entity.type === 'relation') {
const note = entity.getNote();
const attributes = (note.getOwnedAttributes(entity.name)).filter(relation => relation.type === 'relation-definition');
const relDefinitions = note.getLabels('relation:' + entity.name);
for (const attribute of attributes) {
const definition = attribute.value;
for (const relDefinition of relDefinitions) {
const definition = relDefinition.getDefinition();
if (definition.inverseRelation && definition.inverseRelation.trim()) {
const targetNote = entity.getTargetNote();

View File

@@ -158,7 +158,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
}
if (taskContext.data.safeImport && attributeService.isAttributeDangerous(attr.type, attr.name)) {
attr.name = 'disabled-' + attr.name;
attr.name = 'disabled:' + attr.name;
}
attributes.push(attr);

View File

@@ -184,6 +184,12 @@ const DEFAULT_KEYBOARD_ACTIONS = [
description: "Add note above to the selection",
scope: "note-tree"
},
{
actionName: "duplicateSubtree",
defaultShortcuts: [],
description: "Duplicate subtree",
scope: "note-tree"
},
{
@@ -307,6 +313,12 @@ const DEFAULT_KEYBOARD_ACTIONS = [
description: "Cuts the selection from the current note and creates subnote with the selected text",
scope: "text-detail"
},
{
actionName: "addIncludeNoteToText",
defaultShortcuts: [],
description: "Opens the dialog to include a note",
scope: "text-detail"
},
{
separator: "Attributes (labels & relations)"

View File

@@ -338,7 +338,7 @@ class Note {
decrypt() {
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
this.title = protectedSessionService.decryptString(note.title);
this.title = protectedSessionService.decryptString(this.title);
this.isDecrypted = true;
}

View File

@@ -2,6 +2,7 @@
const NoteRevision = require('../entities/note_revision');
const dateUtils = require('../services/date_utils');
const log = require('../services/log');
/**
* @param {Note} note
@@ -9,14 +10,21 @@ const dateUtils = require('../services/date_utils');
function protectNoteRevisions(note) {
for (const revision of note.getRevisions()) {
if (note.isProtected !== revision.isProtected) {
const content = revision.getContent();
try {
const content = revision.getContent();
revision.isProtected = note.isProtected;
revision.isProtected = note.isProtected;
// this will force de/encryption
revision.setContent(content);
// this will force de/encryption
revision.setContent(content);
revision.save();
revision.save();
}
catch (e) {
log.error("Could not un/protect note revision ID = " + revision.noteRevisionId);
throw e;
}
}
}
}

View File

@@ -185,18 +185,25 @@ function protectNoteRecursively(note, protect, includingSubTree, taskContext) {
}
function protectNote(note, protect) {
if (protect !== note.isProtected) {
const content = note.getContent();
try {
if (protect !== note.isProtected) {
const content = note.getContent();
note.isProtected = protect;
note.isProtected = protect;
// this will force de/encryption
note.setContent(content);
// this will force de/encryption
note.setContent(content);
note.save();
note.save();
}
noteRevisionService.protectNoteRevisions(note);
}
catch (e) {
log.error("Could not un/protect note ID = " + note.noteId);
noteRevisionService.protectNoteRevisions(note);
throw e;
}
}
function findImageLinks(content, foundLinks) {
@@ -668,8 +675,10 @@ function scanForLinks(note) {
}
}
function eraseDeletedNotes() {
const eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds');
function eraseDeletedNotes(eraseNotesAfterTimeInSeconds = null) {
if (eraseNotesAfterTimeInSeconds === null) {
eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds');
}
const cutoffDate = new Date(Date.now() - eraseNotesAfterTimeInSeconds * 1000);
@@ -719,6 +728,10 @@ function eraseDeletedNotes() {
log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`);
}
function eraseDeletedNotesNow() {
eraseDeletedNotes(0);
}
// do a replace in str - all keys should be replaced by the corresponding values
function replaceByMap(str, mapObj) {
const re = new RegExp(Object.keys(mapObj).join("|"),"g");
@@ -739,7 +752,10 @@ function duplicateSubtree(origNoteId, newParentNoteId) {
const res = duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapping);
res.note.title += " (dup)";
if (!res.note.title.endsWith('(dup)')) {
res.note.title += " (dup)";
}
res.note.save();
return res;
@@ -822,9 +838,9 @@ function getNoteIdMapping(origNote) {
sqlInit.dbReady.then(() => {
// first cleanup kickoff 5 minutes after startup
setTimeout(cls.wrap(eraseDeletedNotes), 5 * 60 * 1000);
setTimeout(cls.wrap(() => eraseDeletedNotes()), 5 * 60 * 1000);
setInterval(cls.wrap(eraseDeletedNotes), 4 * 3600 * 1000);
setInterval(cls.wrap(() => eraseDeletedNotes()), 4 * 3600 * 1000);
});
module.exports = {
@@ -838,5 +854,6 @@ module.exports = {
duplicateSubtree,
duplicateSubtreeWithoutRoot,
getUndeletedParentBranches,
triggerNoteTitleChanged
triggerNoteTitleChanged,
eraseDeletedNotesNow
};

View File

@@ -43,10 +43,18 @@ function decryptNotes(notes) {
}
function encrypt(plainText) {
if (plainText === null) {
return null;
}
return dataEncryptionService.encrypt(getDataKey(), plainText);
}
function decrypt(cipherText) {
if (cipherText === null) {
return null;
}
return dataEncryptionService.decrypt(getDataKey(), cipherText);
}