mirror of
https://github.com/zadam/trilium.git
synced 2025-10-29 17:26:38 +01:00
Compare commits
74 Commits
v0.45.0-be
...
v0.45.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6afc299efb | ||
|
|
369274ead7 | ||
|
|
04e6431c09 | ||
|
|
e89057a771 | ||
|
|
4f27254e64 | ||
|
|
577dc95ab8 | ||
|
|
a266d6a3d5 | ||
|
|
749b6cb57e | ||
|
|
b0b2951ff6 | ||
|
|
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 | ||
|
|
af41e5d115 | ||
|
|
4f75b6aaaf | ||
|
|
82f410f695 | ||
|
|
2bc06959c3 | ||
|
|
b898973ee6 | ||
|
|
a2b0d8a379 | ||
|
|
06a4eab7d5 | ||
|
|
25df1a054c | ||
|
|
8c4ff7ed2a | ||
|
|
609829653e | ||
|
|
5f20d033a8 | ||
|
|
93d0324177 | ||
|
|
0afd3c65aa | ||
|
|
8901c3ec91 | ||
|
|
c671b0a345 | ||
|
|
9f424836e2 | ||
|
|
7f5af4b959 |
18
.idea/codeStyles/Project.xml
generated
18
.idea/codeStyles/Project.xml
generated
@@ -9,23 +9,5 @@
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="USE_EXPLICIT_JS_EXTENSION" value="TRUE" />
|
||||
</JSCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="java.util" alias="false" withSubpackages="false" />
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
<package name="java" alias="false" withSubpackages="true" />
|
||||
<package name="javax" alias="false" withSubpackages="true" />
|
||||
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||
<package name="" alias="true" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
</JetCodeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:12.16.3-alpine
|
||||
FROM node:12.19.0-alpine
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -5,7 +5,7 @@ SERIES=${VERSION:0:4}-latest
|
||||
|
||||
cat package.json | grep -v electron > server-package.json
|
||||
|
||||
sudo docker build -t zadam/trilium:$VERSION -t zadam/trilium:$SERIES .
|
||||
sudo docker build -t zadam/trilium:$VERSION --network host -t zadam/trilium:$SERIES .
|
||||
|
||||
if [[ $VERSION != *"beta"* ]]; then
|
||||
sudo docker tag zadam/trilium:$VERSION zadam/trilium:latest
|
||||
|
||||
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2
libraries/boxicons/css/boxicons.min.css
vendored
2
libraries/boxicons/css/boxicons.min.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
libraries/ckeditor/ckeditor.js
vendored
2
libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
77
package-lock.json
generated
77
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.44.9",
|
||||
"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.2",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.2.tgz",
|
||||
"integrity": "sha512-0lleEf9msAXGDi2GukAuiGdw3VDgSTlONOnJgqDEz1fuSEVsXz5RX+hNPKDsVDerLTFg/C34RuJf4LwHvkKcBA==",
|
||||
"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",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.45.0-beta",
|
||||
"version": "0.45.8",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -40,7 +40,6 @@
|
||||
"electron-window-state": "5.0.3",
|
||||
"express": "4.17.1",
|
||||
"express-session": "1.17.1",
|
||||
"file-type": "16.0.0",
|
||||
"fs-extra": "9.0.1",
|
||||
"helmet": "4.1.1",
|
||||
"html": "1.0.0",
|
||||
@@ -66,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",
|
||||
@@ -77,7 +77,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.2",
|
||||
"electron": "9.3.2",
|
||||
"electron": "9.3.5",
|
||||
"electron-builder": "22.9.1",
|
||||
"electron-packager": "15.1.0",
|
||||
"electron-rebuild": "2.3.2",
|
||||
|
||||
@@ -57,8 +57,15 @@ function id() {
|
||||
return randtoken.generate(10);
|
||||
}
|
||||
|
||||
function note(title, type = 'text', mime = 'text/html') {
|
||||
const note = new Note(noteCache, {noteId: id(), title, type, mime});
|
||||
function note(title, extraParams = {}) {
|
||||
const row = Object.assign({
|
||||
noteId: id(),
|
||||
title: title,
|
||||
type: 'text',
|
||||
mime: 'text/html'
|
||||
}, extraParams);
|
||||
|
||||
const note = new Note(noteCache, row);
|
||||
|
||||
return new NoteBuilder(note);
|
||||
}
|
||||
|
||||
@@ -247,6 +247,6 @@ describe("Invalid expressions", () => {
|
||||
searchContext
|
||||
});
|
||||
|
||||
expect(searchContext.error).toEqual('Misplaced or incomplete expression "="')
|
||||
expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""')
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,8 +53,8 @@ describe("Search", () => {
|
||||
|
||||
it("normal search looks also at type and mime", () => {
|
||||
rootNote
|
||||
.child(note("Effective Java", 'book', ''))
|
||||
.child(note("Hello World.java", 'code', 'text/x-java'));
|
||||
.child(note("Effective Java", {type: 'book', mime:''}))
|
||||
.child(note("Hello World.java", {type: 'code', mime: 'text/x-java'}));
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
let searchResults = searchService.findNotesWithQuery('book', searchContext);
|
||||
@@ -178,7 +178,7 @@ describe("Search", () => {
|
||||
// dates should not be coerced into numbers which would then give wrong numbers
|
||||
|
||||
rootNote
|
||||
.child(note("My note")
|
||||
.child(note("My note", {dateCreated: dateUtils.localNowDateTime()})
|
||||
.label('year', new Date().getFullYear().toString())
|
||||
.label('month', dateUtils.localNowDate().substr(0, 7))
|
||||
.label('date', dateUtils.localNowDate())
|
||||
@@ -209,6 +209,8 @@ describe("Search", () => {
|
||||
test("#month = month", 1);
|
||||
test("#month = 'MONTH'", 0);
|
||||
|
||||
test("note.dateCreated =* month", 1);
|
||||
|
||||
test("#date = TODAY", 1);
|
||||
test("#date = today", 1);
|
||||
test("#date = 'today'", 0);
|
||||
@@ -586,7 +588,7 @@ describe("Search", () => {
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
|
||||
let searchResults = searchService.findNotesWithQuery('# note.text *=* rati and note.noteId != root', searchContext);
|
||||
let searchResults = searchService.findNotesWithQuery('# note.text *=* vaki and note.noteId != root', searchContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Slovakia");
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -8,6 +8,11 @@ const TPL = `
|
||||
<p>Your username is <strong id="credentials-username"></strong>.</p>
|
||||
|
||||
<h3>Change password</h3>
|
||||
|
||||
<div class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;">
|
||||
Please take care to remember your new password. Password is used to encrypt protected notes. If you forget your password, then all your protected notes are forever lost with no recovery options.
|
||||
</div>
|
||||
|
||||
<form id="change-password-form">
|
||||
<div class="form-group">
|
||||
<label for="old-password">Old password</label>
|
||||
|
||||
@@ -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,8 +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);
|
||||
@@ -13,6 +14,12 @@ async function getRenderedContent(note) {
|
||||
const fullNote = await server.get('notes/' + note.noteId);
|
||||
|
||||
$rendered = $('<div class="ck-content">').html(fullNote.content);
|
||||
|
||||
if ($rendered.find('span.math-tex').length > 0) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.KATEX);
|
||||
|
||||
renderMathInElement($rendered[0], {});
|
||||
}
|
||||
}
|
||||
else if (type === 'code') {
|
||||
const fullNote = await server.get('notes/' + note.noteId);
|
||||
@@ -25,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);
|
||||
@@ -51,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
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import linkService from "./link.js";
|
||||
import treeCache from "./tree_cache.js";
|
||||
import utils from "./utils.js";
|
||||
import attributeRenderer from "./attribute_renderer.js";
|
||||
import libraryLoader from "./library_loader.js";
|
||||
|
||||
function setupGlobalTooltip() {
|
||||
$(document).on("mouseenter", "a", mouseEnterHandler);
|
||||
@@ -101,7 +102,15 @@ async function renderTooltip(note, noteComplement) {
|
||||
}
|
||||
|
||||
if (note.type === 'text' && !utils.isHtmlEmpty(noteComplement.content)) {
|
||||
content += '<div class="ck-content">' + noteComplement.content + '</div>';
|
||||
const $content = $('<div class="ck-content">').append(noteComplement.content);
|
||||
|
||||
if ($content.find('span.math-tex').length > 0) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.KATEX);
|
||||
|
||||
renderMathInElement($content[0], {});
|
||||
}
|
||||
|
||||
content += $content[0].outerHTML;
|
||||
}
|
||||
else if (note.type === 'code' && noteComplement.content && noteComplement.content.trim()) {
|
||||
content += $("<pre>")
|
||||
|
||||
72
src/public/app/services/open.js
Normal file
72
src/public/app/services/open.js
Normal file
@@ -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);
|
||||
|
||||
@@ -115,13 +115,13 @@ export default class TabManager extends Component {
|
||||
|
||||
// using pushState instead of directly modifying document.location because it does not trigger hashchange
|
||||
window.history.pushState(null, "", url);
|
||||
}
|
||||
|
||||
document.title = "Trilium Notes";
|
||||
document.title = "Trilium Notes";
|
||||
|
||||
if (activeTabContext.note) {
|
||||
// it helps navigating in history if note title is included in the title
|
||||
document.title += " - " + activeTabContext.note.title;
|
||||
}
|
||||
if (activeTabContext.note) {
|
||||
// it helps navigating in history if note title is included in the title
|
||||
document.title += " - " + activeTabContext.note.title;
|
||||
}
|
||||
|
||||
this.triggerEvent('activeNoteChanged'); // trigger this even in on popstate event
|
||||
|
||||
@@ -54,7 +54,7 @@ function closePersistent(id) {
|
||||
}
|
||||
|
||||
function showMessage(message, delay = 2000) {
|
||||
console.debug(utils.now(), "message: ", message);
|
||||
console.debug(utils.now(), "message:", message);
|
||||
|
||||
toast({
|
||||
title: "Info",
|
||||
|
||||
@@ -20,6 +20,9 @@ class TreeCache {
|
||||
async loadInitialTree() {
|
||||
const resp = await server.get('tree');
|
||||
|
||||
// FIXME: we need to do this to cover for ascendants of template notes which are not loaded
|
||||
await this.loadParents(resp, false);
|
||||
|
||||
// clear the cache only directly before adding new content which is important for e.g. switching to protected session
|
||||
|
||||
/** @type {Object.<string, NoteShort>} */
|
||||
@@ -40,6 +43,8 @@ class TreeCache {
|
||||
async loadSubTree(subTreeNoteId) {
|
||||
const resp = await server.get('tree?subTreeNoteId=' + subTreeNoteId);
|
||||
|
||||
await this.loadParents(resp, true);
|
||||
|
||||
this.addResp(resp);
|
||||
|
||||
return this.notes[subTreeNoteId];
|
||||
@@ -82,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;
|
||||
|
||||
@@ -148,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,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) {
|
||||
@@ -191,13 +205,18 @@ class TreeCache {
|
||||
if (note.type === 'search') {
|
||||
const searchResultNoteIds = await server.get('search-note/' + note.noteId);
|
||||
|
||||
if (!searchResultNoteIds) {
|
||||
throw new Error(`Search note ${note.noteId} failed.`);
|
||||
if (!Array.isArray(searchResultNoteIds)) {
|
||||
throw new Error(`Search note ${note.noteId} failed: ${searchResultNoteIds}`);
|
||||
}
|
||||
|
||||
// force to load all the notes at once instead of one by one
|
||||
await this.getNotes(searchResultNoteIds);
|
||||
|
||||
// reset all the virtual branches from old search results
|
||||
if (note.noteId in treeCache.notes) {
|
||||
treeCache.notes[note.noteId].children = [];
|
||||
}
|
||||
|
||||
const branches = resp.branches.filter(b => b.noteId === note.noteId || b.parentNoteId === note.noteId);
|
||||
|
||||
searchResultNoteIds.forEach((resultNoteId, index) => branches.push({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +283,9 @@ export default class AttributeDetailWidget extends TabAwareWidget {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.attribute.value = suggestion.notePath;
|
||||
const pathChunks = suggestion.notePath.split('/');
|
||||
|
||||
this.attribute.value = pathChunks[pathChunks.length - 1]; // noteId
|
||||
|
||||
this.triggerCommand('updateAttributeList', { attributes: this.allAttributes });
|
||||
this.updateRelatedNotes();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
|
||||
}
|
||||
}]);
|
||||
|
||||
$input.on('autocomplete:noteselected', e => this.promotedAttributeChanged(e))
|
||||
$input.on('autocomplete:selected', e => this.promotedAttributeChanged(e))
|
||||
});
|
||||
}
|
||||
else if (definition.labelType === 'number') {
|
||||
|
||||
@@ -38,7 +38,7 @@ const TPL = `
|
||||
cursor: text !important;
|
||||
}
|
||||
|
||||
.note-detail-editable-text *:first-child {
|
||||
.note-detail-editable-text *:not(figure):first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
});
|
||||
}
|
||||
|
||||
addTextToActiveEditorEvent(text) {
|
||||
addTextToActiveEditorEvent({text}) {
|
||||
if (!this.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,7 @@ function exportBranch(req, res) {
|
||||
|
||||
try {
|
||||
if (type === 'subtree' && (format === 'html' || format === 'markdown')) {
|
||||
const start = Date.now();
|
||||
|
||||
zipExportService.exportToZip(taskContext, branch, format, res);
|
||||
|
||||
console.log("Export took", Date.now() - start, "ms");
|
||||
}
|
||||
else if (type === 'single') {
|
||||
singleExportService.exportSingleNote(taskContext, branch, format, res);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -38,7 +38,8 @@ async function searchFromNote(req) {
|
||||
}
|
||||
|
||||
if (note.isDeleted) {
|
||||
return [400, `Note ${req.params.noteId} is deleted.`];
|
||||
// this can be triggered from recent changes and it's harmless to return empty list rather than fail
|
||||
return [];
|
||||
}
|
||||
|
||||
if (note.type !== 'search') {
|
||||
|
||||
@@ -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-21T22:57:54+02:00", buildRevision: "283808d69181628b84d7d48b5029c51bc5a1cf98" };
|
||||
module.exports = { buildDate:"2021-01-11T22:29:31+01:00", buildRevision: "369274ead75947a68bf7bbb5ab1e784e81521030" };
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -143,7 +143,7 @@ function exportToZip(taskContext, branch, format, res) {
|
||||
const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ function exportToZip(taskContext, branch, format, res) {
|
||||
<link rel="stylesheet" href="${cssUrl}">
|
||||
<base target="_parent">
|
||||
</head>
|
||||
<body>
|
||||
<body class="ck-content">
|
||||
<h1>${utils.escapeHtml(title)}</h1>
|
||||
${content}
|
||||
</body>
|
||||
@@ -433,14 +433,13 @@ ${content}
|
||||
}
|
||||
|
||||
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-Type', 'application/zip');
|
||||
|
||||
zipFile.end();
|
||||
|
||||
zipFile.outputStream.pipe(res);
|
||||
zipFile.end();
|
||||
|
||||
taskContext.taskSucceeded();
|
||||
}
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
const sax = require("sax");
|
||||
const FileType = require('file-type');
|
||||
const stream = require('stream');
|
||||
const {Throttle} = require('stream-throttle');
|
||||
const log = require("../log");
|
||||
const utils = require("../utils");
|
||||
const sql = require("../sql");
|
||||
@@ -8,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) {
|
||||
@@ -38,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>');
|
||||
|
||||
@@ -106,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
|
||||
});
|
||||
}
|
||||
@@ -138,17 +143,6 @@ function importEnex(taskContext, file, parentNote) {
|
||||
}
|
||||
else if (currentTag === 'mime') {
|
||||
resource.mime = text.toLowerCase();
|
||||
|
||||
if (text.startsWith("image/")) {
|
||||
resource.title = "image";
|
||||
|
||||
// images don't have "file-name" tag so we'll create attribute here
|
||||
resource.attributes.push({
|
||||
type: 'label',
|
||||
name: 'originalFileName',
|
||||
value: resource.title + "." + text.substr(6) // extension from mime type
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (previousTag === 'note') {
|
||||
@@ -161,7 +155,7 @@ function importEnex(taskContext, file, parentNote) {
|
||||
} else if (currentTag === 'tag') {
|
||||
note.attributes.push({
|
||||
type: 'label',
|
||||
name: text,
|
||||
name: attributeService.sanitizeAttributeName(text),
|
||||
value: ''
|
||||
})
|
||||
}
|
||||
@@ -239,15 +233,15 @@ 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');
|
||||
|
||||
const fileTypeFromBuffer = FileType.fromBuffer(resource.content);
|
||||
if (fileTypeFromBuffer) {
|
||||
// If fileType returns something for buffer, then set the mime given
|
||||
resource.mime = fileTypeFromBuffer.mime;
|
||||
}
|
||||
resource.mime = resource.mime || "application/octet-stream";
|
||||
|
||||
const createFileNote = () => {
|
||||
const resourceNote = noteService.createNewNote({
|
||||
@@ -260,7 +254,7 @@ function importEnex(taskContext, file, parentNote) {
|
||||
}).note;
|
||||
|
||||
for (const attr of resource.attributes) {
|
||||
noteEntity.addAttribute(attr.type, attr.name, attr.value);
|
||||
resourceNote.addAttribute(attr.type, attr.name, attr.value);
|
||||
}
|
||||
|
||||
updateDates(resourceNote.noteId, utcDateCreated, utcDateModified);
|
||||
@@ -274,10 +268,18 @@ function importEnex(taskContext, file, parentNote) {
|
||||
|
||||
if (resource.mime && resource.mime.startsWith('image/')) {
|
||||
try {
|
||||
const originalName = "image." + resource.mime.substr(6);
|
||||
const originalName = (resource.title && resource.title !== 'resource')
|
||||
? resource.title
|
||||
: `image.${resource.mime.substr(6)}`; // default if real name is not present
|
||||
|
||||
const {url, note: imageNote} = imageService.saveImage(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages);
|
||||
|
||||
for (const attr of resource.attributes) {
|
||||
if (attr.name !== 'originalFileName') { // this one is already saved in imageService
|
||||
imageNote.addAttribute(attr.type, attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
|
||||
updateDates(imageNote.noteId, utcDateCreated, utcDateModified);
|
||||
|
||||
const imageLink = `<img src="${url}">`;
|
||||
@@ -312,7 +314,7 @@ function importEnex(taskContext, file, parentNote) {
|
||||
path.pop();
|
||||
|
||||
if (tag === 'note') {
|
||||
saveNotePromises.push(saveNote());
|
||||
saveNote();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -331,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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ const htmlSanitizer = require('../html_sanitizer');
|
||||
* @param {Note} parentNote
|
||||
* @return {Promise<*[]|*>}
|
||||
*/
|
||||
function importOpml(taskContext, fileBuffer, parentNote) {
|
||||
const xml = new Promise(function(resolve, reject)
|
||||
async function importOpml(taskContext, fileBuffer, parentNote) {
|
||||
const xml = await new Promise(function(resolve, reject)
|
||||
{
|
||||
parseString(fileBuffer, function (err, result) {
|
||||
if (err) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -31,10 +31,7 @@ function initNotSyncedOptions(initialized, startNotePath = 'root', opts = {}) {
|
||||
optionService.createOption('openTabs', JSON.stringify([
|
||||
{
|
||||
notePath: startNotePath,
|
||||
active: true,
|
||||
sidebar: {
|
||||
widgets: []
|
||||
}
|
||||
active: true
|
||||
}
|
||||
]), false);
|
||||
|
||||
@@ -103,6 +100,15 @@ function initStartupOptions() {
|
||||
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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,26 +32,29 @@ class NoteContentProtectedFulltextExp extends Expression {
|
||||
FROM notes JOIN note_contents USING (noteId)
|
||||
WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 1`)) {
|
||||
|
||||
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in noteCache.notes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
content = protectedSessionService.decryptString(content);
|
||||
}
|
||||
catch (e) {
|
||||
log.info('Cannot decrypt content of note', noteId);
|
||||
log.info(`Cannot decrypt content of note ${noteId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
content = content.toLowerCase();
|
||||
|
||||
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(/ /g, ' ');
|
||||
}
|
||||
|
||||
if (this.tokens.find(token => !content.includes(token))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inputNoteSet.hasNoteId(noteId) && noteId in noteCache.notes) {
|
||||
if (!this.tokens.find(token => !content.includes(token))) {
|
||||
resultNoteSet.add(noteCache.notes[noteId]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,18 +26,21 @@ class NoteContentUnprotectedFulltextExp extends Expression {
|
||||
FROM notes JOIN note_contents USING (noteId)
|
||||
WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 0`)) {
|
||||
|
||||
content = content.toString().toLowerCase();
|
||||
|
||||
if (type === 'text' && mime === 'text/html') {
|
||||
content = striptags(content);
|
||||
content = content.replace(/ /g, ' ');
|
||||
}
|
||||
|
||||
if (this.tokens.find(token => !content.includes(token))) {
|
||||
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in noteCache.notes)) {
|
||||
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(/ /g, ' ');
|
||||
}
|
||||
|
||||
if (!this.tokens.find(token => !content.includes(token))) {
|
||||
resultNoteSet.add(noteCache.notes[noteId]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ const stringComparators = {
|
||||
">=": comparedValue => (val => val >= comparedValue),
|
||||
"<": comparedValue => (val => val < comparedValue),
|
||||
"<=": comparedValue => (val => val <= comparedValue),
|
||||
"*=": comparedValue => (val => val.endsWith(comparedValue)),
|
||||
"=*": comparedValue => (val => val.startsWith(comparedValue)),
|
||||
"*=*": comparedValue => (val => val.includes(comparedValue)),
|
||||
"*=": comparedValue => (val => val && val.endsWith(comparedValue)),
|
||||
"=*": comparedValue => (val => val && val.startsWith(comparedValue)),
|
||||
"*=*": comparedValue => (val => val && val.includes(comparedValue)),
|
||||
};
|
||||
|
||||
const numericComparators = {
|
||||
|
||||
@@ -80,10 +80,14 @@ function getExpression(tokens, searchContext, level = 0) {
|
||||
|
||||
if (i + 2 < tokens.length) {
|
||||
if (tokens[i + 1].token === '+') {
|
||||
delta += parseInt(tokens[i + 2].token);
|
||||
i += 2;
|
||||
|
||||
delta += parseInt(tokens[i].token);
|
||||
}
|
||||
else if (tokens[i + 1].token === '-') {
|
||||
delta -= parseInt(tokens[i + 2].token);
|
||||
i += 2;
|
||||
|
||||
delta -= parseInt(tokens[i].token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,16 +200,18 @@ function getExpression(tokens, searchContext, level = 0) {
|
||||
if (PropertyComparisonExp.isProperty(tokens[i].token)) {
|
||||
const propertyName = tokens[i].token;
|
||||
const operator = tokens[i + 1].token;
|
||||
const comparedValue = tokens[i + 2].token;
|
||||
|
||||
i += 2;
|
||||
|
||||
const comparedValue = resolveConstantOperand();
|
||||
|
||||
const comparator = buildComparator(operator, comparedValue);
|
||||
|
||||
if (!comparator) {
|
||||
searchContext.addError(`Can't find operator '${operator}' in ${context(i)}`);
|
||||
searchContext.addError(`Can't find operator '${operator}' in ${context(i - 2)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
i += 2;
|
||||
|
||||
return new PropertyComparisonExp(propertyName, comparator);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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