Compare commits

...

74 Commits

Author SHA1 Message Date
zadam
6afc299efb release 0.45.8 2021-01-11 22:29:31 +01:00
zadam
369274ead7 new env variable to specify start note, #1532 2021-01-11 22:29:02 +01:00
zadam
04e6431c09 use correct class in exported HTMLs so that content style is applied, #1504 2020-12-30 23:20:12 +01:00
zadam
e89057a771 search note content only if not excluded by other expressions 2020-12-25 20:46:04 +01:00
zadam
4f27254e64 fix top margin of images and tables which can obscure their handles 2020-12-25 13:01:35 +01:00
zadam
577dc95ab8 convert   into whitespace also for large notes 2020-12-25 12:55:16 +01:00
zadam
a266d6a3d5 don't strip tags for very large text notes, #1500 2020-12-24 23:37:21 +01:00
zadam
749b6cb57e don't strip tags for very large text notes, #1500 2020-12-24 23:33:42 +01:00
zadam
b0b2951ff6 cleanup 2020-12-22 22:30:04 +01:00
zadam
1f3d73b9fd release 0.45.7 2020-12-22 20:21:15 +01:00
zadam
bdfd760b9d fixed some encryption issues 2020-12-21 23:19:03 +01:00
zadam
7133e60267 make encryption more robust in face of null values, #1483 2020-12-21 22:08:55 +01:00
zadam
fc4edf4aa7 better comment for instanceName 2020-12-21 21:05:34 +01:00
zadam
eaf93a70cd fix inverse relation creation, closes #1498 2020-12-21 20:55:01 +01:00
zadam
b093569ec5 increase toast size limit 2020-12-18 21:23:51 +01:00
zadam
4633c68a0c avoid resorting children on every child add, fixes #1480 2020-12-10 16:10:10 +01:00
zadam
33571e0ef3 better logging for un/protect errors 2020-12-09 22:49:55 +01:00
zadam
31876d2cf9 fix automatically scheduled note deletion 2020-12-09 22:45:34 +01:00
zadam
81c6043cb6 fix printing notes with math, closes #1484 2020-12-09 21:59:30 +01:00
zadam
1982d054ef inherit also note type and mime from template note, closes #1475 2020-12-07 09:35:39 +01:00
zadam
e56979c482 add button to erase deleted notes now into the options 2020-12-06 22:11:49 +01:00
zadam
58555b3660 release 0.45.6 2020-12-04 22:08:24 +01:00
zadam
b7b1324dd0 fixed disabled prefix in case of unsafe import to conform to new attribute name pattern constraints 2020-12-04 22:05:29 +01:00
zadam
e318acc977 fix incorrectly set isInheritable on inherited attrs 2020-11-27 22:33:33 +01:00
zadam
8ae82f5b69 fix "open in new tab" in tree context menu 2020-11-24 23:18:53 +01:00
zadam
26442f418a fix "open in new window" link context menu 2020-11-24 23:06:37 +01:00
zadam
23a432e7d8 don't show imageLinks in link map when they are connecting parent (text note) and child (image), closes #1461 2020-11-24 20:12:49 +01:00
zadam
984ecaf99c show again the table handle and type around 2020-11-23 20:56:14 +01:00
zadam
21b73a86b2 show also keyboard shortcut for duplicateSubtree in context menu 2020-11-23 20:17:53 +01:00
zadam
7d8277699c add keyboard shortcut for duplicate subtree, #1451 2020-11-23 19:44:49 +01:00
zadam
928ed7a034 add keyboard shortcut for include note, closes #1410 2020-11-22 22:44:06 +01:00
zadam
882b6be580 release 0.45.5 2020-11-20 22:50:10 +01:00
zadam
e5fa1e0ed5 hide table's selection handle, fixes #1448 2020-11-20 21:01:44 +01:00
zadam
1047aecfbd template subtree is now deep-duplicated on template assignment 2020-11-19 14:29:26 +01:00
zadam
314e0a453f "duplicate note" now duplicates whole note subtree instead of just individual note 2020-11-19 14:06:32 +01:00
zadam
8ec476ba96 fix ENEX import note saving 2020-11-19 13:30:39 +01:00
zadam
a346ba7038 removed outstandingPushes counting which is not needed 2020-11-18 22:30:00 +01:00
zadam
fd6b2f1e7f enex import cleanup 2020-11-18 21:30:56 +01:00
zadam
6662b9dbf9 rate limiting to improve responsiveness during / after import 2020-11-17 23:05:05 +01:00
zadam
c0a29ede05 small fixes to ENEX import 2020-11-17 22:35:20 +01:00
zadam
845907b8d2 fix recent changes to show all deleted notes (also without note revisions) 2020-11-17 21:06:38 +01:00
zadam
b12008e313 more robust search in face of inconsistent cache 2020-11-17 20:44:38 +01:00
zadam
a108ef91a0 fixed .createNoteLink API documentation 2020-11-17 20:11:10 +01:00
zadam
b5480b4137 fix sync check - it was always reporting success even in failure cases 2020-11-15 20:50:24 +01:00
zadam
47d61c416d release 0.45.4 2020-11-12 22:15:23 +01:00
zadam
6c57b2220f fix export download, fixes #1411 2020-11-12 22:13:59 +01:00
zadam
99f01b9ccf fix overwriting / deleting auto links, closes #1406 2020-11-11 23:15:48 +01:00
zadam
d5a9abd911 fix duplicating relations after change, closes #1405 2020-11-11 23:02:14 +01:00
zadam
a3a2bc0a74 fix "reviving" deleted attributes, closes #1404 2020-11-11 22:44:13 +01:00
zadam
402e5c4d81 release 0.45.3 2020-11-10 22:54:40 +01:00
zadam
5157fc15e9 electron update 2020-11-10 22:54:03 +01:00
zadam
4bd87b1796 update ckeditor5-math which fixes focus and placeholder issues 2020-11-07 21:28:12 +01:00
zadam
ce33eb3abd fix wrong behavior when customResourceProvider has empty value, fixes #1393 2020-11-06 21:52:57 +01:00
zadam
f988935a33 fixed & refactored opening/downloading file notes 2020-11-05 21:26:24 +01:00
zadam
9b05d30b47 update boxicons to 2.0.7 2020-11-03 22:44:50 +01:00
zadam
9e97fdcc49 convert H1 to H2 also during import 2020-11-01 20:38:39 +01:00
zadam
8e8148ce42 fix math rendering in note revisions, fixes #1359 2020-10-30 15:06:11 +01:00
zadam
af41e5d115 release 0.45.2 2020-10-29 22:57:25 +01:00
zadam
4f75b6aaaf fix removing stale branches from saved search after refresh, fixes #1354 2020-10-29 22:41:33 +01:00
zadam
82f410f695 fix math rendering in included note and note tooltip, fixes #1340 2020-10-29 21:06:30 +01:00
zadam
2bc06959c3 add a warning to change password dialog, fixes #1344 2020-10-29 20:57:26 +01:00
zadam
b898973ee6 fixed update ckeditor to 23.1.0 2020-10-29 20:09:25 +01:00
zadam
a2b0d8a379 update ckeditor to 23.1.0 2020-10-29 20:02:38 +01:00
zadam
06a4eab7d5 improved detection of image notes in ENEX import, fixes #1348 2020-10-28 23:36:45 +01:00
zadam
25df1a054c fix triggering change event when item is chosen from autocomplete, fixes #1345 2020-10-28 21:48:34 +01:00
zadam
8c4ff7ed2a fix using smart values with .dateCreated, closes #1338 2020-10-27 22:45:22 +01:00
zadam
609829653e fix docker build 2020-10-27 19:39:54 +01:00
zadam
5f20d033a8 release 0.45.1 2020-10-26 22:51:10 +01:00
zadam
93d0324177 fix case where parents of templates are not loaded
(cherry picked from commit a3f4fc7762)
2020-10-26 20:13:11 +01:00
zadam
0afd3c65aa fix setting note title on back/forward button click, closes #1334 2020-10-26 20:11:43 +01:00
zadam
8901c3ec91 fix recent changes showing deleted search note, closes #1331 2020-10-26 19:58:56 +01:00
zadam
c671b0a345 fix OPML import, closes #1333 2020-10-26 19:02:33 +01:00
zadam
9f424836e2 fix adding relation noteId as value, closes #1329 2020-10-26 16:05:34 +01:00
zadam
7f5af4b959 fix broken addTextToActiveEditor API method, closes #1332 2020-10-26 15:57:37 +01:00
89 changed files with 6722 additions and 6322 deletions

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
FROM node:12.16.3-alpine
FROM node:12.19.0-alpine
# Create app directory
WORKDIR /usr/src/app

View File

@@ -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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 912 KiB

After

Width:  |  Height:  |  Size: 952 KiB

View File

@@ -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;
}
}
}

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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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 ""')
});
});

View File

@@ -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");
});

View File

@@ -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}
*/

View File

@@ -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;
}

View File

@@ -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);
}
});
});

View File

@@ -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));

View File

@@ -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>

View File

@@ -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 {
});
});
}
}
}

View File

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

View File

@@ -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() {

View File

@@ -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;

View File

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

View File

@@ -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);
}

View File

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

View File

@@ -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>")

View 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
}

View File

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

View File

@@ -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

View File

@@ -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",

View File

@@ -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({

View File

@@ -2,9 +2,9 @@ import treeService from './tree.js';
import treeCache from "./tree_cache.js";
import hoistedNoteService from './hoisted_note.js';
import clipboard from './clipboard.js';
import protectedSessionHolder from "./protected_session_holder.js";
import noteCreateService from "./note_create.js";
import contextMenu from "./context_menu.js";
import appContext from "./app_context.js";
class TreeContextMenu {
/**
@@ -95,7 +95,7 @@ class TreeContextMenu {
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
{ title: 'Paste after', command: "pasteNotesAfterFromClipboard", uiIcon: "paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
{ title: "Duplicate 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;

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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();

View File

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

View File

@@ -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">

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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') {

View File

@@ -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;
}

View File

@@ -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");
}
}

View File

@@ -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

View File

@@ -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 */

View File

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

View File

@@ -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();
}
}
}

View File

@@ -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);

View File

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

View File

@@ -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
};

View File

@@ -31,19 +31,36 @@ function getRecentChanges(req) {
}
}
// now we need to also collect date points not represented in note revisions:
// 1. creation for all notes (dateCreated)
// 2. deletion for deleted notes (dateModified)
const notes = sql.getRows(`
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)) {

View File

@@ -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') {

View File

@@ -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);
}
}

View File

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

View File

@@ -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)}`);

View File

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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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
};

View File

@@ -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" };

View File

@@ -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();

View File

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

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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'

View File

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

View File

@@ -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);
});
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -158,7 +158,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
}
if (taskContext.data.safeImport && attributeService.isAttributeDangerous(attr.type, attr.name)) {
attr.name = 'disabled-' + attr.name;
attr.name = 'disabled:' + attr.name;
}
attributes.push(attr);
@@ -275,7 +275,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
return ""; // remove whole H1 tag
}
else {
return match;
return `<h2>${text}</h2>`;
}
});

View File

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

View File

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

View File

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

View File

@@ -185,18 +185,25 @@ function protectNoteRecursively(note, protect, includingSubTree, taskContext) {
}
function protectNote(note, protect) {
if (protect !== note.isProtected) {
const content = note.getContent();
try {
if (protect !== note.isProtected) {
const content = note.getContent();
note.isProtected = protect;
note.isProtected = protect;
// this will force de/encryption
note.setContent(content);
// this will force de/encryption
note.setContent(content);
note.save();
note.save();
}
noteRevisionService.protectNoteRevisions(note);
}
catch (e) {
log.error("Could not un/protect note ID = " + note.noteId);
noteRevisionService.protectNoteRevisions(note);
throw e;
}
}
function findImageLinks(content, foundLinks) {
@@ -668,8 +675,10 @@ function scanForLinks(note) {
}
}
function eraseDeletedNotes() {
const eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds');
function eraseDeletedNotes(eraseNotesAfterTimeInSeconds = null) {
if (eraseNotesAfterTimeInSeconds === null) {
eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds');
}
const cutoffDate = new Date(Date.now() - eraseNotesAfterTimeInSeconds * 1000);
@@ -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
};

View File

@@ -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() {

View File

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

View File

@@ -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(/&nbsp;/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]);
}
}

View File

@@ -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(/&nbsp;/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(/&nbsp;/g, ' ');
}
if (!this.tokens.find(token => !content.includes(token))) {
resultNoteSet.add(noteCache.notes[noteId]);
}
}

View File

@@ -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 = {

View File

@@ -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);
}

View File

@@ -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 = {

View File

@@ -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
};

View File

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