Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f3d73b9fd | |||
| bdfd760b9d | |||
| 7133e60267 | |||
| fc4edf4aa7 | |||
| eaf93a70cd | |||
| b093569ec5 | |||
| 4633c68a0c | |||
| 33571e0ef3 | |||
| 31876d2cf9 | |||
| 81c6043cb6 | |||
| 1982d054ef | |||
| e56979c482 | |||
| 58555b3660 | |||
| b7b1324dd0 | |||
| e318acc977 | |||
| 8ae82f5b69 | |||
| 26442f418a | |||
| 23a432e7d8 | |||
| 984ecaf99c | |||
| 21b73a86b2 | |||
| 7d8277699c | |||
| 928ed7a034 | |||
| 882b6be580 | |||
| e5fa1e0ed5 | |||
| 1047aecfbd | |||
| 314e0a453f | |||
| 8ec476ba96 | |||
| a346ba7038 | |||
| fd6b2f1e7f | |||
| 6662b9dbf9 | |||
| c0a29ede05 | |||
| 845907b8d2 | |||
| b12008e313 | |||
| a108ef91a0 | |||
| b5480b4137 | |||
| 47d61c416d | |||
| 6c57b2220f | |||
| 99f01b9ccf | |||
| d5a9abd911 | |||
| a3a2bc0a74 | |||
| 402e5c4d81 | |||
| 5157fc15e9 | |||
| 4bd87b1796 | |||
| ce33eb3abd | |||
| f988935a33 | |||
| 9b05d30b47 | |||
| 9e97fdcc49 | |||
| 8e8148ce42 |
Generated
-3
@@ -6,9 +6,6 @@
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</value>
|
||||
</option>
|
||||
<H2CodeStyleSettings version="5">
|
||||
<option name="USE_GENERAL_STYLE" value="false" />
|
||||
</H2CodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="USE_EXPLICIT_JS_EXTENSION" value="TRUE" />
|
||||
</JSCodeStyleSettings>
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+4501
-4387
File diff suppressed because it is too large
Load Diff
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
+1500
-1462
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 912 KiB After Width: | Height: | Size: 952 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,12 +1,12 @@
|
||||
/* !!!!!! 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;
|
||||
}
|
||||
|
||||
/*
|
||||
* CKEditor 5 (v22.0.0) content styles.
|
||||
* Generated on Thu, 27 Aug 2020 12:13:06 GMT.
|
||||
* CKEditor 5 (v23.1.0) content styles.
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -23,32 +23,6 @@
|
||||
--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 */
|
||||
.ck-content .image-style-side {
|
||||
float: right;
|
||||
@@ -84,6 +58,17 @@
|
||||
max-width: 100%;
|
||||
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 */
|
||||
.ck-content .image.image_resized {
|
||||
max-width: 100%;
|
||||
@@ -98,22 +83,31 @@
|
||||
.ck-content .image.image_resized > figcaption {
|
||||
display: block;
|
||||
}
|
||||
/* 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-highlight/theme/highlight.css */
|
||||
.ck-content .marker-yellow {
|
||||
background-color: var(--ck-highlight-marker-yellow);
|
||||
}
|
||||
/* ckeditor5-basic-styles/theme/code.css */
|
||||
.ck-content code {
|
||||
background-color: hsla(0, 0%, 78%, 0.3);
|
||||
padding: .15em;
|
||||
border-radius: 2px;
|
||||
/* 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-font/theme/fontsize.css */
|
||||
.ck-content .text-tiny {
|
||||
@@ -146,6 +140,12 @@
|
||||
border-left: 0;
|
||||
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 */
|
||||
.ck-content .table {
|
||||
margin: 1em auto;
|
||||
@@ -215,13 +215,6 @@
|
||||
-ms-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 */
|
||||
.ck-content .todo-list {
|
||||
list-style: none;
|
||||
@@ -289,6 +282,18 @@
|
||||
.ck-content .todo-list .todo-list__label .todo-list__label__description {
|
||||
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 */
|
||||
.ck-content hr {
|
||||
margin: 15px 0;
|
||||
@@ -330,4 +335,4 @@
|
||||
.ck-content .page-break::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Generated
+27
-50
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.45.1",
|
||||
"version": "0.45.6",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -586,11 +586,6 @@
|
||||
"defer-to-connect": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@tokenizer/token": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.1.1.tgz",
|
||||
"integrity": "sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w=="
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.0.0.tgz",
|
||||
@@ -611,7 +606,8 @@
|
||||
"@types/debug": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
|
||||
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
|
||||
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/eslint": {
|
||||
"version": "7.2.4",
|
||||
@@ -2658,9 +2654,9 @@
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"version": "9.3.3",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.3.tgz",
|
||||
"integrity": "sha512-xghKeUY1qgnEcJ5w2rXo/toH+8NT2Dktx2aAxBNPV7CIJr3mejJJAPwLbycwtddzr37tgKxHeHlc8ivfKtMkJQ==",
|
||||
"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",
|
||||
@@ -3680,17 +3676,6 @@
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"file-type": {
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.0.0.tgz",
|
||||
"integrity": "sha512-0u4D11yDu0NXF+8qp1eiQ/6cOwPI7sbu9/bGzUOhlwzKbB9FYFivgTtsVPtlkXAakfByWjOsLp2/Jx927OVetg==",
|
||||
"requires": {
|
||||
"readable-web-to-node-stream": "^2.0.0",
|
||||
"strtok3": "^6.0.3",
|
||||
"token-types": "^2.0.0",
|
||||
"typedarray-to-buffer": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
@@ -4853,6 +4838,11 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz",
|
||||
@@ -5882,11 +5872,6 @@
|
||||
"pify": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"peek-readable": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-3.1.0.tgz",
|
||||
"integrity": "sha512-KGuODSTV6hcgdZvDrIDBUkN0utcAVj1LL7FfGbM0viKTtCHmtZcuEJ+lGqsp0fTFkGqesdtemV2yUSMeyy3ddA=="
|
||||
},
|
||||
"pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
@@ -6229,11 +6214,6 @@
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"readable-web-to-node-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-+oZJurc4hXpaaqsN68GoZGQAQIA3qr09Or4fqEsargABnbe5Aau8hFn6ISVleT3cpY/0n/8drn7huyyEvTbghA=="
|
||||
},
|
||||
"rechoir": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz",
|
||||
@@ -6938,6 +6918,22 @@
|
||||
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
||||
"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": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
|
||||
@@ -7005,16 +7001,6 @@
|
||||
"resolved": "https://registry.npmjs.org/striptags/-/striptags-3.1.1.tgz",
|
||||
"integrity": "sha1-yMPn/db7S7OjKjt1LltePjgJPr0="
|
||||
},
|
||||
"strtok3": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.0.4.tgz",
|
||||
"integrity": "sha512-rqWMKwsbN9APU47bQTMEYTPcwdpKDtmf1jVhHzNW2cL1WqAxaM9iBb9t5P2fj+RV2YsErUWgQzHD5JwV0uCTEQ==",
|
||||
"requires": {
|
||||
"@tokenizer/token": "^0.1.1",
|
||||
"@types/debug": "^4.1.5",
|
||||
"peek-readable": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -7293,15 +7279,6 @@
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||
},
|
||||
"token-types": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-2.0.0.tgz",
|
||||
"integrity": "sha512-WWvu8sGK8/ZmGusekZJJ5NM6rRVTTDO7/bahz4NGiSDb/XsmdYBn6a1N/bymUHuWYTWeuLUg98wUzvE4jPdCZw==",
|
||||
"requires": {
|
||||
"@tokenizer/token": "^0.1.0",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
|
||||
|
||||
+3
-2
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.45.2",
|
||||
"version": "0.45.7",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -65,6 +65,7 @@
|
||||
"semver": "7.3.2",
|
||||
"serve-favicon": "2.5.0",
|
||||
"session-file-store": "1.5.0",
|
||||
"stream-throttle": "^0.1.3",
|
||||
"striptags": "3.1.1",
|
||||
"tmp": "^0.2.1",
|
||||
"turndown": "7.0.0",
|
||||
@@ -76,7 +77,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.2",
|
||||
"electron": "9.3.3",
|
||||
"electron": "9.3.5",
|
||||
"electron-builder": "22.9.1",
|
||||
"electron-packager": "15.1.0",
|
||||
"electron-rebuild": "2.3.2",
|
||||
|
||||
@@ -34,6 +34,10 @@ class Attribute extends Entity {
|
||||
this.isInheritable = !!this.isInheritable;
|
||||
}
|
||||
|
||||
isAutoLink() {
|
||||
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Note|null}
|
||||
*/
|
||||
|
||||
@@ -38,7 +38,7 @@ class Branch extends Entity {
|
||||
}
|
||||
|
||||
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]);
|
||||
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 10;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import utils from "../services/utils.js";
|
||||
import ws from "../services/ws.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import treeCache from "../services/tree_cache.js";
|
||||
import openService from "../services/open.js";
|
||||
|
||||
const $dialog = $("#export-dialog");
|
||||
const $form = $("#export-form");
|
||||
@@ -73,9 +74,9 @@ $form.on('submit', () => {
|
||||
function exportBranch(branchId, type, format, version) {
|
||||
taskId = utils.randomString(10);
|
||||
|
||||
const url = utils.getUrlForDownload(`api/notes/${branchId}/export/${type}/${format}/${version}/${taskId}`);
|
||||
const url = openService.getUrlForDownload(`api/notes/${branchId}/export/${type}/${format}/${version}/${taskId}`);
|
||||
|
||||
utils.download(url);
|
||||
openService.download(url);
|
||||
}
|
||||
|
||||
$('input[name=export-type]').on('change', function () {
|
||||
@@ -133,4 +134,4 @@ ws.subscribeToMessages(async message => {
|
||||
|
||||
toastService.showPersistent(toast);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@ import utils from '../services/utils.js';
|
||||
import server from '../services/server.js';
|
||||
import toastService from "../services/toast.js";
|
||||
import appContext from "../services/app_context.js";
|
||||
import libraryLoader from "../services/library_loader.js";
|
||||
import openService from "../services/open.js";
|
||||
|
||||
const $dialog = $("#note-revisions-dialog");
|
||||
const $list = $("#note-revision-list");
|
||||
@@ -120,11 +122,7 @@ async function setContentPane() {
|
||||
|
||||
const $downloadButton = $('<button class="btn btn-sm btn-primary" type="button">Download</button>');
|
||||
|
||||
$downloadButton.on('click', () => {
|
||||
const url = utils.getUrlForDownload(`api/notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}/download`);
|
||||
|
||||
utils.download(url);
|
||||
});
|
||||
$downloadButton.on('click', () => openService.downloadNoteRevision(revisionItem.noteId, revisionItem.noteRevisionId));
|
||||
|
||||
$titleButtons.append($downloadButton);
|
||||
|
||||
@@ -132,6 +130,12 @@ async function setContentPane() {
|
||||
|
||||
if (revisionItem.type === 'text') {
|
||||
$content.html(fullNoteRevision.content);
|
||||
|
||||
if ($content.find('span.math-tex').length > 0) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.KATEX);
|
||||
|
||||
renderMathInElement($content[0], {});
|
||||
}
|
||||
}
|
||||
else if (revisionItem.type === 'code') {
|
||||
$content.html($("<pre>").text(fullNoteRevision.content));
|
||||
|
||||
@@ -81,7 +81,7 @@ export default class KeyboardShortcutsOptions {
|
||||
.filter(shortcut => !!shortcut);
|
||||
|
||||
const opts = {};
|
||||
opts['keyboardShortcuts' + actionName] = JSON.stringify(shortcuts);
|
||||
opts['keyboardShortcuts' + actionName.substr(0, 1).toUpperCase() + actionName.substr(1)] = JSON.stringify(shortcuts);
|
||||
|
||||
server.put('options', opts);
|
||||
});
|
||||
@@ -138,4 +138,4 @@ export default class KeyboardShortcutsOptions {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -60,7 +60,7 @@ class NoteShort {
|
||||
/** @param {string} content-type, e.g. "application/json" */
|
||||
this.mime = row.mime;
|
||||
/** @param {boolean} */
|
||||
this.isDeleted = row.isDeleted;
|
||||
this.isDeleted = !!row.isDeleted;
|
||||
}
|
||||
|
||||
addParent(parentNoteId, branchId) {
|
||||
@@ -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() {
|
||||
|
||||
@@ -274,7 +274,10 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
*
|
||||
* @method
|
||||
* @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;
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ function linkContextMenu(e) {
|
||||
appContext.tabManager.openTabWithNote(notePath);
|
||||
}
|
||||
else if (command === 'openNoteInNewWindow') {
|
||||
appContext.openInNewWindow(notePath);
|
||||
appContext.triggerCommand('openInWindow', {notePath});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import server from "./server.js";
|
||||
import utils from "./utils.js";
|
||||
import renderService from "./render.js";
|
||||
import protectedSessionService from "./protected_session.js";
|
||||
import protectedSessionHolder from "./protected_session_holder.js";
|
||||
import libraryLoader from "./library_loader.js";
|
||||
import openService from "./open.js";
|
||||
|
||||
async function getRenderedContent(note) {
|
||||
const type = getRenderingType(note);
|
||||
@@ -32,24 +32,11 @@ async function getRenderedContent(note) {
|
||||
.css("max-width", "100%");
|
||||
}
|
||||
else if (type === 'file' || type === 'pdf') {
|
||||
function getFileUrl() {
|
||||
return utils.getUrlForDownload("api/notes/" + note.noteId + "/download");
|
||||
}
|
||||
|
||||
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
|
||||
const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
|
||||
|
||||
$downloadButton.on('click', () => utils.download(getFileUrl()));
|
||||
$openButton.on('click', () => {
|
||||
if (utils.isElectron()) {
|
||||
const open = utils.dynamicRequire("open");
|
||||
|
||||
open(getFileUrl(), {url: true});
|
||||
}
|
||||
else {
|
||||
window.location.href = getFileUrl();
|
||||
}
|
||||
});
|
||||
$downloadButton.on('click', () => openService.downloadFileNote(note.noteId));
|
||||
$openButton.on('click', () => openService.openFileNote(note.noteId));
|
||||
|
||||
// open doesn't work for protected notes since it works through browser which isn't in protected session
|
||||
$openButton.toggle(!note.isProtected);
|
||||
@@ -58,7 +45,7 @@ async function getRenderedContent(note) {
|
||||
|
||||
if (type === 'pdf') {
|
||||
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
|
||||
$pdfPreview.attr("src", utils.getUrlForDownload("api/notes/" + note.noteId + "/open"));
|
||||
$pdfPreview.attr("src", openService.getUrlForDownload("api/notes/" + note.noteId + "/open"));
|
||||
|
||||
$rendered.append($pdfPreview);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
@@ -102,5 +102,5 @@ async function duplicateNote(noteId, parentNoteId) {
|
||||
export default {
|
||||
createNote,
|
||||
createNewTopLevelNote,
|
||||
duplicateNote
|
||||
duplicateSubtree
|
||||
};
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import utils from "./utils.js";
|
||||
import server from "./server.js";
|
||||
|
||||
function getFileUrl(noteId) {
|
||||
return getUrlForDownload("api/notes/" + noteId + "/download");
|
||||
}
|
||||
|
||||
function download(url) {
|
||||
if (utils.isElectron()) {
|
||||
const remote = utils.dynamicRequire('electron').remote;
|
||||
|
||||
remote.getCurrentWebContents().downloadURL(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFileNote(noteId) {
|
||||
const url = getFileUrl(noteId) + '?' + Date.now(); // don't use cache
|
||||
|
||||
download(url);
|
||||
}
|
||||
|
||||
async function openFileNote(noteId) {
|
||||
if (utils.isElectron()) {
|
||||
const resp = await server.post("notes/" + noteId + "/saveToTmpDir");
|
||||
|
||||
const electron = utils.dynamicRequire('electron');
|
||||
const res = await electron.shell.openPath(resp.tmpFilePath);
|
||||
|
||||
if (res) {
|
||||
// fallback in case there's no default application for this file
|
||||
open(getFileUrl(noteId), {url: true});
|
||||
}
|
||||
}
|
||||
else {
|
||||
window.location.href = getFileUrl(noteId);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadNoteRevision(noteId, noteRevisionId) {
|
||||
const url = getUrlForDownload(`api/notes/${noteId}/revisions/${noteRevisionId}/download`);
|
||||
|
||||
download(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param url - should be without initial slash!!!
|
||||
*/
|
||||
function getUrlForDownload(url) {
|
||||
if (utils.isElectron()) {
|
||||
// electron needs absolute URL so we extract current host, port, protocol
|
||||
return getHost() + '/' + url;
|
||||
}
|
||||
else {
|
||||
// web server can be deployed on subdomain so we need to use relative path
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function getHost() {
|
||||
const url = new URL(window.location.href);
|
||||
return url.protocol + "//" + url.hostname + ":" + url.port;
|
||||
}
|
||||
|
||||
export default {
|
||||
download,
|
||||
downloadFileNote,
|
||||
openFileNote,
|
||||
downloadNoteRevision,
|
||||
getUrlForDownload
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 note(s) here", command: "duplicateNote", 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;
|
||||
|
||||
@@ -105,24 +105,6 @@ function formatLabel(label) {
|
||||
return str;
|
||||
}
|
||||
|
||||
function getHost() {
|
||||
const url = new URL(window.location.href);
|
||||
return url.protocol + "//" + url.hostname + ":" + url.port;
|
||||
}
|
||||
|
||||
function download(url) {
|
||||
url += '?' + Date.now(); // don't use cache
|
||||
|
||||
if (isElectron()) {
|
||||
const remote = dynamicRequire('electron').remote;
|
||||
|
||||
remote.getCurrentWebContents().downloadURL(url);
|
||||
}
|
||||
else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
function toObject(array, fn) {
|
||||
const obj = {};
|
||||
|
||||
@@ -294,20 +276,6 @@ async function clearBrowserCache() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param url - should be without initial slash!!!
|
||||
*/
|
||||
function getUrlForDownload(url) {
|
||||
if (isElectron()) {
|
||||
// electron needs absolute URL so we extract current host, port, protocol
|
||||
return getHost() + '/' + url;
|
||||
}
|
||||
else {
|
||||
// web server can be deployed on subdomain so we need to use relative path
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function copySelectionToClipboard() {
|
||||
const text = window.getSelection().toString();
|
||||
if (navigator.clipboard) {
|
||||
@@ -366,7 +334,6 @@ export default {
|
||||
escapeHtml,
|
||||
stopWatch,
|
||||
formatLabel,
|
||||
download,
|
||||
toObject,
|
||||
randomString,
|
||||
bindGlobalShortcut,
|
||||
@@ -384,7 +351,6 @@ export default {
|
||||
focusSavedElement,
|
||||
isHtmlEmpty,
|
||||
clearBrowserCache,
|
||||
getUrlForDownload,
|
||||
normalizeShortcut,
|
||||
copySelectionToClipboard,
|
||||
isCKEditorInitialized,
|
||||
|
||||
@@ -8,8 +8,6 @@ import options from "./options.js";
|
||||
import treeCache from "./tree_cache.js";
|
||||
import noteAttributeCache from "./note_attribute_cache.js";
|
||||
|
||||
const $outstandingSyncsCount = $("#outstanding-syncs-count");
|
||||
|
||||
const messageHandlers = [];
|
||||
|
||||
let ws;
|
||||
@@ -64,8 +62,6 @@ async function handleMessage(event) {
|
||||
let syncRows = message.data;
|
||||
lastPingTs = Date.now();
|
||||
|
||||
$outstandingSyncsCount.html(message.outstandingSyncs);
|
||||
|
||||
if (syncRows.length > 0) {
|
||||
logRows(syncRows);
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ function SetupModel() {
|
||||
}
|
||||
|
||||
async function checkOutstandingSyncs() {
|
||||
const { stats, initialized } = await $.get('api/sync/stats');
|
||||
const { outstandingPullCount, initialized } = await $.get('api/sync/stats');
|
||||
|
||||
if (initialized) {
|
||||
if (utils.isElectron()) {
|
||||
@@ -143,9 +143,7 @@ async function checkOutstandingSyncs() {
|
||||
}
|
||||
}
|
||||
else {
|
||||
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls;
|
||||
|
||||
$("#outstanding-syncs").html(totalOutstandingSyncs);
|
||||
$("#outstanding-syncs").html(outstandingPullCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -41,7 +41,7 @@ const TPL = `
|
||||
|
||||
<a class="dropdown-item sync-now-button" title="Trigger sync">
|
||||
<span class="bx bx-refresh"></span>
|
||||
Sync now (<span id="outstanding-syncs-count">0</span>)
|
||||
Sync now
|
||||
</a>
|
||||
|
||||
<a class="dropdown-item" data-trigger-command="openNewWindow">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1341,7 +1341,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
protectedSessionService.protectNote(node.data.noteId, false, true);
|
||||
}
|
||||
|
||||
duplicateNoteCommand({node}) {
|
||||
duplicateSubtreeCommand({node}) {
|
||||
const nodesToDuplicate = this.getSelectedOrActiveNodes(node);
|
||||
|
||||
for (const nodeToDuplicate of nodesToDuplicate) {
|
||||
@@ -1353,7 +1353,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
|
||||
const branch = treeCache.getBranch(nodeToDuplicate.data.branchId);
|
||||
|
||||
noteCreateService.duplicateNote(nodeToDuplicate.data.noteId, branch.parentNoteId);
|
||||
noteCreateService.duplicateSubtree(nodeToDuplicate.data.noteId, branch.parentNoteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import utils from "../../services/utils.js";
|
||||
import openService from "../../services/open.js";
|
||||
import server from "../../services/server.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import TypeWidget from "./type_widget.js";
|
||||
@@ -73,24 +74,8 @@ export default class FileTypeWidget extends TypeWidget {
|
||||
this.$uploadNewRevisionButton = this.$widget.find(".file-upload-new-revision");
|
||||
this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input");
|
||||
|
||||
this.$downloadButton.on('click', () => utils.download(this.getFileUrl()));
|
||||
|
||||
this.$openButton.on('click', async () => {
|
||||
if (utils.isElectron()) {
|
||||
const resp = await server.post("notes/" + this.noteId + "/saveToTmpDir");
|
||||
|
||||
const electron = utils.dynamicRequire('electron');
|
||||
const res = await electron.shell.openPath(resp.tmpFilePath);
|
||||
|
||||
if (res) {
|
||||
// fallback in case there's no default application for this file
|
||||
open(this.getFileUrl(), {url: true});
|
||||
}
|
||||
}
|
||||
else {
|
||||
window.location.href = this.getFileUrl();
|
||||
}
|
||||
});
|
||||
this.$downloadButton.on('click', () => openService.downloadFileNote(this.noteId));
|
||||
this.$openButton.on('click', () => openService.openFileNote(this.noteId));
|
||||
|
||||
this.$uploadNewRevisionButton.on("click", () => {
|
||||
this.$uploadNewRevisionInput.trigger("click");
|
||||
@@ -146,14 +131,10 @@ export default class FileTypeWidget extends TypeWidget {
|
||||
}
|
||||
else if (note.mime === 'application/pdf') {
|
||||
this.$pdfPreview.show();
|
||||
this.$pdfPreview.attr("src", utils.getUrlForDownload("api/notes/" + this.noteId + "/open"));
|
||||
this.$pdfPreview.attr("src", openService.getUrlForDownload("api/notes/" + this.noteId + "/open"));
|
||||
}
|
||||
|
||||
// open doesn't work for protected notes since it works through browser which isn't in protected session
|
||||
this.$openButton.toggle(!note.isProtected);
|
||||
}
|
||||
|
||||
getFileUrl() {
|
||||
return utils.getUrlForDownload("api/notes/" + this.noteId + "/download");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import utils from "../../services/utils.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import server from "../../services/server.js";
|
||||
import openService from "../../services/open.js";
|
||||
import TypeWidget from "./type_widget.js";
|
||||
|
||||
const TPL = `
|
||||
@@ -64,7 +65,7 @@ class ImageTypeWidget extends TypeWidget {
|
||||
this.$fileSize = this.$widget.find(".image-filesize");
|
||||
|
||||
this.$imageDownloadButton = this.$widget.find(".image-download");
|
||||
this.$imageDownloadButton.on('click', () => utils.download(this.getFileUrl()));
|
||||
this.$imageDownloadButton.on('click', () => openService.downloadFileNote(this.noteId));
|
||||
|
||||
this.$copyToClipboardButton.on('click',() => {
|
||||
this.$imageWrapper.attr('contenteditable','true');
|
||||
@@ -145,10 +146,6 @@ class ImageTypeWidget extends TypeWidget {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
getFileUrl() {
|
||||
return utils.getUrlForDownload(`api/notes/${this.noteId}/download`);
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageTypeWidget
|
||||
|
||||
@@ -59,7 +59,7 @@ ul.fancytree-container {
|
||||
font-size: x-large;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
content: "\ea1d";
|
||||
content: "\e9b2";
|
||||
position: relative;
|
||||
top: -2px;
|
||||
margin-right: 5px;
|
||||
@@ -72,7 +72,7 @@ ul.fancytree-container {
|
||||
|
||||
.fancytree-node.fancytree-expanded .fancytree-expander:before {
|
||||
font-family: 'boxicons' !important;
|
||||
content: "\ea17";
|
||||
content: "\e9ac";
|
||||
}
|
||||
|
||||
/** some common text styling for cssClass label */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -28,8 +28,10 @@ function updateNoteAttribute(req) {
|
||||
|| body.name !== attribute.name
|
||||
|| (body.type === 'relation' && body.value !== attribute.value)) {
|
||||
|
||||
let newAttribute;
|
||||
|
||||
if (body.type !== 'relation' || !!body.value.trim()) {
|
||||
const newAttribute = attribute.createClone(body.type, body.name, body.value);
|
||||
newAttribute = attribute.createClone(body.type, body.name, body.value);
|
||||
newAttribute.save();
|
||||
}
|
||||
|
||||
@@ -37,7 +39,7 @@ function updateNoteAttribute(req) {
|
||||
attribute.save();
|
||||
|
||||
return {
|
||||
attributeId: attribute.attributeId
|
||||
attributeId: newAttribute ? newAttribute.attributeId : null
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -52,8 +54,9 @@ function updateNoteAttribute(req) {
|
||||
attribute.type = body.type;
|
||||
}
|
||||
|
||||
if (body.value.trim()) {
|
||||
if (body.type !== 'relation' || body.value.trim()) {
|
||||
attribute.value = body.value;
|
||||
attribute.isDeleted = false;
|
||||
}
|
||||
else {
|
||||
// relations should never have empty target
|
||||
@@ -144,8 +147,10 @@ function updateNoteAttributes(req) {
|
||||
|
||||
// all the remaining existing attributes are not defined anymore and should be deleted
|
||||
for (const toDeleteAttr of existingAttrs) {
|
||||
toDeleteAttr.isDeleted = true;
|
||||
toDeleteAttr.save();
|
||||
if (!toDeleteAttr.isAutoLink()) {
|
||||
toDeleteAttr.isDeleted = true;
|
||||
toDeleteAttr.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -187,10 +187,14 @@ function changeTitle(req) {
|
||||
return note;
|
||||
}
|
||||
|
||||
function duplicateNote(req) {
|
||||
function duplicateSubtree(req) {
|
||||
const {noteId, parentNoteId} = req.params;
|
||||
|
||||
return noteService.duplicateNote(noteId, parentNoteId);
|
||||
return noteService.duplicateSubtree(noteId, parentNoteId);
|
||||
}
|
||||
|
||||
function eraseDeletedNotesNow() {
|
||||
noteService.eraseDeletedNotesNow();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -204,5 +208,6 @@ module.exports = {
|
||||
setNoteTypeMime,
|
||||
getRelationMap,
|
||||
changeTitle,
|
||||
duplicateNote
|
||||
duplicateSubtree,
|
||||
eraseDeletedNotesNow
|
||||
};
|
||||
|
||||
@@ -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(`
|
||||
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.utcDateCreated AS utcDate,
|
||||
notes.dateCreated AS date
|
||||
FROM
|
||||
notes`);
|
||||
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.utcDateCreated AS utcDate,
|
||||
notes.dateCreated AS date
|
||||
FROM
|
||||
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) {
|
||||
if (noteCacheService.isInAncestor(note.noteId, ancestorNoteId)) {
|
||||
|
||||
@@ -4,6 +4,7 @@ const imageType = require('image-type');
|
||||
const imageService = require('../../services/image');
|
||||
const dateNoteService = require('../../services/date_notes');
|
||||
const noteService = require('../../services/notes');
|
||||
const attributeService = require('../../services/attributes');
|
||||
|
||||
function uploadImage(req) {
|
||||
const file = req.file;
|
||||
@@ -37,7 +38,7 @@ function saveNote(req) {
|
||||
|
||||
if (req.body.labels) {
|
||||
for (const {name, value} of req.body.labels) {
|
||||
note.setLabel(name, value);
|
||||
note.setLabel(attributeService.sanitizeAttributeName(name), value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ function saveSyncSeed(req) {
|
||||
}]
|
||||
}
|
||||
|
||||
log.info("Saved sync seed.");
|
||||
|
||||
sqlInit.createDatabaseForSync(options);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ const dateUtils = require('../../services/date_utils');
|
||||
const entityConstructor = require('../../entities/entity_constructor');
|
||||
const utils = require('../../services/utils');
|
||||
|
||||
function testSync() {
|
||||
async function testSync() {
|
||||
try {
|
||||
if (!syncOptions.isSyncSetup()) {
|
||||
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
|
||||
// this is important in case when sync server has been just initialized
|
||||
@@ -43,7 +43,7 @@ function getStats() {
|
||||
|
||||
const stats = {
|
||||
initialized: optionService.getOption('initialized') === 'true',
|
||||
stats: syncService.stats
|
||||
outstandingPullCount: syncService.getOutstandingPullCount()
|
||||
};
|
||||
|
||||
log.info(`Returning sync stats: ${JSON.stringify(stats)}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,10 @@ async function handleRequest(req, res) {
|
||||
const attrs = repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')");
|
||||
|
||||
for (const attr of attrs) {
|
||||
if (!attr.value.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const regex = new RegExp(attr.value);
|
||||
let match;
|
||||
|
||||
|
||||
@@ -153,8 +153,9 @@ 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.duplicateNote);
|
||||
apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree);
|
||||
|
||||
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
const repository = require('./repository');
|
||||
const sql = require('./sql');
|
||||
const utils = require('./utils');
|
||||
const Attribute = require('../entities/attribute');
|
||||
|
||||
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 = {
|
||||
getNotesWithLabel,
|
||||
getNotesWithLabels,
|
||||
@@ -156,5 +169,6 @@ module.exports = {
|
||||
getAttributeNames,
|
||||
isAttributeType,
|
||||
isAttributeDangerous,
|
||||
getBuiltinAttributeNames
|
||||
getBuiltinAttributeNames,
|
||||
sanitizeAttributeName
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2020-10-29T22:57:25+01:00", buildRevision: "4f75b6aaafef8144080fd17f403a605f61f5590d" };
|
||||
module.exports = { buildDate:"2020-12-22T20:21:15+01:00", buildRevision: "bdfd760b9d428dc29c45a0e016d12a25af220043" };
|
||||
|
||||
@@ -11,6 +11,7 @@ const entityChangesService = require('./entity_changes.js');
|
||||
const optionsService = require('./options');
|
||||
const Branch = require('../entities/branch');
|
||||
const dateUtils = require('./date_utils');
|
||||
const attributeService = require('./attributes');
|
||||
|
||||
class ConsistencyChecks {
|
||||
constructor(autoFix) {
|
||||
@@ -607,20 +608,10 @@ class ConsistencyChecks {
|
||||
findWronglyNamedAttributes() {
|
||||
const attrNames = sql.getColumn(`SELECT DISTINCT name FROM attributes`);
|
||||
|
||||
const attrNameMatcher = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
|
||||
|
||||
for (const origName of attrNames) {
|
||||
if (!attrNameMatcher.test(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, "_");
|
||||
}
|
||||
const fixedName = attributeService.sanitizeAttributeName(origName);
|
||||
|
||||
if (fixedName !== origName) {
|
||||
if (this.autoFix) {
|
||||
// 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
|
||||
@@ -659,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();
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const eventService = require('./events');
|
||||
const scriptService = require('./script');
|
||||
const treeService = require('./tree');
|
||||
const log = require('./log');
|
||||
const noteService = require('./notes');
|
||||
const repository = require('./repository');
|
||||
const Attribute = require('../entities/attribute');
|
||||
|
||||
@@ -58,17 +58,25 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNote = repository.getNote(entity.value);
|
||||
const templateNote = repository.getNote(entity.value);
|
||||
|
||||
if (!targetNote || !targetNote.isStringNote()) {
|
||||
if (!templateNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNoteContent = targetNote.getContent();
|
||||
if (templateNote.isStringNote()) {
|
||||
const templateNoteContent = templateNote.getContent();
|
||||
|
||||
if (targetNoteContent) {
|
||||
note.setContent(targetNoteContent);
|
||||
if (templateNoteContent) {
|
||||
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') {
|
||||
treeService.sortNotesAlphabetically(entity.noteId);
|
||||
@@ -86,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();
|
||||
|
||||
@@ -5,8 +5,7 @@ const sanitizeHtml = require('sanitize-html');
|
||||
function sanitize(dirtyHtml) {
|
||||
return sanitizeHtml(dirtyHtml, {
|
||||
allowedTags: [
|
||||
// h1 is removed since that should be note's title
|
||||
'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'li', 'b', 'i', 'strong', 'em', 'strike', 'abbr', 'code', 'hr', 'br', 'div',
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'section', 'img',
|
||||
'figure', 'span', 'label', 'input'
|
||||
|
||||
@@ -37,7 +37,7 @@ function getImageType(buffer) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
return imageType(buffer);
|
||||
return imageType(buffer) || "jpg"; // optimistic JPG default
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const sax = require("sax");
|
||||
const stream = require('stream');
|
||||
const {Throttle} = require('stream-throttle');
|
||||
const log = require("../log");
|
||||
const utils = require("../utils");
|
||||
const sql = require("../sql");
|
||||
@@ -7,6 +8,7 @@ const noteService = require("../notes");
|
||||
const imageService = require("../image");
|
||||
const protectedSessionService = require('../protected_session');
|
||||
const htmlSanitizer = require("../html_sanitizer");
|
||||
const attributeService = require("../attributes");
|
||||
|
||||
// date format is e.g. 20181121T193703Z
|
||||
function parseDate(text) {
|
||||
@@ -37,10 +39,6 @@ function importEnex(taskContext, file, parentNote) {
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
})).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) {
|
||||
const openingNoteIndex = content.indexOf('<en-note>');
|
||||
|
||||
@@ -105,9 +103,17 @@ function importEnex(taskContext, file, parentNote) {
|
||||
const previousTag = getPreviousTag();
|
||||
|
||||
if (previousTag === 'note-attributes') {
|
||||
let labelName = currentTag;
|
||||
|
||||
if (labelName === 'source-url') {
|
||||
labelName = 'sourceUrl';
|
||||
}
|
||||
|
||||
labelName = attributeService.sanitizeAttributeName(labelName);
|
||||
|
||||
note.attributes.push({
|
||||
type: 'label',
|
||||
name: currentTag,
|
||||
name: labelName,
|
||||
value: text
|
||||
});
|
||||
}
|
||||
@@ -149,7 +155,7 @@ function importEnex(taskContext, file, parentNote) {
|
||||
} else if (currentTag === 'tag') {
|
||||
note.attributes.push({
|
||||
type: 'label',
|
||||
name: text,
|
||||
name: attributeService.sanitizeAttributeName(text),
|
||||
value: ''
|
||||
})
|
||||
}
|
||||
@@ -227,6 +233,10 @@ function importEnex(taskContext, file, parentNote) {
|
||||
taskContext.increaseProgressCount();
|
||||
|
||||
for (const resource of resources) {
|
||||
if (!resource.content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hash = utils.md5(resource.content);
|
||||
|
||||
const mediaRegex = new RegExp(`<en-media hash="${hash}"[^>]*>`, 'g');
|
||||
@@ -304,7 +314,7 @@ function importEnex(taskContext, file, parentNote) {
|
||||
path.pop();
|
||||
|
||||
if (tag === 'note') {
|
||||
saveNotePromises.push(saveNote());
|
||||
saveNote();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -323,12 +333,15 @@ function importEnex(taskContext, file, parentNote) {
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
// 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();
|
||||
bufferStream.end(file.buffer);
|
||||
|
||||
bufferStream.pipe(saxStream);
|
||||
bufferStream
|
||||
// rate limiting to improve responsiveness during / after import
|
||||
.pipe(new Throttle({rate: 500000}))
|
||||
.pipe(saxStream);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,8 @@ function convertTextToHtml(text) {
|
||||
}
|
||||
|
||||
function importMarkdown(taskContext, file, parentNote) {
|
||||
const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
|
||||
|
||||
const markdownContent = file.buffer.toString("UTF-8");
|
||||
|
||||
const reader = new commonmark.Parser();
|
||||
@@ -127,7 +129,7 @@ function importMarkdown(taskContext, file, parentNote) {
|
||||
|
||||
htmlContent = htmlSanitizer.sanitize(htmlContent);
|
||||
|
||||
const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
|
||||
htmlContent = handleH1(htmlContent, title);
|
||||
|
||||
const {note} = noteService.createNewNote({
|
||||
parentNoteId: parentNote.noteId,
|
||||
@@ -143,12 +145,25 @@ function importMarkdown(taskContext, file, parentNote) {
|
||||
return note;
|
||||
}
|
||||
|
||||
function handleH1(content, title) {
|
||||
content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => {
|
||||
if (title.trim() === text.trim()) {
|
||||
return ""; // remove whole H1 tag
|
||||
} else {
|
||||
return `<h2>${text}</h2>`;
|
||||
}
|
||||
});
|
||||
return content;
|
||||
}
|
||||
|
||||
function importHtml(taskContext, file, parentNote) {
|
||||
const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
|
||||
let content = file.buffer.toString("UTF-8");
|
||||
|
||||
content = htmlSanitizer.sanitize(content);
|
||||
|
||||
content = handleH1(content, title);
|
||||
|
||||
const {note} = noteService.createNewNote({
|
||||
parentNoteId: parentNote.noteId,
|
||||
title,
|
||||
|
||||
@@ -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);
|
||||
@@ -275,7 +275,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
||||
return ""; // remove whole H1 tag
|
||||
}
|
||||
else {
|
||||
return match;
|
||||
return `<h2>${text}</h2>`;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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)"
|
||||
@@ -365,7 +377,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
},
|
||||
{
|
||||
actionName: "openDevTools",
|
||||
defaultShortcuts: ["CommandOrControl+Shift+I"],
|
||||
defaultShortcuts: isElectron ? ["CommandOrControl+Shift+I"] : [],
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
@@ -408,13 +420,7 @@ for (const action of DEFAULT_KEYBOARD_ACTIONS) {
|
||||
}
|
||||
}
|
||||
|
||||
let cachedActions = null;
|
||||
|
||||
function getKeyboardActions() {
|
||||
if (cachedActions) {
|
||||
return cachedActions;
|
||||
}
|
||||
|
||||
const actions = JSON.parse(JSON.stringify(DEFAULT_KEYBOARD_ACTIONS));
|
||||
|
||||
for (const action of actions) {
|
||||
@@ -442,8 +448,6 @@ function getKeyboardActions() {
|
||||
}
|
||||
}
|
||||
|
||||
cachedActions = actions;
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+104
-23
@@ -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);
|
||||
|
||||
@@ -687,7 +696,7 @@ function eraseDeletedNotes() {
|
||||
|
||||
sql.executeMany(`
|
||||
UPDATE notes
|
||||
SET title = '[deleted]',
|
||||
SET title = '[erased]',
|
||||
isProtected = 0,
|
||||
isErased = 1
|
||||
WHERE noteId IN (???)`, noteIdsToErase);
|
||||
@@ -719,26 +728,75 @@ function eraseDeletedNotes() {
|
||||
log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`);
|
||||
}
|
||||
|
||||
function duplicateNote(noteId, parentNoteId) {
|
||||
const origNote = repository.getNote(noteId);
|
||||
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");
|
||||
|
||||
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()) {
|
||||
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);
|
||||
newNote.noteId = undefined; // force creation of new note
|
||||
newNote.title += " (dup)";
|
||||
newNote.noteId = noteIdMapping[origNote.noteId];
|
||||
newNote.dateCreated = dateUtils.localNowDateTime();
|
||||
newNote.utcDateCreated = dateUtils.utcNowDateTime();
|
||||
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({
|
||||
noteId: newNote.noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
parentNoteId: newParentNoteId,
|
||||
// here increasing just by 1 to make sure it's directly after original
|
||||
notePosition: origBranch ? origBranch.notePosition + 1 : null
|
||||
}).save();
|
||||
@@ -746,22 +804,43 @@ function duplicateNote(noteId, parentNoteId) {
|
||||
for (const attribute of origNote.getOwnedAttributes()) {
|
||||
const attr = new Attribute(attribute);
|
||||
attr.attributeId = undefined; // force creation of new attribute
|
||||
attr.utcDateCreated = dateUtils.utcNowDateTime();
|
||||
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();
|
||||
}
|
||||
|
||||
for (const childBranch of origNote.getChildBranches()) {
|
||||
duplicateSubtreeInner(childBranch.getNote(), childBranch, newNote.noteId, noteIdMapping);
|
||||
}
|
||||
|
||||
return {
|
||||
note: newNote,
|
||||
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(() => {
|
||||
// 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 = {
|
||||
@@ -772,7 +851,9 @@ module.exports = {
|
||||
undeleteNote,
|
||||
protectNoteRecursively,
|
||||
scanForLinks,
|
||||
duplicateNote,
|
||||
duplicateSubtree,
|
||||
duplicateSubtreeWithoutRoot,
|
||||
getUndeletedParentBranches,
|
||||
triggerNoteTitleChanged
|
||||
triggerNoteTitleChanged,
|
||||
eraseDeletedNotesNow
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,10 +17,14 @@ const utils = require('../../utils.js');
|
||||
*/
|
||||
function findNotesWithExpression(expression) {
|
||||
const hoistedNote = noteCache.notes[hoistedNoteService.getHoistedNoteId()];
|
||||
const allNotes = (hoistedNote && hoistedNote.noteId !== 'root')
|
||||
let allNotes = (hoistedNote && hoistedNote.noteId !== 'root')
|
||||
? hoistedNote.subtreeNotes
|
||||
: 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 searchContext = {
|
||||
|
||||
+9
-22
@@ -20,10 +20,7 @@ const entityConstructor = require('../entities/entity_constructor');
|
||||
|
||||
let proxyToggle = true;
|
||||
|
||||
const stats = {
|
||||
outstandingPushes: 0,
|
||||
outstandingPulls: 0
|
||||
};
|
||||
let outstandingPullCount = 0;
|
||||
|
||||
async function sync() {
|
||||
try {
|
||||
@@ -135,11 +132,7 @@ async function pullChanges(syncContext) {
|
||||
|
||||
const pulledDate = Date.now();
|
||||
|
||||
stats.outstandingPulls = resp.maxEntityChangeId - lastSyncedPull;
|
||||
|
||||
if (stats.outstandingPulls < 0) {
|
||||
stats.outstandingPulls = 0;
|
||||
}
|
||||
outstandingPullCount = Math.max(0, resp.maxEntityChangeId - lastSyncedPull);
|
||||
|
||||
const {entityChanges} = resp;
|
||||
|
||||
@@ -159,13 +152,13 @@ async function pullChanges(syncContext) {
|
||||
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);
|
||||
});
|
||||
|
||||
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) {
|
||||
@@ -359,31 +352,25 @@ function setLastSyncedPush(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() {
|
||||
return sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes');
|
||||
}
|
||||
|
||||
function getOutstandingPullCount() {
|
||||
return outstandingPullCount;
|
||||
}
|
||||
|
||||
sqlInit.dbReady.then(() => {
|
||||
setInterval(cls.wrap(sync), 60000);
|
||||
|
||||
// kickoff initial sync immediately
|
||||
setTimeout(cls.wrap(sync), 3000);
|
||||
|
||||
setInterval(cls.wrap(updatePushStats), 1000);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
sync,
|
||||
login,
|
||||
getEntityChangesRecords,
|
||||
stats,
|
||||
getOutstandingPullCount,
|
||||
getMaxEntityChangeId
|
||||
};
|
||||
|
||||
+1
-2
@@ -110,8 +110,7 @@ function sendPing(client, syncRows = []) {
|
||||
|
||||
sendMessage(client, {
|
||||
type: 'sync',
|
||||
data: syncRows,
|
||||
outstandingSyncs: stats.outstandingPushes + stats.outstandingPulls
|
||||
data: syncRows
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user