Compare commits

...

44 Commits

Author SHA1 Message Date
zadam
6afc299efb release 0.45.8 2021-01-11 22:29:31 +01:00
zadam
369274ead7 new env variable to specify start note, #1532 2021-01-11 22:29:02 +01:00
zadam
04e6431c09 use correct class in exported HTMLs so that content style is applied, #1504 2020-12-30 23:20:12 +01:00
zadam
e89057a771 search note content only if not excluded by other expressions 2020-12-25 20:46:04 +01:00
zadam
4f27254e64 fix top margin of images and tables which can obscure their handles 2020-12-25 13:01:35 +01:00
zadam
577dc95ab8 convert   into whitespace also for large notes 2020-12-25 12:55:16 +01:00
zadam
a266d6a3d5 don't strip tags for very large text notes, #1500 2020-12-24 23:37:21 +01:00
zadam
749b6cb57e don't strip tags for very large text notes, #1500 2020-12-24 23:33:42 +01:00
zadam
b0b2951ff6 cleanup 2020-12-22 22:30:04 +01:00
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
zadam
882b6be580 release 0.45.5 2020-11-20 22:50:10 +01:00
zadam
e5fa1e0ed5 hide table's selection handle, fixes #1448 2020-11-20 21:01:44 +01:00
zadam
1047aecfbd template subtree is now deep-duplicated on template assignment 2020-11-19 14:29:26 +01:00
zadam
314e0a453f "duplicate note" now duplicates whole note subtree instead of just individual note 2020-11-19 14:06:32 +01:00
zadam
8ec476ba96 fix ENEX import note saving 2020-11-19 13:30:39 +01:00
zadam
a346ba7038 removed outstandingPushes counting which is not needed 2020-11-18 22:30:00 +01:00
zadam
fd6b2f1e7f enex import cleanup 2020-11-18 21:30:56 +01:00
zadam
6662b9dbf9 rate limiting to improve responsiveness during / after import 2020-11-17 23:05:05 +01:00
zadam
c0a29ede05 small fixes to ENEX import 2020-11-17 22:35:20 +01:00
zadam
845907b8d2 fix recent changes to show all deleted notes (also without note revisions) 2020-11-17 21:06:38 +01:00
zadam
b12008e313 more robust search in face of inconsistent cache 2020-11-17 20:44:38 +01:00
zadam
a108ef91a0 fixed .createNoteLink API documentation 2020-11-17 20:11:10 +01:00
zadam
b5480b4137 fix sync check - it was always reporting success even in failure cases 2020-11-15 20:50:24 +01:00
50 changed files with 485 additions and 242 deletions

View File

@@ -1,5 +1,5 @@
[General] [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= instanceName=
# set to true to allow using Trilium without authentication (makes sense for server build only, desktop build doesn't need password) # set to true to allow using Trilium without authentication (makes sense for server build only, desktop build doesn't need password)

View File

@@ -1,12 +1,12 @@
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */ /* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
.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; display: none;
} }
/* /*
* CKEditor 5 (v22.0.0) content styles. * CKEditor 5 (v23.1.0) content styles.
* Generated on Thu, 27 Aug 2020 12:13:06 GMT. * Generated on Thu, 29 Oct 2020 12:17:48 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
*/ */
@@ -23,32 +23,6 @@
--ck-todo-list-checkmark-size: 16px; --ck-todo-list-checkmark-size: 16px;
} }
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* ckeditor5-image/theme/imagestyle.css */ /* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side { .ck-content .image-style-side {
float: right; float: right;
@@ -84,6 +58,17 @@
max-width: 100%; max-width: 100%;
min-width: 50px; min-width: 50px;
} }
/* ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: hsl(0, 0%, 20%);
background-color: hsl(0, 0%, 97%);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-image/theme/imageresize.css */ /* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized { .ck-content .image.image_resized {
max-width: 100%; max-width: 100%;
@@ -98,22 +83,31 @@
.ck-content .image.image_resized > figcaption { .ck-content .image.image_resized > figcaption {
display: block; display: block;
} }
/* ckeditor5-image/theme/imagecaption.css */ /* ckeditor5-highlight/theme/highlight.css */
.ck-content .image > figcaption { .ck-content .marker-yellow {
display: table-caption; background-color: var(--ck-highlight-marker-yellow);
caption-side: bottom;
word-break: break-word;
color: hsl(0, 0%, 20%);
background-color: hsl(0, 0%, 97%);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
} }
/* ckeditor5-basic-styles/theme/code.css */ /* ckeditor5-highlight/theme/highlight.css */
.ck-content code { .ck-content .marker-green {
background-color: hsla(0, 0%, 78%, 0.3); background-color: var(--ck-highlight-marker-green);
padding: .15em; }
border-radius: 2px; /* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
} }
/* ckeditor5-font/theme/fontsize.css */ /* ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny { .ck-content .text-tiny {
@@ -146,6 +140,12 @@
border-left: 0; border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%); border-right: solid 5px hsl(0, 0%, 80%);
} }
/* ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* ckeditor5-table/theme/table.css */ /* ckeditor5-table/theme/table.css */
.ck-content .table { .ck-content .table {
margin: 1em auto; margin: 1em auto;
@@ -215,13 +215,6 @@
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }
/* ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 1em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-list/theme/todolist.css */ /* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list { .ck-content .todo-list {
list-style: none; list-style: none;
@@ -289,6 +282,18 @@
.ck-content .todo-list .todo-list__label .todo-list__label__description { .ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle; vertical-align: middle;
} }
/* ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 1em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-html-embed/theme/htmlembed.css */
.ck-content .raw-html-embed {
margin: 1em auto;
min-width: 15em;
}
/* ckeditor5-horizontal-line/theme/horizontalline.css */ /* ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr { .ck-content hr {
margin: 15px 0; margin: 15px 0;
@@ -330,4 +335,4 @@
.ck-content .page-break::after { .ck-content .page-break::after {
display: none; display: none;
} }
} }

29
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "trilium", "name": "trilium",
"version": "0.45.3", "version": "0.45.6",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -2654,9 +2654,9 @@
} }
}, },
"electron": { "electron": {
"version": "9.3.4", "version": "9.3.5",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.4.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-9.3.5.tgz",
"integrity": "sha512-OHP8qMKgW8D8GtH+altB22WJw/lBOyyVdoz5e8D0/iPBmJU3Jm93vO4z4Eh/9DvdSXlH8bMHUCMLL9PVW6f+tw==", "integrity": "sha512-EPmDsp7sO0UPtw7nLD1ufse/nBskP+ifXzBgUg9psCUlapkzuwYi6pmLAzKLW/bVjwgyUKwh1OKWILWfOeLGcQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@electron/get": "^1.0.1", "@electron/get": "^1.0.1",
@@ -4838,6 +4838,11 @@
"type-check": "~0.3.2" "type-check": "~0.3.2"
} }
}, },
"limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"line-column": { "line-column": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz",
@@ -6913,6 +6918,22 @@
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
}, },
"stream-throttle": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz",
"integrity": "sha1-rdV8jXzHOoFjDTHNVdOWHPr7qcM=",
"requires": {
"commander": "^2.2.0",
"limiter": "^1.0.5"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
}
}
},
"streamsearch": { "streamsearch": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",

View File

@@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.45.4", "version": "0.45.8",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@@ -65,6 +65,7 @@
"semver": "7.3.2", "semver": "7.3.2",
"serve-favicon": "2.5.0", "serve-favicon": "2.5.0",
"session-file-store": "1.5.0", "session-file-store": "1.5.0",
"stream-throttle": "^0.1.3",
"striptags": "3.1.1", "striptags": "3.1.1",
"tmp": "^0.2.1", "tmp": "^0.2.1",
"turndown": "7.0.0", "turndown": "7.0.0",
@@ -76,7 +77,7 @@
}, },
"devDependencies": { "devDependencies": {
"cross-env": "7.0.2", "cross-env": "7.0.2",
"electron": "9.3.4", "electron": "9.3.5",
"electron-builder": "22.9.1", "electron-builder": "22.9.1",
"electron-packager": "15.1.0", "electron-packager": "15.1.0",
"electron-rebuild": "2.3.2", "electron-rebuild": "2.3.2",

View File

@@ -38,7 +38,7 @@ class Branch extends Entity {
} }
beforeSaving() { beforeSaving() {
if (this.notePosition === undefined) { if (this.notePosition === undefined || this.notePosition === null) {
const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]); const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 10; this.notePosition = maxNotePos === null ? 0 : maxNotePos + 10;
} }

View File

@@ -51,6 +51,12 @@ const TPL = `
<label for="erase-notes-after-time-in-seconds">Erase notes after X seconds</label> <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"> <input class="form-control" id="erase-notes-after-time-in-seconds" type="number" min="0">
</div> </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>
<div> <div>
@@ -117,6 +123,13 @@ export default class ProtectedSessionOptions {
return false; 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 = $("#protected-session-timeout-in-seconds");
this.$protectedSessionTimeout.on('change', () => { this.$protectedSessionTimeout.on('change', () => {

View File

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

View File

@@ -274,7 +274,10 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* *
* @method * @method
* @param {string} notePath (or noteId) * @param {string} notePath (or noteId)
* @param {string} [noteTitle] - if not present we'll use note title * @param {object} [params]
* @param {boolean} [params.showTooltip=true] - enable/disable tooltip on the link
* @param {boolean} [params.showNotePath=false] - show also whole note's path as part of the link
* @param {string} [title=] - custom link tile with note's title as default
*/ */
this.createNoteLink = linkService.createNoteLink; this.createNoteLink = linkService.createNoteLink;

View File

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

View File

@@ -88,7 +88,7 @@ function parseSelectedHtml(selectedHtml) {
} }
} }
async function duplicateNote(noteId, parentNoteId) { async function duplicateSubtree(noteId, parentNoteId) {
const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`); const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`);
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
@@ -102,5 +102,5 @@ async function duplicateNote(noteId, parentNoteId) {
export default { export default {
createNote, createNote,
createNewTopLevelNote, createNewTopLevelNote,
duplicateNote duplicateSubtree
}; };

View File

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

View File

@@ -87,6 +87,8 @@ class TreeCache {
const branchRows = resp.branches; const branchRows = resp.branches;
const attributeRows = resp.attributes; const attributeRows = resp.attributes;
const noteIdsToSort = new Set();
for (const noteRow of noteRows) { for (const noteRow of noteRows) {
const {noteId} = noteRow; const {noteId} = noteRow;
@@ -153,7 +155,9 @@ class TreeCache {
const parentNote = this.notes[branch.parentNoteId]; const parentNote = this.notes[branch.parentNoteId];
if (parentNote) { 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) { async reloadNotes(noteIds) {

View File

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

View File

@@ -8,8 +8,6 @@ import options from "./options.js";
import treeCache from "./tree_cache.js"; import treeCache from "./tree_cache.js";
import noteAttributeCache from "./note_attribute_cache.js"; import noteAttributeCache from "./note_attribute_cache.js";
const $outstandingSyncsCount = $("#outstanding-syncs-count");
const messageHandlers = []; const messageHandlers = [];
let ws; let ws;
@@ -64,8 +62,6 @@ async function handleMessage(event) {
let syncRows = message.data; let syncRows = message.data;
lastPingTs = Date.now(); lastPingTs = Date.now();
$outstandingSyncsCount.html(message.outstandingSyncs);
if (syncRows.length > 0) { if (syncRows.length > 0) {
logRows(syncRows); logRows(syncRows);

View File

@@ -130,7 +130,7 @@ function SetupModel() {
} }
async function checkOutstandingSyncs() { async function checkOutstandingSyncs() {
const { stats, initialized } = await $.get('api/sync/stats'); const { outstandingPullCount, initialized } = await $.get('api/sync/stats');
if (initialized) { if (initialized) {
if (utils.isElectron()) { if (utils.isElectron()) {
@@ -143,9 +143,7 @@ async function checkOutstandingSyncs() {
} }
} }
else { else {
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls; $("#outstanding-syncs").html(outstandingPullCount);
$("#outstanding-syncs").html(totalOutstandingSyncs);
} }
} }

View File

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

View File

@@ -41,7 +41,7 @@ const TPL = `
<a class="dropdown-item sync-now-button" title="Trigger sync"> <a class="dropdown-item sync-now-button" title="Trigger sync">
<span class="bx bx-refresh"></span> <span class="bx bx-refresh"></span>
Sync now (<span id="outstanding-syncs-count">0</span>) Sync now
</a> </a>
<a class="dropdown-item" data-trigger-command="openNewWindow"> <a class="dropdown-item" data-trigger-command="openNewWindow">

View File

@@ -248,13 +248,22 @@ export default class NoteDetailWidget extends TabAwareWidget {
this.$widget.find('.note-detail-printable:visible').printThis({ this.$widget.find('.note-detail-printable:visible').printThis({
header: $("<h2>").text(this.note && this.note.title).prop('outerHTML'), 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, importCSS: false,
loadCSS: [ loadCSS: [
"libraries/codemirror/codemirror.css", "libraries/codemirror/codemirror.css",
"libraries/ckeditor/ckeditor-content.css", "libraries/ckeditor/ckeditor-content.css",
"libraries/ckeditor/ckeditor-content.css", "libraries/ckeditor/ckeditor-content.css",
"libraries/bootstrap/css/bootstrap.min.css", "libraries/bootstrap/css/bootstrap.min.css",
"libraries/katex/katex.min.css",
"stylesheets/print.css", "stylesheets/print.css",
"stylesheets/relation_map.css", "stylesheets/relation_map.css",
"stylesheets/themes.css" "stylesheets/themes.css"

View File

@@ -1341,7 +1341,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
protectedSessionService.protectNote(node.data.noteId, false, true); protectedSessionService.protectNote(node.data.noteId, false, true);
} }
duplicateNoteCommand({node}) { duplicateSubtreeCommand({node}) {
const nodesToDuplicate = this.getSelectedOrActiveNodes(node); const nodesToDuplicate = this.getSelectedOrActiveNodes(node);
for (const nodeToDuplicate of nodesToDuplicate) { for (const nodeToDuplicate of nodesToDuplicate) {
@@ -1353,7 +1353,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const branch = treeCache.getBranch(nodeToDuplicate.data.branchId); const branch = treeCache.getBranch(nodeToDuplicate.data.branchId);
noteCreateService.duplicateNote(nodeToDuplicate.data.noteId, branch.parentNoteId); noteCreateService.duplicateSubtree(nodeToDuplicate.data.noteId, branch.parentNoteId);
} }
} }
} }

View File

@@ -38,7 +38,7 @@ const TPL = `
cursor: text !important; cursor: text !important;
} }
.note-detail-editable-text *:first-child { .note-detail-editable-text *:not(figure):first-child {
margin-top: 0 !important; margin-top: 0 !important;
} }

View File

@@ -143,6 +143,11 @@ body {
--ck-color-dropdown-panel-background: var(--accented-background-color); --ck-color-dropdown-panel-background: var(--accented-background-color);
--ck-color-dropdown-panel-border: var(--main-border-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. ----------------------------------------- */ /* -- Overrides the default .ck-input class colors. ----------------------------------------- */
--ck-color-input-background: var(--accented-background-color); --ck-color-input-background: var(--accented-background-color);
@@ -199,6 +204,9 @@ body {
--ck-color-engine-placeholder-text: var(--muted-text-color); --ck-color-engine-placeholder-text: var(--muted-text-color);
--ck-z-modal: 10000; --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 { body {

View File

@@ -23,11 +23,7 @@ function exportBranch(req, res) {
try { try {
if (type === 'subtree' && (format === 'html' || format === 'markdown')) { if (type === 'subtree' && (format === 'html' || format === 'markdown')) {
const start = Date.now();
zipExportService.exportToZip(taskContext, branch, format, res); zipExportService.exportToZip(taskContext, branch, format, res);
console.log("Export took", Date.now() - start, "ms");
} }
else if (type === 'single') { else if (type === 'single') {
singleExportService.exportSingleNote(taskContext, branch, format, res); singleExportService.exportSingleNote(taskContext, branch, format, res);

View File

@@ -3,15 +3,33 @@
const sql = require('../../services/sql'); const sql = require('../../services/sql');
function getRelations(noteIds) { function getRelations(noteIds) {
return (sql.getManyRows(` noteIds = Array.from(noteIds);
SELECT noteId, name, value AS targetNoteId
FROM attributes return [
WHERE (noteId IN (???) OR value IN (???)) // first read all non-image relations
AND type = 'relation' ...sql.getManyRows(`
AND isDeleted = 0 SELECT noteId, name, value AS targetNoteId
AND noteId != '' FROM attributes
AND value != '' WHERE (noteId IN (???) OR value IN (???))
`, Array.from(noteIds))); 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) { function getLinkMap(req) {

View File

@@ -187,10 +187,14 @@ function changeTitle(req) {
return note; return note;
} }
function duplicateNote(req) { function duplicateSubtree(req) {
const {noteId, parentNoteId} = req.params; const {noteId, parentNoteId} = req.params;
return noteService.duplicateNote(noteId, parentNoteId); return noteService.duplicateSubtree(noteId, parentNoteId);
}
function eraseDeletedNotesNow() {
noteService.eraseDeletedNotesNow();
} }
module.exports = { module.exports = {
@@ -204,5 +208,6 @@ module.exports = {
setNoteTypeMime, setNoteTypeMime,
getRelationMap, getRelationMap,
changeTitle, changeTitle,
duplicateNote duplicateSubtree,
eraseDeletedNotesNow
}; };

View File

@@ -31,19 +31,36 @@ function getRecentChanges(req) {
} }
} }
// now we need to also collect date points not represented in note revisions:
// 1. creation for all notes (dateCreated)
// 2. deletion for deleted notes (dateModified)
const notes = sql.getRows(` const notes = sql.getRows(`
SELECT SELECT
notes.noteId, notes.noteId,
notes.isDeleted AS current_isDeleted, notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId, notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased, notes.isErased AS current_isErased,
notes.title AS current_title, notes.title AS current_title,
notes.isProtected AS current_isProtected, notes.isProtected AS current_isProtected,
notes.title, notes.title,
notes.utcDateCreated AS utcDate, notes.utcDateCreated AS utcDate,
notes.dateCreated AS date notes.dateCreated AS date
FROM FROM
notes`); notes
UNION ALL
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateModified AS utcDate,
notes.dateModified AS date
FROM
notes
WHERE notes.isDeleted = 1 AND notes.isErased = 0`);
for (const note of notes) { for (const note of notes) {
if (noteCacheService.isInAncestor(note.noteId, ancestorNoteId)) { if (noteCacheService.isInAncestor(note.noteId, ancestorNoteId)) {

View File

@@ -4,6 +4,7 @@ const imageType = require('image-type');
const imageService = require('../../services/image'); const imageService = require('../../services/image');
const dateNoteService = require('../../services/date_notes'); const dateNoteService = require('../../services/date_notes');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const attributeService = require('../../services/attributes');
function uploadImage(req) { function uploadImage(req) {
const file = req.file; const file = req.file;
@@ -37,7 +38,7 @@ function saveNote(req) {
if (req.body.labels) { if (req.body.labels) {
for (const {name, value} of req.body.labels) { for (const {name, value} of req.body.labels) {
note.setLabel(name, value); note.setLabel(attributeService.sanitizeAttributeName(name), value);
} }
} }

View File

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

View File

@@ -13,13 +13,13 @@ const dateUtils = require('../../services/date_utils');
const entityConstructor = require('../../entities/entity_constructor'); const entityConstructor = require('../../entities/entity_constructor');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
function testSync() { async function testSync() {
try { try {
if (!syncOptions.isSyncSetup()) { if (!syncOptions.isSyncSetup()) {
return { success: false, message: "Sync server host is not configured. Please configure sync first." }; return { success: false, message: "Sync server host is not configured. Please configure sync first." };
} }
syncService.login(); await syncService.login();
// login was successful so we'll kick off sync now // login was successful so we'll kick off sync now
// this is important in case when sync server has been just initialized // this is important in case when sync server has been just initialized
@@ -43,7 +43,7 @@ function getStats() {
const stats = { const stats = {
initialized: optionService.getOption('initialized') === 'true', initialized: optionService.getOption('initialized') === 'true',
stats: syncService.stats outstandingPullCount: syncService.getOutstandingPullCount()
}; };
log.info(`Returning sync stats: ${JSON.stringify(stats)}`); log.info(`Returning sync stats: ${JSON.stringify(stats)}`);

View File

@@ -57,7 +57,7 @@ function getTree(req) {
const noteIds = sql.getColumn(` const noteIds = sql.getColumn(`
WITH RECURSIVE WITH RECURSIVE
treeWithDescendants(noteId, isExpanded) AS ( 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 UNION
SELECT branches.noteId, branches.isExpanded FROM branches SELECT branches.noteId, branches.isExpanded FROM branches
JOIN treeWithDescendants ON branches.parentNoteId = treeWithDescendants.noteId JOIN treeWithDescendants ON branches.parentNoteId = treeWithDescendants.noteId

View File

@@ -153,8 +153,9 @@ function register(app) {
route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision); route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision);
apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision); apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision);
apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap); 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(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateNote); apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree);
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate); apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);

View File

@@ -2,7 +2,6 @@
const repository = require('./repository'); const repository = require('./repository');
const sql = require('./sql'); const sql = require('./sql');
const utils = require('./utils');
const Attribute = require('../entities/attribute'); const Attribute = require('../entities/attribute');
const ATTRIBUTE_TYPES = [ 'label', 'relation' ]; const ATTRIBUTE_TYPES = [ 'label', 'relation' ];
@@ -146,6 +145,20 @@ function getBuiltinAttributeNames() {
]); ]);
} }
function sanitizeAttributeName(origName) {
let fixedName;
if (origName === '') {
fixedName = "unnamed";
}
else {
// any not allowed character should be replaced with underscore
fixedName = origName.replace(/[^\p{L}\p{N}_:]/ug, "_");
}
return fixedName;
}
module.exports = { module.exports = {
getNotesWithLabel, getNotesWithLabel,
getNotesWithLabels, getNotesWithLabels,
@@ -156,5 +169,6 @@ module.exports = {
getAttributeNames, getAttributeNames,
isAttributeType, isAttributeType,
isAttributeDangerous, isAttributeDangerous,
getBuiltinAttributeNames getBuiltinAttributeNames,
sanitizeAttributeName
}; };

View File

@@ -1 +1 @@
module.exports = { buildDate:"2020-11-12T22:15:23+01:00", buildRevision: "6c57b2220ff05059d7460369b195d281fcd9cbb6" }; module.exports = { buildDate:"2021-01-11T22:29:31+01:00", buildRevision: "369274ead75947a68bf7bbb5ab1e784e81521030" };

View File

@@ -11,6 +11,7 @@ const entityChangesService = require('./entity_changes.js');
const optionsService = require('./options'); const optionsService = require('./options');
const Branch = require('../entities/branch'); const Branch = require('../entities/branch');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const attributeService = require('./attributes');
class ConsistencyChecks { class ConsistencyChecks {
constructor(autoFix) { constructor(autoFix) {
@@ -607,20 +608,10 @@ class ConsistencyChecks {
findWronglyNamedAttributes() { findWronglyNamedAttributes() {
const attrNames = sql.getColumn(`SELECT DISTINCT name FROM attributes`); const attrNames = sql.getColumn(`SELECT DISTINCT name FROM attributes`);
const attrNameMatcher = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
for (const origName of attrNames) { for (const origName of attrNames) {
if (!attrNameMatcher.test(origName)) { const fixedName = attributeService.sanitizeAttributeName(origName);
let fixedName;
if (origName === '') {
fixedName = "unnamed";
}
else {
// any not allowed character should be replaced with underscore
fixedName = origName.replace(/[^\p{L}\p{N}_:]/ug, "_");
}
if (fixedName !== origName) {
if (this.autoFix) { if (this.autoFix) {
// there isn't a good way to update this: // there isn't a good way to update this:
// - just SQL query will fix it in DB but not notify frontend (or other caches) that it has been fixed // - just SQL query will fix it in DB but not notify frontend (or other caches) that it has been fixed
@@ -659,7 +650,7 @@ class ConsistencyChecks {
// root branch should always be expanded // root branch should always be expanded
sql.execute("UPDATE branches SET isExpanded = 1 WHERE branchId = 'root'"); 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 // we run this only if basic checks passed since this assumes basic data consistency
this.checkTreeCycles(); this.checkTreeCycles();

View File

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

View File

@@ -143,7 +143,7 @@ function exportToZip(taskContext, branch, format, res) {
const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable(); const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable();
// if it's a leaf then we'll export it even if it's empty // if it's a leaf then we'll export it even if it's empty
if (available && ((note.getContent()).length > 0 || childBranches.length === 0)) { if (available && (note.getContent().length > 0 || childBranches.length === 0)) {
meta.dataFileName = getDataFileName(note, baseFileName, existingFileNames); meta.dataFileName = getDataFileName(note, baseFileName, existingFileNames);
} }
@@ -234,7 +234,7 @@ function exportToZip(taskContext, branch, format, res) {
<link rel="stylesheet" href="${cssUrl}"> <link rel="stylesheet" href="${cssUrl}">
<base target="_parent"> <base target="_parent">
</head> </head>
<body> <body class="ck-content">
<h1>${utils.escapeHtml(title)}</h1> <h1>${utils.escapeHtml(title)}</h1>
${content} ${content}
</body> </body>
@@ -433,14 +433,13 @@ ${content}
} }
const note = branch.getNote(); const note = branch.getNote();
const zipFileName = (branch.prefix ? (branch.prefix + " - ") : "") + note.title + ".zip"; const zipFileName = (branch.prefix ? `${branch.prefix} - ` : "") + note.title + ".zip";
res.setHeader('Content-Disposition', utils.getContentDisposition(zipFileName)); res.setHeader('Content-Disposition', utils.getContentDisposition(zipFileName));
res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Type', 'application/zip');
zipFile.end();
zipFile.outputStream.pipe(res); zipFile.outputStream.pipe(res);
zipFile.end();
taskContext.taskSucceeded(); taskContext.taskSucceeded();
} }

View File

@@ -1,7 +1,7 @@
const eventService = require('./events'); const eventService = require('./events');
const scriptService = require('./script'); const scriptService = require('./script');
const treeService = require('./tree'); const treeService = require('./tree');
const log = require('./log'); const noteService = require('./notes');
const repository = require('./repository'); const repository = require('./repository');
const Attribute = require('../entities/attribute'); const Attribute = require('../entities/attribute');
@@ -58,17 +58,25 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
return; return;
} }
const targetNote = repository.getNote(entity.value); const templateNote = repository.getNote(entity.value);
if (!targetNote || !targetNote.isStringNote()) { if (!templateNote) {
return; return;
} }
const targetNoteContent = targetNote.getContent(); if (templateNote.isStringNote()) {
const templateNoteContent = templateNote.getContent();
if (targetNoteContent) { if (templateNoteContent) {
note.setContent(targetNoteContent); note.setContent(templateNoteContent);
}
note.type = templateNote.type;
note.mime = templateNote.mime;
note.save();
} }
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
} }
else if (entity.type === 'label' && entity.name === 'sorted') { else if (entity.type === 'label' && entity.name === 'sorted') {
treeService.sortNotesAlphabetically(entity.noteId); treeService.sortNotesAlphabetically(entity.noteId);
@@ -86,10 +94,10 @@ eventService.subscribe(eventService.CHILD_NOTE_CREATED, ({ parentNote, childNote
function processInverseRelations(entityName, entity, handler) { function processInverseRelations(entityName, entity, handler) {
if (entityName === 'attributes' && entity.type === 'relation') { if (entityName === 'attributes' && entity.type === 'relation') {
const note = entity.getNote(); 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) { for (const relDefinition of relDefinitions) {
const definition = attribute.value; const definition = relDefinition.getDefinition();
if (definition.inverseRelation && definition.inverseRelation.trim()) { if (definition.inverseRelation && definition.inverseRelation.trim()) {
const targetNote = entity.getTargetNote(); const targetNote = entity.getTargetNote();

View File

@@ -37,7 +37,7 @@ function getImageType(buffer) {
} }
} }
else { else {
return imageType(buffer); return imageType(buffer) || "jpg"; // optimistic JPG default
} }
} }

View File

@@ -1,5 +1,6 @@
const sax = require("sax"); const sax = require("sax");
const stream = require('stream'); const stream = require('stream');
const {Throttle} = require('stream-throttle');
const log = require("../log"); const log = require("../log");
const utils = require("../utils"); const utils = require("../utils");
const sql = require("../sql"); const sql = require("../sql");
@@ -7,6 +8,7 @@ const noteService = require("../notes");
const imageService = require("../image"); const imageService = require("../image");
const protectedSessionService = require('../protected_session'); const protectedSessionService = require('../protected_session');
const htmlSanitizer = require("../html_sanitizer"); const htmlSanitizer = require("../html_sanitizer");
const attributeService = require("../attributes");
// date format is e.g. 20181121T193703Z // date format is e.g. 20181121T193703Z
function parseDate(text) { function parseDate(text) {
@@ -37,10 +39,6 @@ function importEnex(taskContext, file, parentNote) {
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
})).note; })).note;
// we're persisting notes as we parse the document, but these are run asynchronously and may not be finished
// when we finish parsing. We use this to be sure that all saving has been finished before returning successfully.
const saveNotePromises = [];
function extractContent(content) { function extractContent(content) {
const openingNoteIndex = content.indexOf('<en-note>'); const openingNoteIndex = content.indexOf('<en-note>');
@@ -105,9 +103,17 @@ function importEnex(taskContext, file, parentNote) {
const previousTag = getPreviousTag(); const previousTag = getPreviousTag();
if (previousTag === 'note-attributes') { if (previousTag === 'note-attributes') {
let labelName = currentTag;
if (labelName === 'source-url') {
labelName = 'sourceUrl';
}
labelName = attributeService.sanitizeAttributeName(labelName);
note.attributes.push({ note.attributes.push({
type: 'label', type: 'label',
name: currentTag, name: labelName,
value: text value: text
}); });
} }
@@ -149,7 +155,7 @@ function importEnex(taskContext, file, parentNote) {
} else if (currentTag === 'tag') { } else if (currentTag === 'tag') {
note.attributes.push({ note.attributes.push({
type: 'label', type: 'label',
name: text, name: attributeService.sanitizeAttributeName(text),
value: '' value: ''
}) })
} }
@@ -227,6 +233,10 @@ function importEnex(taskContext, file, parentNote) {
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
for (const resource of resources) { for (const resource of resources) {
if (!resource.content) {
continue;
}
const hash = utils.md5(resource.content); const hash = utils.md5(resource.content);
const mediaRegex = new RegExp(`<en-media hash="${hash}"[^>]*>`, 'g'); const mediaRegex = new RegExp(`<en-media hash="${hash}"[^>]*>`, 'g');
@@ -304,7 +314,7 @@ function importEnex(taskContext, file, parentNote) {
path.pop(); path.pop();
if (tag === 'note') { if (tag === 'note') {
saveNotePromises.push(saveNote()); saveNote();
} }
}); });
@@ -323,12 +333,15 @@ function importEnex(taskContext, file, parentNote) {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
{ {
// resolve only when we parse the whole document AND saving of all notes have been finished // resolve only when we parse the whole document AND saving of all notes have been finished
saxStream.on("end", () => { Promise.all(saveNotePromises).then(() => resolve(rootNote)) }); saxStream.on("end", () => resolve(rootNote));
const bufferStream = new stream.PassThrough(); const bufferStream = new stream.PassThrough();
bufferStream.end(file.buffer); bufferStream.end(file.buffer);
bufferStream.pipe(saxStream); bufferStream
// rate limiting to improve responsiveness during / after import
.pipe(new Throttle({rate: 500000}))
.pipe(saxStream);
}); });
} }

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
const NoteRevision = require('../entities/note_revision'); const NoteRevision = require('../entities/note_revision');
const dateUtils = require('../services/date_utils'); const dateUtils = require('../services/date_utils');
const log = require('../services/log');
/** /**
* @param {Note} note * @param {Note} note
@@ -9,14 +10,21 @@ const dateUtils = require('../services/date_utils');
function protectNoteRevisions(note) { function protectNoteRevisions(note) {
for (const revision of note.getRevisions()) { for (const revision of note.getRevisions()) {
if (note.isProtected !== revision.isProtected) { 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 // this will force de/encryption
revision.setContent(content); 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) { function protectNote(note, protect) {
if (protect !== note.isProtected) { try {
const content = note.getContent(); if (protect !== note.isProtected) {
const content = note.getContent();
note.isProtected = protect; note.isProtected = protect;
// this will force de/encryption // this will force de/encryption
note.setContent(content); 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) { function findImageLinks(content, foundLinks) {
@@ -668,8 +675,10 @@ function scanForLinks(note) {
} }
} }
function eraseDeletedNotes() { function eraseDeletedNotes(eraseNotesAfterTimeInSeconds = null) {
const eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds'); if (eraseNotesAfterTimeInSeconds === null) {
eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds');
}
const cutoffDate = new Date(Date.now() - eraseNotesAfterTimeInSeconds * 1000); const cutoffDate = new Date(Date.now() - eraseNotesAfterTimeInSeconds * 1000);
@@ -719,26 +728,75 @@ function eraseDeletedNotes() {
log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`); log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`);
} }
function duplicateNote(noteId, parentNoteId) { function eraseDeletedNotesNow() {
const origNote = repository.getNote(noteId); 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");
return str.replace(re, matched => mapObj[matched]);
}
function duplicateSubtree(origNoteId, newParentNoteId) {
if (origNoteId === 'root') {
throw new Error('Duplicating root is not possible');
}
const origNote = repository.getNote(origNoteId);
// might be null if orig note is not in the target newParentNoteId
const origBranch = origNote.getBranches().find(branch => branch.parentNoteId === newParentNoteId);
const noteIdMapping = getNoteIdMapping(origNote);
const res = duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapping);
if (!res.note.title.endsWith('(dup)')) {
res.note.title += " (dup)";
}
res.note.save();
return res;
}
function duplicateSubtreeWithoutRoot(origNoteId, newNoteId) {
if (origNoteId === 'root') {
throw new Error('Duplicating root is not possible');
}
const origNote = repository.getNote(origNoteId);
const noteIdMapping = getNoteIdMapping(origNote);
for (const childBranch of origNote.getChildBranches()) {
duplicateSubtreeInner(childBranch.getNote(), childBranch, newNoteId, noteIdMapping);
}
}
function duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapping) {
if (origNote.isProtected && !protectedSessionService.isProtectedSessionAvailable()) { if (origNote.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
throw new Error(`Cannot duplicate note=${origNote.noteId} because it is protected and protected session is not available`); throw new Error(`Cannot duplicate note=${origNote.noteId} because it is protected and protected session is not available`);
} }
// might be null if orig note is not in the target parentNoteId
const origBranch = origNote.getBranches().find(branch => branch.parentNoteId === parentNoteId);
const newNote = new Note(origNote); const newNote = new Note(origNote);
newNote.noteId = undefined; // force creation of new note newNote.noteId = noteIdMapping[origNote.noteId];
newNote.title += " (dup)"; newNote.dateCreated = dateUtils.localNowDateTime();
newNote.utcDateCreated = dateUtils.utcNowDateTime();
newNote.save(); newNote.save();
newNote.setContent(origNote.getContent()); let content = origNote.getContent();
if (['text', 'relation-map', 'search'].includes(origNote.type)) {
// fix links in the content
content = replaceByMap(content, noteIdMapping);
}
newNote.setContent(content);
const newBranch = new Branch({ const newBranch = new Branch({
noteId: newNote.noteId, noteId: newNote.noteId,
parentNoteId: parentNoteId, parentNoteId: newParentNoteId,
// here increasing just by 1 to make sure it's directly after original // here increasing just by 1 to make sure it's directly after original
notePosition: origBranch ? origBranch.notePosition + 1 : null notePosition: origBranch ? origBranch.notePosition + 1 : null
}).save(); }).save();
@@ -746,22 +804,43 @@ function duplicateNote(noteId, parentNoteId) {
for (const attribute of origNote.getOwnedAttributes()) { for (const attribute of origNote.getOwnedAttributes()) {
const attr = new Attribute(attribute); const attr = new Attribute(attribute);
attr.attributeId = undefined; // force creation of new attribute attr.attributeId = undefined; // force creation of new attribute
attr.utcDateCreated = dateUtils.utcNowDateTime();
attr.noteId = newNote.noteId; attr.noteId = newNote.noteId;
// if relation points to within the duplicated tree then replace the target to the duplicated note
// if it points outside of duplicated tree then keep the original target
if (attr.type === 'relation' && attr.value in noteIdMapping) {
attr.value = noteIdMapping[attr.value];
}
attr.save(); attr.save();
} }
for (const childBranch of origNote.getChildBranches()) {
duplicateSubtreeInner(childBranch.getNote(), childBranch, newNote.noteId, noteIdMapping);
}
return { return {
note: newNote, note: newNote,
branch: newBranch branch: newBranch
}; };
} }
function getNoteIdMapping(origNote) {
const noteIdMapping = {};
// pregenerate new noteIds since we'll need to fix relation references even for not yet created notes
for (const origNoteId of origNote.getDescendantNoteIds()) {
noteIdMapping[origNoteId] = utils.newEntityId();
}
return noteIdMapping;
}
sqlInit.dbReady.then(() => { sqlInit.dbReady.then(() => {
// first cleanup kickoff 5 minutes after startup // 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 = { module.exports = {
@@ -772,7 +851,9 @@ module.exports = {
undeleteNote, undeleteNote,
protectNoteRecursively, protectNoteRecursively,
scanForLinks, scanForLinks,
duplicateNote, duplicateSubtree,
duplicateSubtreeWithoutRoot,
getUndeletedParentBranches, getUndeletedParentBranches,
triggerNoteTitleChanged triggerNoteTitleChanged,
eraseDeletedNotesNow
}; };

View File

@@ -31,10 +31,7 @@ function initNotSyncedOptions(initialized, startNotePath = 'root', opts = {}) {
optionService.createOption('openTabs', JSON.stringify([ optionService.createOption('openTabs', JSON.stringify([
{ {
notePath: startNotePath, notePath: startNotePath,
active: true, active: true
sidebar: {
widgets: []
}
} }
]), false); ]), false);
@@ -103,6 +100,15 @@ function initStartupOptions() {
log.info(`Created missing option "${name}" with default value "${value}"`); log.info(`Created missing option "${name}" with default value "${value}"`);
} }
} }
if (process.env.TRILIUM_START_NOTE_ID) {
optionService.setOption('openTabs', JSON.stringify([
{
notePath: process.env.TRILIUM_START_NOTE_ID,
active: true
}
]));
}
} }
function getKeyboardDefaultOptions() { function getKeyboardDefaultOptions() {

View File

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

View File

@@ -32,26 +32,29 @@ class NoteContentProtectedFulltextExp extends Expression {
FROM notes JOIN note_contents USING (noteId) FROM notes JOIN note_contents USING (noteId)
WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 1`)) { WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 1`)) {
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in noteCache.notes)) {
continue;
}
try { try {
content = protectedSessionService.decryptString(content); content = protectedSessionService.decryptString(content);
} }
catch (e) { catch (e) {
log.info('Cannot decrypt content of note', noteId); log.info(`Cannot decrypt content of note ${noteId}`);
continue; continue;
} }
content = content.toLowerCase(); content = content.toLowerCase();
if (type === 'text' && mime === 'text/html') { if (type === 'text' && mime === 'text/html') {
content = striptags(content); if (content.length < 20000) { // striptags is slow for very large notes
content = striptags(content);
}
content = content.replace(/&nbsp;/g, ' '); content = content.replace(/&nbsp;/g, ' ');
} }
if (this.tokens.find(token => !content.includes(token))) { if (!this.tokens.find(token => !content.includes(token))) {
continue;
}
if (inputNoteSet.hasNoteId(noteId) && noteId in noteCache.notes) {
resultNoteSet.add(noteCache.notes[noteId]); resultNoteSet.add(noteCache.notes[noteId]);
} }
} }

View File

@@ -26,18 +26,21 @@ class NoteContentUnprotectedFulltextExp extends Expression {
FROM notes JOIN note_contents USING (noteId) FROM notes JOIN note_contents USING (noteId)
WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 0`)) { WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 0`)) {
content = content.toString().toLowerCase(); if (!inputNoteSet.hasNoteId(noteId) || !(noteId in noteCache.notes)) {
if (type === 'text' && mime === 'text/html') {
content = striptags(content);
content = content.replace(/&nbsp;/g, ' ');
}
if (this.tokens.find(token => !content.includes(token))) {
continue; continue;
} }
if (inputNoteSet.hasNoteId(noteId) && noteId in noteCache.notes) { content = content.toString().toLowerCase();
if (type === 'text' && mime === 'text/html') {
if (content.length < 20000) { // striptags is slow for very large notes
content = striptags(content);
}
content = content.replace(/&nbsp;/g, ' ');
}
if (!this.tokens.find(token => !content.includes(token))) {
resultNoteSet.add(noteCache.notes[noteId]); resultNoteSet.add(noteCache.notes[noteId]);
} }
} }

View File

@@ -17,10 +17,14 @@ const utils = require('../../utils.js');
*/ */
function findNotesWithExpression(expression) { function findNotesWithExpression(expression) {
const hoistedNote = noteCache.notes[hoistedNoteService.getHoistedNoteId()]; const hoistedNote = noteCache.notes[hoistedNoteService.getHoistedNoteId()];
const allNotes = (hoistedNote && hoistedNote.noteId !== 'root') let allNotes = (hoistedNote && hoistedNote.noteId !== 'root')
? hoistedNote.subtreeNotes ? hoistedNote.subtreeNotes
: Object.values(noteCache.notes); : Object.values(noteCache.notes);
// in the process of loading data sometimes we create "skeleton" note instances which are expected to be filled later
// in case of inconsistent data this might not work and search will then crash on these
allNotes = allNotes.filter(note => note.type !== undefined);
const allNoteSet = new NoteSet(allNotes); const allNoteSet = new NoteSet(allNotes);
const searchContext = { const searchContext = {

View File

@@ -20,10 +20,7 @@ const entityConstructor = require('../entities/entity_constructor');
let proxyToggle = true; let proxyToggle = true;
const stats = { let outstandingPullCount = 0;
outstandingPushes: 0,
outstandingPulls: 0
};
async function sync() { async function sync() {
try { try {
@@ -135,11 +132,7 @@ async function pullChanges(syncContext) {
const pulledDate = Date.now(); const pulledDate = Date.now();
stats.outstandingPulls = resp.maxEntityChangeId - lastSyncedPull; outstandingPullCount = Math.max(0, resp.maxEntityChangeId - lastSyncedPull);
if (stats.outstandingPulls < 0) {
stats.outstandingPulls = 0;
}
const {entityChanges} = resp; const {entityChanges} = resp;
@@ -159,13 +152,13 @@ async function pullChanges(syncContext) {
syncUpdateService.updateEntity(entityChange, entity, syncContext.sourceId); syncUpdateService.updateEntity(entityChange, entity, syncContext.sourceId);
} }
stats.outstandingPulls = resp.maxEntityChangeId - entityChange.id; outstandingPullCount = Math.max(0, resp.maxEntityChangeId - entityChange.id);
} }
setLastSyncedPull(entityChanges[entityChanges.length - 1].entityChange.id); setLastSyncedPull(entityChanges[entityChanges.length - 1].entityChange.id);
}); });
log.info(`Pulled ${entityChanges.length} changes starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${stats.outstandingPulls} outstanding pulls`); log.info(`Pulled ${entityChanges.length} changes starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`);
} }
if (atLeastOnePullApplied) { if (atLeastOnePullApplied) {
@@ -359,31 +352,25 @@ function setLastSyncedPush(entityChangeId) {
optionService.setOption('lastSyncedPush', entityChangeId); optionService.setOption('lastSyncedPush', entityChangeId);
} }
function updatePushStats() {
if (syncOptions.isSyncSetup()) {
const lastSyncedPush = optionService.getOption('lastSyncedPush');
stats.outstandingPushes = sql.getValue("SELECT COUNT(1) FROM entity_changes WHERE isSynced = 1 AND id > ?", [lastSyncedPush]);
}
}
function getMaxEntityChangeId() { function getMaxEntityChangeId() {
return sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes'); return sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes');
} }
function getOutstandingPullCount() {
return outstandingPullCount;
}
sqlInit.dbReady.then(() => { sqlInit.dbReady.then(() => {
setInterval(cls.wrap(sync), 60000); setInterval(cls.wrap(sync), 60000);
// kickoff initial sync immediately // kickoff initial sync immediately
setTimeout(cls.wrap(sync), 3000); setTimeout(cls.wrap(sync), 3000);
setInterval(cls.wrap(updatePushStats), 1000);
}); });
module.exports = { module.exports = {
sync, sync,
login, login,
getEntityChangesRecords, getEntityChangesRecords,
stats, getOutstandingPullCount,
getMaxEntityChangeId getMaxEntityChangeId
}; };

View File

@@ -110,8 +110,7 @@ function sendPing(client, syncRows = []) {
sendMessage(client, { sendMessage(client, {
type: 'sync', type: 'sync',
data: syncRows, data: syncRows
outstandingSyncs: stats.outstandingPushes + stats.outstandingPulls
}); });
} }