Compare commits

...

77 Commits

Author SHA1 Message Date
zadam
f4266754d8 release 0.43.4 2020-08-27 23:58:58 +02:00
zadam
dc288fb18c Merge remote-tracking branch 'origin/stable' into stable 2020-08-27 23:22:58 +02:00
zadam
26dfa1ffdb activate PDF preview also in server build, fixes #1208 2020-08-27 23:22:36 +02:00
zadam
153de63f4d update note title in link map when note is renamed 2020-08-20 15:45:33 +02:00
zadam
a89629b3de add fallback when resizing image fails, closes #1190 2020-08-20 11:34:14 +02:00
zadam
eec850c11f fix toggle sidebar issues, closes #1196 2020-08-20 11:14:27 +02:00
zadam
e8d63b5647 add "search in note" to "note actions" menu, #1184 2020-08-14 20:58:19 +02:00
zadam
bd8b83898f Merge remote-tracking branch 'origin/stable' into stable 2020-08-12 23:59:42 +02:00
zadam
97109efb6c fix attribute cache invalidation 2020-08-12 23:59:33 +02:00
Jody
3e89855aa3 Add web app manifest (#1174)
* Add web app manifest, link to new manifest on mobile template.

* Remove duplicate manifest entry.
2020-08-12 21:39:12 +02:00
zadam
3b148eb6f8 fixed demo document to correctly show included notes 2020-08-10 23:42:06 +02:00
zadam
4e8d1dac67 fixes for rendering PDF previews e.g. in include note 2020-08-10 23:39:17 +02:00
zadam
7779fd1dfe synchronize the removal of the selection in cuttonote to make it more predictable 2020-08-10 22:42:57 +02:00
zadam
960d7dede3 add scrolling margins, #1181 2020-08-09 23:20:57 +02:00
zadam
224fbdc8cd small improvements to text preview in file notes 2020-08-04 22:06:25 +02:00
zadam
8561201abc invalidate note complement cache quickly after load 2020-08-04 21:57:08 +02:00
zadam
3c1a809276 external links should open only in new window, not in the original window, closes #1171 2020-08-04 20:42:32 +02:00
zadam
4b101baf00 CKEditor 21 content styles 2020-08-03 23:36:33 +02:00
zadam
782127dd91 fix noproxy handling for image downloading 2020-08-03 23:33:44 +02:00
zadam
4fc8bace94 support horizontal line, closes #1164 2020-08-01 23:41:30 +02:00
zadam
47a22f6e8d release 0.43.3 2020-07-31 23:34:05 +02:00
zadam
17d7ff3ff1 small improvements to sync table handling 2020-07-31 00:08:01 +02:00
zadam
3582013a33 import of initial demo document sets sync.isSynced incorrectly to always 0, #1163 2020-07-31 00:07:38 +02:00
zadam
95bbdb3b6b CKEditor 21 2020-07-29 23:34:49 +02:00
zadam
8a57960c6e tweaks in sync timeout handling 2020-07-28 23:29:12 +02:00
zadam
5f4a84d967 fix extracting base64 inline images from HTML, fixes #1159 2020-07-26 23:47:06 +02:00
zadam
099e90ed64 fix extracting base64 inline images from HTML, fixes #1159 2020-07-26 22:58:22 +02:00
zadam
3d324b954d fix checking affected notes when modified attribute's owning note is not loaded into cache, #803 2020-07-15 22:36:27 +02:00
zadam
443f389d73 release 0.43.2 2020-07-11 23:58:59 +02:00
zadam
08edc521e4 fix visibility of hyper-link in repeatedly opened dialog 2020-07-11 23:50:38 +02:00
zadam
f54f6d09b0 fix declaring global variables on the setup page, closes #1071 2020-07-11 23:19:24 +02:00
zadam
3219441fdf fallback for missing parsed title, closes trilium-web-clipper/issues/16 2020-07-11 23:05:28 +02:00
zadam
1b0a2b41da added tab and shift-tab shortcuts for indent/outdent, closes #978 2020-07-08 21:57:09 +02:00
zadam
582429e762 updated content styles 2020-07-01 21:03:51 +02:00
zadam
238df0fb40 fix SVG single file import as image and not as file, closes #1114 2020-06-24 21:47:50 +02:00
zadam
89356918f1 fix unescaped HTML in the tree node title, closes #1127 2020-06-24 21:07:55 +02:00
zadam
263b65997c hide "type around" controls in printed PDF, closes #1129 2020-06-24 20:44:00 +02:00
zadam
2b757bfccd upgrade ckeditor to 20.0.0 2020-06-24 16:17:39 +02:00
zadam
c78ca4c9db fixed triggers to sort children of notes with "sorted" label, closes #1126 2020-06-23 22:03:01 +02:00
zadam
a0395e9866 release 0.43.1 2020-06-23 10:11:17 +02:00
zadam
89aa4fbc73 electron 9.0.5 2020-06-23 10:10:59 +02:00
zadam
74a7802088 fix custom resource handler, closes #1125 2020-06-22 23:13:53 +02:00
zadam
a89b6711d1 refactored code to not depend on external elements, #1120 2020-06-22 22:28:45 +02:00
zadam
b2549b2834 Merge remote-tracking branch 'origin/stable' into stable 2020-06-22 22:00:22 +02:00
zadam
959c4cbe64 removed icon tooltip again 2020-06-22 22:00:08 +02:00
Shon Ramamurthy
d03d3603d2 Add optional support for note title tooltips under note tree widget (#1120)
* Add support for note title tooltips under note tree widget

This change adds an option to set the 'tooltip' configuration of the
Fancytree component. This allows tooltips containing the note title to
be displayed when a hover is performed over a note title in the tree
widget.

* Revert DB Upgrade

The db upgrade is reverted as this is not required for options.

* Simplify boolean option comparison

With this change, the existing 'is(key)' method is used to perform
tooltip enable option boolean comparison.

* Display tooltip only on center-pane overlap - Experimental

With this change, a straight-forward method to detect HTML element
overlap has been identified (source:
https://gist.github.com/jtsternberg/c272d7de5b967cec2d3d). It is now
possible to detect whether the center-pane element overlaps with the
Fancytree node title-bar. Using this approach we now have a rough
implementation which only displays a note-title tooltip when there is a
center-pane overlap.

At this stage, this change is experimental and the following needs to be
further addressed,
 - Register the 'mouseenter' event handler in an appropriate place. The
   current placement of this event handler is only for testing.
 - This change is now enabled by default. It needs to be seen whether it
   would still make sense to disable it via an option.

* Remove option to set tooltip

With this change, the tooltip options menu item has been removed as it
becomes relevant to have this feature enabled by default.

* Revert further changes related to the options menu

Further changes are rolled back which was earlier related to the tooltip
options setting. Some of these were missed in the previous commit.

* Remove debug logging

Remove debug logging and unnecessary line breaks.

* Move note-title tooltip handler under note_tree.js

With this change, we move the definition for the note-title tooltip
handler inside 'note_tree.js'. Registration is done inside
'side_pane_toggles.js' as we would need the handler to detect the
'center-pane' element first before detecting collisions.
2020-06-22 21:58:58 +02:00
zadam
e1c2573778 add tooltip to fancytree node icon, #1120 2020-06-21 12:47:24 +02:00
zadam
2af2b45062 fix keyboard shortcut for print this note, #1118 2020-06-20 00:04:31 +02:00
zadam
eabe4775bd fix click handler of links inside included note 2020-06-19 23:20:52 +02:00
zadam
da9b321aa0 allow mouse interactions in ckeditor widgets, fixes #1117 2020-06-19 22:24:57 +02:00
zadam
c18d8d2d1b add back lost edited notes widget, closes #1115 2020-06-18 08:56:23 +02:00
zadam
5f2361ebd5 release 0.43.0-beta 2020-06-15 23:26:12 +02:00
zadam
9791dab97d recent changes sorting fixes, closes #1099 2020-06-15 23:22:11 +02:00
zadam
85d986534d fix enforcing node's http requests for sync 2020-06-15 18:24:43 +02:00
zadam
00faf758e8 fixes and polish 2020-06-15 17:56:53 +02:00
zadam
6ba2e5cf73 add word count widget to the demo document (plus cleanup of external links) 2020-06-14 16:29:29 +02:00
zadam
fb3876d28b promoted attr selection using autocomplete will trigger change event to save it, closes #699 2020-06-14 16:04:00 +02:00
zadam
fb975849b9 small API additions 2020-06-14 14:30:57 +02:00
zadam
16fef78344 add API method to force refresh of included notes, closes #1106 2020-06-14 10:50:08 +02:00
zadam
e0b4b369dc transaction handling fixes 2020-06-14 00:35:53 +02:00
zadam
0df7851214 fix sync 2020-06-13 22:34:15 +02:00
zadam
5d47c2b23e opening transactions only on write operations which enforces exclusive lock only there to improve concurrency, custom handling of sync request timeouts, #1093, #1018 2020-06-13 10:23:36 +02:00
zadam
d09b021487 add time limit to frontend update 2020-06-11 00:13:56 +02:00
zadam
910bda860c fix delete note function just work one time, closes #1101 2020-06-10 23:43:59 +02:00
zadam
2d92b4931a fix ctrl+click opening the link twice/thrice, closes #1094 2020-06-10 00:10:27 +02:00
zadam
212b719ee9 better detection of changes in attributes and how they affect notes 2020-06-09 22:59:22 +02:00
zadam
1db892d22f return the ability to hide archived notes, closes #1095 2020-06-08 23:15:49 +02:00
zadam
535dcb6d12 release 0.42.7 2020-06-08 10:43:12 +02:00
zadam
4426362799 cleanup sqlite to make the distributed archives smaller 2020-06-08 10:42:40 +02:00
zadam
2c609e8136 promoted attributes widget is now auto-updating, fixes #700 2020-06-08 00:29:52 +02:00
zadam
11b73b79ed refresh promoted attributes when change detected 2020-06-07 23:57:10 +02:00
zadam
e70c862e72 fix import 2020-06-07 23:55:55 +02:00
zadam
b3e66d5a83 fixed command line anonymization 2020-06-07 10:45:41 +02:00
zadam
e8cd821e57 futrther improvements to anonymization 2020-06-07 10:20:48 +02:00
zadam
be7ac74235 better fallback for resolving filenames of binary attachments 2020-06-05 10:40:35 +02:00
zadam
58fa0832f6 fix focusing title after creating a note 2020-06-04 21:44:34 +02:00
zadam
1502b9ce66 prevent attribute inheritance cycle via template, closes #1077 2020-06-04 12:27:41 +02:00
84 changed files with 1002 additions and 561 deletions

View File

@@ -33,6 +33,9 @@ find $DIR/libraries -name "*.map" -type f -delete
rm -r $DIR/src/public/app
rm -r $DIR/node_modules/sqlite3/build
rm -r $DIR/node_modules/sqlite3/deps
sed -i -e 's/app\/desktop.js/app-dist\/desktop.js/g' $DIR/src/views/desktop.ejs
sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' $DIR/src/views/mobile.ejs
sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' $DIR/src/views/setup.ejs
sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' $DIR/src/views/setup.ejs

Binary file not shown.

View File

@@ -0,0 +1,4 @@
UPDATE sync SET isSynced = 1 WHERE entityName != 'options' OR (
entityName = 'options'
AND 1 = (SELECT isSynced FROM options WHERE name = sync.entityId)
)

View File

@@ -1,23 +1,53 @@
/*
* !!!!!!! This stylesheet is heavily modified compared to the original for similarity with in-editor look !!!!!!!
* This is used for printing and tar HTML export
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
* CKEditor 5 (v17.0.0) content styles.
* Generated on Fri, 13 Mar 2020 13:27:10 GMT.
.ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
display: none;
}
/*
* CKEditor 5 (v21.0.0) content styles.
* Generated on Wed, 29 Jul 2020 12:14:43 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
*/
:root {
--ck-highlight-marker-blue: #72cdfd;
--ck-highlight-marker-green: #63f963;
--ck-highlight-marker-pink: #fc7999;
--ck-highlight-marker-yellow: #fdfd77;
--ck-highlight-pen-green: #118800;
--ck-highlight-pen-red: #e91313;
--ck-color-mention-background: hsla(341, 100%, 30%, 0.1);
--ck-color-mention-text: hsl(341, 100%, 30%);
--ck-highlight-marker-blue: hsl(201, 97%, 72%);
--ck-highlight-marker-green: hsl(120, 93%, 68%);
--ck-highlight-marker-pink: hsl(345, 96%, 73%);
--ck-highlight-marker-yellow: hsl(60, 97%, 73%);
--ck-highlight-pen-green: hsl(112, 100%, 27%);
--ck-highlight-pen-red: hsl(0, 85%, 49%);
--ck-image-style-spacing: 1.5em;
--ck-todo-list-checkmark-size: 16px;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
margin: 1em auto;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
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%;
@@ -32,37 +62,11 @@
.ck-content .image.image_resized > figcaption {
display: block;
}
/* ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
margin: 1em auto;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image > img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 50px;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side,
.ck-content .image-style-align-left,
.ck-content .image-style-align-center,
.ck-content .image-style-align-right {
max-width: 50%;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
max-width: 50%;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left {
@@ -79,42 +83,6 @@
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: #fff;
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote {
overflow: hidden;
@@ -130,38 +98,6 @@
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 1em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 1em auto;
display: table;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border-color: hsl(0, 0%, 75%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list {
list-style: none;
@@ -229,16 +165,12 @@
.ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* 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-horizontal-line/theme/horizontalline.css */
.ck-content hr {
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
@@ -266,17 +198,108 @@
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
border-width: 1px 0 0;
border-style: solid;
border-color: hsl(0, 0%, 37%);
margin: 0;
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny {
font-size: .7em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-small {
font-size: .85em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-big {
font-size: 1.4em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-huge {
font-size: 1.8em;
}
/* 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;
display: table;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border: 1px solid hsl(0, 0%, 75%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="rtl"] .table th {
text-align: right;
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="ltr"] .table th {
text-align: left;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: hsl(0, 0%, 100%);
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-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-code-block/theme/codeblock.css */
.ck-content pre {
padding: 1em;
color: #353535;
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
@@ -293,6 +316,11 @@
padding: 0;
border-radius: 0;
}
/* ckeditor5-mention/theme/mention.css */
.ck-content .mention {
background: var(--ck-color-mention-background);
color: var(--ck-color-mention-text);
}
@media print {
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
@@ -302,4 +330,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

File diff suppressed because one or more lines are too long

14
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.42.5",
"version": "0.43.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3345,9 +3345,9 @@
}
},
"electron": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.2.tgz",
"integrity": "sha512-+a3KegLvQXVjC3b6yBWwZmtWp3tHf9ut27yORAWHO9JRFtKfNf88fi1UvTPJSW8R0sUH7ZEdzN6A95T22KGtlA==",
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.5.tgz",
"integrity": "sha512-bnL9H48LuQ250DML8xUscsKiuSu+xv5umXbpBXYJ0BfvYVmFfNbG3jCfhrsH7aP6UcQKVxOG1R/oQExd0EFneQ==",
"dev": true,
"requires": {
"@electron/get": "^1.0.1",
@@ -7923,9 +7923,9 @@
"integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q=="
},
"node-abi": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.16.0.tgz",
"integrity": "sha512-+sa0XNlWDA6T+bDLmkCUYn6W5k5W6BPRL6mqzSCs6H/xUgtl4D5x2fORKDzopKiU6wsyn/+wXlRXwXeSp+mtoA==",
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.18.0.tgz",
"integrity": "sha512-yi05ZoiuNNEbyT/xXfSySZE+yVnQW6fxPZuFbLyS1s6b5Kw3HzV2PHOM4XR+nsjzkHxByK+2Wg+yCQbe35l8dw==",
"requires": {
"semver": "^5.4.1"
},

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.42.6",
"version": "0.43.4",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -54,7 +54,7 @@
"jimp": "0.10.3",
"mime-types": "2.1.27",
"multer": "1.4.2",
"node-abi": "2.16.0",
"node-abi": "2.18.0",
"open": "7.0.3",
"portscanner": "2.2.0",
"rand-token": "1.0.1",
@@ -78,7 +78,7 @@
"yazl": "^2.5.1"
},
"devDependencies": {
"electron": "9.0.2",
"electron": "9.0.5",
"electron-builder": "22.6.0",
"electron-packager": "14.2.1",
"electron-rebuild": "1.10.1",

View File

@@ -1,12 +1,24 @@
const backupService = require('./services/backup');
const sqlInit = require('./services/sql_init');
require('./entities/entity_constructor');
backupService.anonymize().then(resp => {
if (resp.success) {
console.log("Anonymization failed.");
sqlInit.dbReady.then(async () => {
try {
console.log("Starting anonymization...");
const resp = await backupService.anonymize();
if (resp.success) {
console.log("Anonymized file has been saved to: " + resp.anonymizedFilePath);
process.exit(0);
} else {
console.log("Anonymization failed.");
}
}
else {
console.log("Anonymized file has been saved to: " + resp.anonymizedFilePath);
catch (e) {
console.error(e.message, e.stack);
}
process.exit(0);
process.exit(1);
});

View File

@@ -28,17 +28,6 @@ app.use((req, res, next) => {
next();
});
app.use((req, res, next) => {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
cls.init(() => {
cls.namespace.set("Hi");
next();
});
});
app.use(bodyParser.json({limit: '500mb'}));
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
@@ -120,4 +109,4 @@ require('./services/scheduler');
module.exports = {
app,
sessionParser
};
};

View File

@@ -105,7 +105,7 @@ class Attribute extends Entity {
// cannot be static!
updatePojo(pojo) {
delete pojo.__note;
delete pojo.__note; // FIXME: probably note necessary anymore
}
createClone(type, name, value) {

View File

@@ -17,7 +17,6 @@ export async function showDialog(widget) {
$addLinkTitleSettings.toggle(!textTypeWidget.hasSelection());
updateTitleFormGroupVisibility();
$addLinkTitleSettings.find('input[type=radio]').on('change', updateTitleFormGroupVisibility);
// with selection hyper link is implied
@@ -28,6 +27,8 @@ export async function showDialog(widget) {
$addLinkTitleSettings.find("input[value='reference-link']").prop("checked", true);
}
updateTitleFormGroupVisibility();
utils.openDialog($dialog);
$autoComplete.val('').trigger('focus');
@@ -89,4 +90,4 @@ $form.on('submit', () => {
}
return false;
});
});

View File

@@ -152,10 +152,10 @@ function AttributesModel() {
attr.value = treeService.getNoteIdFromNotePath(attr.selectedPath);
}
else if (attr.type === 'label-definition') {
attr.value = attr.labelDefinition;
attr.value = JSON.stringify(attr.labelDefinition);
}
else if (attr.type === 'relation-definition') {
attr.value = attr.relationDefinition;
attr.value = JSON.stringify(attr.relationDefinition);
}
delete attr.labelValue;

View File

@@ -16,18 +16,18 @@ export async function showDialog(ancestorNoteId) {
ancestorNoteId = hoistedNoteService.getHoistedNoteId();
}
const result = await server.get('recent-changes/' + ancestorNoteId);
const recentChangesRows = await server.get('recent-changes/' + ancestorNoteId);
// preload all notes into cache
await treeCache.getNotes(result.map(r => r.noteId), true);
await treeCache.getNotes(recentChangesRows.map(r => r.noteId), true);
$content.empty();
if (result.length === 0) {
if (recentChangesRows.length === 0) {
$content.append("No changes yet ...");
}
const groupedByDate = groupByDate(result);
const groupedByDate = groupByDate(recentChangesRows);
for (const [dateDay, dayChanges] of groupedByDate) {
const $changesList = $('<ul>');
@@ -95,10 +95,10 @@ export async function showDialog(ancestorNoteId) {
}
}
function groupByDate(result) {
function groupByDate(rows) {
const groupedByDate = new Map();
for (const row of result) {
for (const row of rows) {
const dateDay = row.date.substr(0, 10);
if (!groupedByDate.has(dateDay)) {

View File

@@ -23,8 +23,12 @@ class Attribute {
}
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
getNote() {
return this.treeCache.notes[this.noteId];
}
get targetNoteId() { // alias
return this.type === 'relation' ? this.value : undefined;
}
get jsonValue() {
@@ -39,6 +43,44 @@ class Attribute {
get toString() {
return `Attribute(attributeId=${this.attributeId}, type=${this.type}, name=${this.name}, value=${this.value})`;
}
/**
* @return {boolean} - returns true if this attribute has the potential to influence the note in the argument.
* That can happen in multiple ways:
* 1. attribute is owned by the note
* 2. attribute is owned by the template of the note
* 3. attribute is owned by some note's ancestor and is inheritable
*/
isAffecting(affectedNote) {
if (!affectedNote) {
return false;
}
const attrNote = this.getNote();
if (!attrNote) {
// the note (owner of the attribute) is not even loaded into the cache so it should not affect anything else
return false;
}
const owningNotes = [affectedNote, ...affectedNote.getTemplateNotes()];
for (const owningNote of owningNotes) {
if (owningNote.noteId === attrNote.noteId) {
return true;
}
}
if (this.isInheritable) {
for (const owningNote of owningNotes) {
if (owningNote.hasAncestor(attrNote)) {
return true;
}
}
}
return false;
}
}
export default Attribute;
export default Attribute;

View File

@@ -170,6 +170,16 @@ class NoteShort {
* @returns {Attribute[]} all note's attributes, including inherited ones
*/
getAttributes(type, name) {
return this.__filterAttrs(this.__getCachedAttributes([]), type, name);
}
__getCachedAttributes(path) {
// notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates
// when template instance is a parent of template itself
if (path.includes(this.noteId)) {
return [];
}
if (!(this.noteId in noteAttributeCache)) {
const ownedAttributes = this.getOwnedAttributes();
@@ -177,11 +187,13 @@ class NoteShort {
ownedAttributes
];
const newPath = [...path, this.noteId];
for (const templateAttr of ownedAttributes.filter(oa => oa.type === 'relation' && oa.name === 'template')) {
const templateNote = this.treeCache.notes[templateAttr.value];
if (templateNote) {
attrArrs.push(templateNote.getAttributes());
if (templateNote && templateNote.noteId !== this.noteId) {
attrArrs.push(templateNote.__getCachedAttributes(newPath));
}
}
@@ -189,7 +201,7 @@ class NoteShort {
for (const parentNote of this.getParentNotes()) {
// these virtual parent-child relationships are also loaded into frontend tree cache
if (parentNote.type !== 'search') {
attrArrs.push(parentNote.getInheritableAttributes());
attrArrs.push(parentNote.__getInheritableAttributes(newPath));
}
}
}
@@ -197,7 +209,7 @@ class NoteShort {
noteAttributeCache.attributes[this.noteId] = attrArrs.flat();
}
return this.__filterAttrs(noteAttributeCache.attributes[this.noteId], type, name);
return noteAttributeCache.attributes[this.noteId];
}
__filterAttrs(attributes, type, name) {
@@ -212,8 +224,8 @@ class NoteShort {
}
}
getInheritableAttributes() {
const attrs = this.getAttributes();
__getInheritableAttributes(path) {
const attrs = this.__getCachedAttributes(path);
return attrs.filter(attr => attr.isInheritable);
}
@@ -425,6 +437,35 @@ class NoteShort {
return targets;
}
/**
* @returns {NoteShort[]}
*/
getTemplateNotes() {
const relations = this.getRelations('template');
return relations.map(rel => this.treeCache.notes[rel.value]);
}
hasAncestor(ancestorNote) {
if (this.noteId === ancestorNote.noteId) {
return true;
}
for (const templateNote of this.getTemplateNotes()) {
if (templateNote.hasAncestor(ancestorNote)) {
return true;
}
}
for (const parentNote of this.getParentNotes()) {
if (parentNote.hasAncestor(ancestorNote)) {console.log(parentNote);
return true;
}
}
return false;
}
/**
* Clear note's attributes cache to force fresh reload for next attribute request.
* Cache is note instance scoped.
@@ -443,6 +484,15 @@ class NoteShort {
.map(attributeId => this.treeCache.attributes[attributeId]);
}
/**
* Return note complement which is most importantly note's content
*
* @return {Promise<NoteComplement>}
*/
async getNoteComplement() {
return await this.treeCache.getNoteComplement(this.noteId);
}
get toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`;
}
@@ -460,4 +510,4 @@ class NoteShort {
}
}
export default NoteShort;
export default NoteShort;

View File

@@ -24,6 +24,7 @@ import NoteRevisionsWidget from "../widgets/collapsible_widgets/note_revisions.j
import SimilarNotesWidget from "../widgets/collapsible_widgets/similar_notes.js";
import WhatLinksHereWidget from "../widgets/collapsible_widgets/what_links_here.js";
import SidePaneToggles from "../widgets/side_pane_toggles.js";
import EditedNotesWidget from "../widgets/collapsible_widgets/edited_notes.js";
const RIGHT_PANE_CSS = `
<style>
@@ -143,6 +144,7 @@ export default class DesktopMainWindowLayout {
.hideInZenMode()
.child(new NoteInfoWidget())
.child(new TabCachingWidget(() => new CalendarWidget()))
.child(new TabCachingWidget(() => new EditedNotesWidget()))
.child(new TabCachingWidget(() => new AttributesWidget()))
.child(new TabCachingWidget(() => new LinkMapWidget()))
.child(new TabCachingWidget(() => new NoteRevisionsWidget()))

View File

@@ -1,7 +1,6 @@
import ScriptContext from "./script_context.js";
import server from "./server.js";
import toastService from "./toast.js";
import treeCache from "./tree_cache.js";
async function getAndExecuteBundle(noteId, originEntity = null) {
const bundle = await server.get('script/bundle/' + noteId);
@@ -77,4 +76,4 @@ export default {
getAndExecuteBundle,
executeStartupBundles,
getWidgetBundlesByParent
}
}

View File

@@ -403,6 +403,13 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @method
*/
this.waitUntilSynced = ws.waitForMaxKnownSyncId;
/**
* This will refresh all currently opened notes which have included note specified in the parameter
*
* @param includedNoteId - noteId of the included note
*/
this.refreshIncludedNote = includedNoteId => appContext.triggerEvent('refreshIncludedNote', {noteId: includedNoteId});
}
export default FrontendScriptApi;
export default FrontendScriptApi;

View File

@@ -49,11 +49,7 @@ function setupGlobs() {
let message = "Uncaught error: ";
if (string.includes("Cannot read property 'defaultView' of undefined")) {
// ignore this specific error which is very common but we don't know where it comes from
// and it seems to be harmless
return true;
} else if (string.includes("script error")) {
if (string.includes("script error")) {
message += 'No details available';
} else {
message += [
@@ -61,8 +57,9 @@ function setupGlobs() {
'URL: ' + url,
'Line: ' + lineNo,
'Column: ' + columnNo,
'Error object: ' + JSON.stringify(error)
].join(' - ');
'Error object: ' + JSON.stringify(error),
'Stack: ' + error && error.stack
].join(', ');
}
ws.logError(message);
@@ -86,9 +83,11 @@ function setupGlobs() {
$("body").on("click", "a.external", function () {
window.open($(this).attr("href"), '_blank');
return false;
});
}
export default {
setupGlobs
}
}

View File

@@ -137,7 +137,7 @@ function linkContextMenu(e) {
$(document).on('mousedown', "a[data-action='note']", goToLink);
$(document).on('mousedown', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
$(document).on('dblclick', '.note-detail-text a', goToLink);
$(document).on('mousedown', '.note-detail-text a', function (e) {
$(document).on('mousedown', '.note-detail-text a:not(.reference-link)', function (e) {
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
@@ -161,8 +161,10 @@ $(document).on('mousedown', '.note-detail-text a', function (e) {
$(document).on('mousedown', '.note-detail-book a', goToLink);
$(document).on('mousedown', '.note-detail-render a', goToLink);
$(document).on('mousedown', '.note-detail-text a.reference-link', goToLink);
$(document).on('mousedown', '.note-detail-readonly-text a.reference-link', goToLink);
$(document).on('mousedown', '.note-detail-readonly-text a', goToLink);
$(document).on('mousedown', 'a.ck-link-actions__preview', goToLink);
$(document).on('click', 'section.include-note a', goToLink);
$(document).on('click', 'a.ck-link-actions__preview', e => {
e.preventDefault();
e.stopPropagation();
@@ -174,6 +176,7 @@ $(document).on('contextmenu', '.note-detail-readonly-text a', linkContextMenu);
$(document).on('contextmenu', "a[data-action='note']", linkContextMenu);
$(document).on('contextmenu', ".note-detail-render a", linkContextMenu);
$(document).on('contextmenu', ".note-paths-widget a", linkContextMenu);
$(document).on('contextmenu', "section.include-note a", linkContextMenu);
export default {
getNotePathFromUrl,

View File

@@ -54,8 +54,9 @@ export default class LoadResults {
this.attributes.push({attributeId, sourceId});
}
getAttributes() {
getAttributes(sourceId = 'none') {
return this.attributes
.filter(row => row.sourceId !== sourceId)
.map(row => this.treeCache.attributes[row.attributeId])
.filter(attr => !!attr);
}
@@ -106,8 +107,8 @@ export default class LoadResults {
* notably changes in note itself should not have any effect on attributes
*/
hasAttributeRelatedChanges() {
return this.branches.length === 0
&& this.attributes.length === 0;
return this.branches.length > 0
|| this.attributes.length > 0;
}
isEmpty() {

View File

@@ -34,7 +34,7 @@ export default class MainTreeExecutors extends Component {
return;
}
const {note} = await noteCreateService.createNote(activeNote.noteId, {
await noteCreateService.createNote(activeNote.noteId, {
isProtected: activeNote.isProtected,
saveSelection: false
});
@@ -56,4 +56,4 @@ export default class MainTreeExecutors extends Component {
saveSelection: false
});
}
}
}

View File

@@ -1,29 +0,0 @@
export default class Mutex {
constructor() {
this.queue = [];
this.pending = false;
}
isLocked() {
return this.pending;
}
acquire() {
const ticket = new Promise(resolve => this.queue.push(resolve));
if (!this.pending) {
this.dispatchNext();
}
return ticket;
}
dispatchNext() {
if (this.queue.length > 0) {
this.pending = true;
this.queue.shift()(this.dispatchNext.bind(this));
} else {
this.pending = false;
}
}
}

View File

@@ -24,7 +24,7 @@ async function getRenderedContent(note) {
.attr("src", `api/images/${note.noteId}/${note.title}`)
.css("max-width", "100%");
}
else if (type === 'file') {
else if (type === 'file' || type === 'pdf') {
function getFileUrl() {
return utils.getUrlForDownload("api/notes/" + note.noteId + "/download");
}
@@ -47,19 +47,21 @@ async function getRenderedContent(note) {
// open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected);
$rendered = $('<div>');
$rendered = $('<div style="display: flex; flex-direction: column; height: 100%;">');
if (note.mime === 'application/pdf' && utils.isElectron()) {
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; height: 100%; flex-grow: 100;"></iframe>');
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"));
$rendered.append($pdfPreview);
}
$rendered
.append($downloadButton)
.append(' &nbsp; ')
.append($openButton);
$rendered.append(
$("<div>")
.append($downloadButton)
.append(' &nbsp; ')
.append($openButton)
);
}
else if (type === 'render') {
$rendered = $('<div>');
@@ -90,6 +92,10 @@ async function getRenderedContent(note) {
function getRenderingType(note) {
let type = note.type;
if (type === 'file' && note.mime === 'application/pdf') {
type = 'pdf';
}
if (note.isProtected) {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
protectedSessionHolder.touchProtectedSession();
@@ -104,4 +110,4 @@ function getRenderingType(note) {
export default {
getRenderedContent
};
};

View File

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

View File

@@ -73,7 +73,7 @@ class TabContext extends Component {
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
if (triggerSwitchEvent) {
this.triggerEvent('tabNoteSwitched', {
await this.triggerEvent('tabNoteSwitched', {
tabContext: this,
notePath: this.notePath
});
@@ -127,4 +127,4 @@ class TabContext extends Component {
}
}
export default TabContext;
export default TabContext;

View File

@@ -203,7 +203,7 @@ export default class TabManager extends Component {
if (activate) {
this.activateTab(tabContext.tabId, false);
this.triggerEvent('tabNoteSwitchedAndActivated', {
await this.triggerEvent('tabNoteSwitchedAndActivated', {
tabContext,
notePath: tabContext.notePath // resolved note path
});

View File

@@ -22,7 +22,7 @@ async function resolveNotePath(notePath) {
*
* @return {string[]}
*/
async function getRunPath(notePath) {
async function getRunPath(notePath, logErrors = true) {
utils.assertArguments(notePath);
notePath = notePath.split("-")[0].trim();
@@ -66,10 +66,14 @@ async function getRunPath(notePath) {
}
if (!parents.some(p => p.noteId === parentNoteId)) {
console.debug(utils.now(), "Did not find parent " + parentNoteId + " for child " + childNoteId);
if (logErrors) {
console.log(utils.now(), "Did not find parent " + parentNoteId + " for child " + childNoteId);
}
if (parents.length > 0) {
console.debug(utils.now(), "Available parents:", parents);
if (logErrors) {
console.log(utils.now(), "Available parents:", parents);
}
const someNotePath = await getSomeNotePath(parents[0]);
@@ -86,7 +90,10 @@ async function getRunPath(notePath) {
break;
}
else {
console.log("No parents so no run path.");
if (logErrors) {
console.log("No parents so no run path.");
}
return;
}
}

View File

@@ -54,6 +54,10 @@ class TreeCache {
if (attr.type === 'relation' && attr.name === 'template' && !(attr.value in existingNotes) && !noteIds.has(attr.value)) {
missingNoteIds.push(attr.value);
}
if (!(attr.noteId in existingNotes) && !noteIds.has(attr.noteId)) {
missingNoteIds.push(attr.noteId);
}
}
if (missingNoteIds.length > 0) {
@@ -269,12 +273,28 @@ class TreeCache {
async getBranchId(parentNoteId, childNoteId) {
const child = await this.getNote(childNoteId);
if (!child) {
console.error(`Could not find branchId for parent=${parentNoteId}, child=${childNoteId} since child does not exist`);
return null;
}
return child.parentToBranch[parentNoteId];
}
/**
* @return {Promise<NoteComplement>}
*/
async getNoteComplement(noteId) {
if (!this.noteComplementPromises[noteId]) {
this.noteComplementPromises[noteId] = server.get('notes/' + noteId).then(row => new NoteComplement(row));
// we don't want to keep large payloads forever in memory so we clean that up quite quickly
// this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components)
// this is also a work around for missing invalidation after change
this.noteComplementPromises[noteId].then(
() => setTimeout(() => this.noteComplementPromises[noteId] = null, 1000)
);
}
return await this.noteComplementPromises[noteId];
@@ -283,4 +303,4 @@ class TreeCache {
const treeCache = new TreeCache();
export default treeCache;
export default treeCache;

View File

@@ -64,8 +64,19 @@ function assertArguments() {
}
}
const entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
function escapeHtml(str) {
return $('<div/>').text(str).html();
return str.replace(/[&<>"'`=\/]/g, s => entityMap[s]);
}
async function stopWatch(what, func) {
@@ -316,6 +327,27 @@ function dynamicRequire(moduleName) {
}
}
function timeLimit(promise, limitMs) {
// better stack trace if created outside of promise
const error = new Error('Process exceeded time limit ' + limitMs);
return new Promise((res, rej) => {
let resolved = false;
promise.then(result => {
resolved = true;
res(result);
});
setTimeout(() => {
if (!resolved) {
rej(error);
}
}, limitMs);
});
}
export default {
reloadApp,
parseDate,
@@ -355,5 +387,6 @@ export default {
normalizeShortcut,
copySelectionToClipboard,
isCKEditorInitialized,
dynamicRequire
};
dynamicRequire,
timeLimit
};

View File

@@ -25,7 +25,8 @@ function logError(message) {
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({
type: 'log-error',
error: message
error: message,
stack: new Error().stack
}));
}
}
@@ -156,7 +157,7 @@ async function consumeSyncData() {
const nonProcessedSyncRows = allSyncRows.filter(sync => !processedSyncIds.has(sync.id));
try {
await processSyncRows(nonProcessedSyncRows);
await utils.timeLimit(processSyncRows(nonProcessedSyncRows), 5000);
}
catch (e) {
logError(`Encountered error ${e.message}: ${e.stack}, reloading frontend.`);

View File

@@ -1,4 +1,5 @@
import CollapsibleWidget from "../collapsible_widget.js";
import treeCache from "../../services/tree_cache.js";
let linkMapContainerIdCtr = 1;
@@ -89,5 +90,19 @@ export default class LinkMapWidget extends CollapsibleWidget {
if (loadResults.getAttributes().find(attr => attr.type === 'relation' && (attr.noteId === this.noteId || attr.value === this.noteId))) {
this.noteSwitched();
}
const changedNoteIds = loadResults.getNoteIds();
if (changedNoteIds.length > 0) {
const $linkMapContainer = this.$body.find('.link-map-container');
for (const noteId of changedNoteIds) {
const note = treeCache.notes[noteId];
if (note) {
$linkMapContainer.find(`a[data-note-path="${noteId}"]`).text(note.title);
}
}
}
}
}

View File

@@ -4,7 +4,6 @@ const TPL = `
<table class="note-info-widget-table">
<style>
.note-info-widget-table {
table-layout: fixed;
width: 100%;
}
@@ -22,22 +21,23 @@ const TPL = `
<tr>
<th>Note ID:</th>
<td colspan="3" class="note-info-note-id"></td>
<td class="note-info-note-id"></td>
</tr>
<tr>
<th>Created:</th>
<td colspan="3" class="note-info-date-created"></td>
<td class="note-info-date-created"></td>
</tr>
<tr>
<th>Modified:</th>
<td colspan="3" class="note-info-date-modified"></td>
<td class="note-info-date-modified"></td>
</tr>
<tr>
<th>Type:</th>
<td class="note-info-type"></td>
<th>MIME:</th>
<td class="note-info-mime"></td>
<td>
<span class="note-info-type"></span>
<span class="note-info-mime"></span>
</td>
</tr>
</table>
`;
@@ -69,9 +69,12 @@ export default class NoteInfoWidget extends CollapsibleWidget {
this.$type.text(note.type);
this.$mime
.text(note.mime)
.attr("title", note.mime);
if (note.mime) {
this.$mime.text('(' + note.mime + ')');
}
else {
this.$mime.empty();
}
}
entitiesReloadedEvent({loadResults}) {

View File

@@ -1,13 +1,11 @@
import utils from '../services/utils.js';
import Mutex from "../services/mutex.js";
/**
* Abstract class for all components in the Trilium's frontend.
*
* Contains also event implementation with following properties:
* - event / command distribution is synchronous which among others mean that events are well ordered - event
* which was sent out first will also be processed first by the component since it was added to the mutex queue
* as the first one
* which was sent out first will also be processed first by the component
* - execution of the event / command is asynchronous - each component executes the event on its own without regard for
* other components.
* - although the execution is async, we are collecting all the promises and therefore it is possible to wait until the
@@ -19,7 +17,6 @@ export default class Component {
/** @type Component[] */
this.children = [];
this.initialized = Promise.resolve();
this.mutex = new Mutex();
}
setParent(parent) {
@@ -79,22 +76,8 @@ export default class Component {
return false;
}
let release;
await fun.call(this, data);
try {
if (this.mutex.isLocked()) {
console.debug("Mutex locked for", this.constructor.name);
}
release = await this.mutex.acquire();
await fun.call(this, data);
return true;
} finally {
if (release) {
release();
}
}
return true;
}
}

View File

@@ -35,7 +35,7 @@ const TPL = `
<a class="dropdown-item sync-now-button" title="Trigger sync">
<span class="bx bx-refresh"></span>
Sync (<span id="outstanding-syncs-count">0</span>)
Sync now (<span id="outstanding-syncs-count">0</span>)
</a>
<a class="dropdown-item" data-trigger-command="openNewWindow">
@@ -116,4 +116,4 @@ export default class GlobalMenuWidget extends BasicWidget {
return this.$widget;
}
}
}

View File

@@ -79,6 +79,7 @@ const TPL = `
<span class="slider checked"></span>
</span>
</div>
<a data-trigger-command="findInText" class="dropdown-item">Search in note <kbd data-command="findInText"></a>
<a data-trigger-command="showNoteRevisions" class="dropdown-item show-note-revisions-button">Revisions</a>
<a data-trigger-command="showAttributes" class="dropdown-item show-attributes-button"><kbd data-command="showAttributes"></kbd> Attributes</a>
<a data-trigger-command="showLinkMap" class="dropdown-item show-link-map-button"><kbd data-command="showLinkMap"></kbd> Link map</a>
@@ -140,4 +141,4 @@ export default class NoteActionsWidget extends TabAwareWidget {
this.refresh();
}
}
}
}

View File

@@ -270,13 +270,30 @@ export default class NoteDetailWidget extends TabAwareWidget {
}
async entitiesReloadedEvent({loadResults}) {
// FIXME: we should test what happens when the loaded note is deleted
if (loadResults.isNoteContentReloaded(this.noteId, this.componentId)
|| (loadResults.isNoteReloaded(this.noteId, this.componentId) && (this.type !== await this.getWidgetType() || this.mime !== this.note.mime))) {
this.handleEvent('noteTypeMimeChanged', {noteId: this.noteId});
}
else {
const attrs = loadResults.getAttributes();
const label = attrs.find(attr =>
attr.type === 'label'
&& ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'bookZoomLevel'].includes(attr.name)
&& attr.isAffecting(this.note));
const relation = attrs.find(attr =>
attr.type === 'relation'
&& ['template', 'renderNote'].includes(attr.name)
&& attr.isAffecting(this.note));
if (label || relation) {
// probably incorrect event
// calling this.refresh() is not enough since the event needs to be propagated to children as well
this.handleEvent('noteTypeMimeChanged', {noteId: this.noteId});
}
}
}
beforeUnloadEvent() {
@@ -314,4 +331,9 @@ export default class NoteDetailWidget extends TabAwareWidget {
saveSelection: true
});
}
// used by cutToNote in CKEditor build
async saveNoteDetailNowCommand() {
await this.spacedUpdate.updateNowIfNecessary();
}
}

View File

@@ -100,4 +100,4 @@ export default class NoteTitleWidget extends TabAwareWidget {
beforeUnloadEvent() {
this.spacedUpdate.updateNowIfNecessary();
}
}
}

View File

@@ -32,6 +32,7 @@ const TPL = `
.tree {
height: 100%;
overflow: auto;
padding-bottom: 20px;
}
.refresh-search-button {
@@ -249,9 +250,39 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.initialized = this.initFancyTree();
this.setupNoteTitleTooltip();
return this.$widget;
}
setupNoteTitleTooltip() {
// the following will dynamically set tree item's tooltip if the whole item's text is not currently visible
// if the whole text is visible then no tooltip is show since that's unnecessarily distracting
// see https://github.com/zadam/trilium/pull/1120 for discussion
// code inspired by https://gist.github.com/jtsternberg/c272d7de5b967cec2d3d
const isEnclosing = ($container, $sub) => {
const conOffset = $container.offset();
const conDistanceFromTop = conOffset.top + $container.outerHeight(true);
const conDistanceFromLeft = conOffset.left + $container.outerWidth(true);
const subOffset = $sub.offset();
const subDistanceFromTop = subOffset.top + $sub.outerHeight(true);
const subDistanceFromLeft = subOffset.left + $sub.outerWidth(true);
return conDistanceFromTop > subDistanceFromTop
&& conOffset.top < subOffset.top
&& conDistanceFromLeft > subDistanceFromLeft
&& conOffset.left < subOffset.left;
};
this.$tree.on("mouseenter", "span.fancytree-title", e => {
e.currentTarget.title = isEnclosing(this.$tree, $(e.currentTarget))
? ""
: e.currentTarget.innerText;
});
}
get hideArchivedNotes() {
return options.is("hideArchivedNotes_" + this.treeName);
}
@@ -273,10 +304,13 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.$tree.fancytree({
titlesTabbable: true,
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: utils.isMobile() ? ["dnd5", "clones"] : ["hotkeys", "dnd5", "clones"],
source: treeData,
scrollOfs: {
top: 100,
bottom: 100
},
scrollParent: this.$tree,
minExpandLevel: 2, // root can't be collapsed
click: (event, data) => {
@@ -576,7 +610,17 @@ export default class NoteTreeWidget extends TabAwareWidget {
const noteList = [];
const hideArchivedNotes = this.hideArchivedNotes;
for (const branch of this.getChildBranches(parentNote)) {
if (hideArchivedNotes) {
const note = await branch.getNote();
if (note.hasLabel('archived')) {
continue;
}
}
const node = await this.prepareNode(branch);
noteList.push(node);
@@ -600,6 +644,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
childBranches = childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId));
}
// we're not checking hideArchivedNotes since that would mean we need to lazy load the child notes
// which would seriously slow down everything.
// we check this flag only once user chooses to expand the parent. This has the negative consequence that
// note may appear as folder but not contain any children when all of them are archived
return childBranches;
}
@@ -728,17 +777,20 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
/** @return {FancytreeNode} */
async getNodeFromPath(notePath, expand = false) {
async getNodeFromPath(notePath, expand = false, logErrors = true) {
utils.assertArguments(notePath);
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
/** @var {FancytreeNode} */
/** @const {FancytreeNode} */
let parentNode = null;
const runPath = await treeService.getRunPath(notePath);
const runPath = await treeService.getRunPath(notePath, logErrors);
if (!runPath) {
console.error("Could not find run path for notePath:", notePath);
if (logErrors) {
console.error("Could not find run path for notePath:", notePath);
}
return;
}
@@ -775,7 +827,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
foundChildNode = this.findChildNode(parentNode, childNoteId);
if (!foundChildNode) {
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteId}, requested path is ${notePath}`);
if (logErrors) {
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteId}, requested path is ${notePath}`);
}
return;
}
}
@@ -802,8 +857,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
/** @return {FancytreeNode} */
async expandToNote(notePath) {
return this.getNodeFromPath(notePath, true);
async expandToNote(notePath, logErrors = true) {
return this.getNodeFromPath(notePath, true, logErrors);
}
updateNode(node) {
@@ -811,13 +866,14 @@ export default class NoteTreeWidget extends TabAwareWidget {
const branch = treeCache.getBranch(node.data.branchId);
const isFolder = this.isFolder(note);
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
node.data.isProtected = note.isProtected;
node.data.noteType = note.type;
node.folder = isFolder;
node.icon = this.getIcon(note, isFolder);
node.extraClasses = this.getExtraClasses(note);
node.title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
node.title = utils.escapeHtml(title);
if (node.isExpanded() !== branch.isExpanded) {
node.setExpanded(branch.isExpanded, {noEvents: true});
@@ -854,8 +910,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.toggleInt(this.isEnabled());
const oldActiveNode = this.getActiveNode();
let oldActiveNodeFocused = false;
if (oldActiveNode) {
oldActiveNodeFocused = oldActiveNode.hasFocus();
oldActiveNode.setActive(false);
oldActiveNode.setFocus(false);
}
@@ -868,8 +927,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
await this.expandToNote(this.tabContext.notePath);
}
newActiveNode.setActive(true, {noEvents: true});
newActiveNode.setActive(true, {noEvents: true, noFocus: !oldActiveNodeFocused});
newActiveNode.makeVisible({scrollIntoView: true});
}
}
@@ -898,7 +956,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
async entitiesReloadedEvent({loadResults}) {
const activeNode = this.getActiveNode();
const activeNodeFocused = activeNode ? activeNode.hasFocus() : false;
const activeNodeFocused = activeNode && activeNode.hasFocus();
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
@@ -1009,7 +1067,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
if (activeNotePath) {
let node = await this.expandToNote(activeNotePath);
let node = await this.expandToNote(activeNotePath, false);
if (node && node.data.noteId !== activeNoteId) {
// if the active note has been moved elsewhere then it won't be found by the path
@@ -1021,11 +1079,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
if (node) {
node.setActive(true, {noEvents: true});
node.setActive(true, {noEvents: true, noFocus: true});
}
else {
// this is used when original note has been deleted and we want to move the focus to the note above/below
node = await this.expandToNote(nextNotePath);
node = await this.expandToNote(nextNotePath, false);
if (node) {
await appContext.tabManager.getActiveTabContext().setNote(nextNotePath);
@@ -1036,7 +1094,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
// return focus if the previously active node was also focused
if (newActiveNode && activeNodeFocused) {
newActiveNode.setFocus(true);
await newActiveNode.setFocus(true);
}
}
}
@@ -1064,7 +1122,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
if (activeNotePath) {
const node = await this.getNodeFromPath(activeNotePath, true);
await node.setActive(true, {noEvents: true});
await node.setActive(true, {noEvents: true, noFocus: true});
}
}

View File

@@ -149,6 +149,8 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
cb(filtered);
}
}]);
$input.on('autocomplete:selected', e => this.promotedAttributeChanged(e))
});
}
else if (definition.labelType === 'number') {
@@ -229,7 +231,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
.prop("title", "Remove this attribute")
.on('click', async () => {
if (valueAttr.attributeId) {
await server.remove("notes/" + this.noteId + "/attributes/" + valueAttr.attributeId);
await server.remove("notes/" + this.noteId + "/attributes/" + valueAttr.attributeId, this.componentId);
}
$tr.remove();
@@ -263,8 +265,14 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
type: $attr.prop("attribute-type"),
name: $attr.prop("attribute-name"),
value: value
});
}, this.componentId);
$attr.prop("attribute-id", result.attributeId);
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
this.refresh();
}
}
}

View File

@@ -20,8 +20,8 @@ const TPL = `
}
</style>
<button class="hide-left-pane-button btn btn-sm icon-button bx bx-chevrons-left" title="Show sidebar"></button>
<button class="show-left-pane-button btn btn-sm icon-button bx bx-chevrons-right" title="Hide sidebar"></button>
<button class="hide-left-pane-button btn btn-sm icon-button bx bx-chevrons-left" title="Hide sidebar"></button>
<button class="show-left-pane-button btn btn-sm icon-button bx bx-chevrons-right" title="Show sidebar"></button>
<button class="hide-right-pane-button btn btn-sm icon-button bx bx-chevrons-right" title="Hide sidebar"></button>
<button class="show-right-pane-button btn btn-sm icon-button bx bx-chevrons-left" title="Show sidebar"></button>

View File

@@ -36,10 +36,10 @@ export default class AbstractTextTypeWidget extends TypeWidget {
.append($link)
);
const {renderedContent} = await noteContentRenderer.getRenderedContent(note);
const {renderedContent, type} = await noteContentRenderer.getRenderedContent(note);
$el.append(
$('<div class="include-note-content">')
$(`<div class="include-note-content type-${type}">`)
.append(renderedContent)
);
}
@@ -62,4 +62,12 @@ export default class AbstractTextTypeWidget extends TypeWidget {
$el.text(title);
}
}
refreshIncludedNote($container, noteId) {
if ($container) {
$container.find(`section[data-note-id="${noteId}"]`).each((_, el) => {
this.loadIncludedNote(noteId, $(el));
});
}
}
}

View File

@@ -257,4 +257,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.textEditor.model.insertContent(imageElement, this.textEditor.model.document.selection);
} );
}
async refreshIncludedNoteEvent({noteId}) {
this.refreshIncludedNote(this.$editor, noteId);
}
}

View File

@@ -121,10 +121,10 @@ export default class FileTypeWidget extends TypeWidget {
this.$pdfPreview.attr('src', '').empty().hide();
if (noteComplement.content) {
this.$previewContent.show();
this.$previewContent.show().scrollTop(0);
this.$previewContent.text(noteComplement.content);
}
else if (note.mime === 'application/pdf' && utils.isElectron()) {
else if (note.mime === 'application/pdf') {
this.$pdfPreview.show();
this.$pdfPreview.attr("src", utils.getUrlForDownload("api/notes/" + this.noteId + "/open"));
}

View File

@@ -81,4 +81,8 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
this.loadIncludedNote(noteId, $(el));
});
}
async refreshIncludedNoteEvent({noteId}) {
this.refreshIncludedNote(this.$content, noteId);
}
}

View File

@@ -0,0 +1,14 @@
{
"name": "Trilium",
"short_name": "Trilium",
"theme_color": "DarkGray",
"background_color": "DarkGray",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [{
"src": "images/app-icons/ios/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
}]
}

View File

@@ -68,6 +68,8 @@
.note-detail-image {
text-align: center;
height: 100%;
overflow: auto;
}
.note-detail-image-view {
@@ -92,4 +94,4 @@
max-height: 300px;
overflow: auto;
margin: 10px;
}
}

View File

@@ -693,11 +693,23 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
overflow: auto;
}
.include-note.box-size-small .include-note-content.type-pdf {
height: 10em; /* PDF is rendered in iframe and must be sized absolutely */
}
.include-note.box-size-medium .include-note-content {
max-height: 20em;
overflow: auto;
}
.include-note.box-size-medium .include-note-content.type-pdf {
height: 20em; /* PDF is rendered in iframe and must be sized absolutely */
}
.include-note.box-size-full .include-note-content.type-pdf {
height: 50em; /* PDF is rendered in iframe and it's not possible to put full height so at least a large height */
}
.alert-warning {
color: var(--main-text-color) !important;
background-color: var(--accented-background-color) !important;

View File

@@ -65,7 +65,11 @@ async function addClipping(req) {
}
async function createNote(req) {
const {title, content, pageUrl, images, clipType} = req.body;
let {title, content, pageUrl, images, clipType} = req.body;
if (!title || !title.trim()) {
title = "Clipped note from " + pageUrl;
}
log.info(`Creating clipped note from ${pageUrl}`);
@@ -155,4 +159,4 @@ module.exports = {
addClipping,
openNote,
handshake
};
};

View File

@@ -49,7 +49,7 @@ async function loginSync(req) {
return {
sourceId: sourceIdService.getCurrentSourceId(),
maxSyncId: await sql.getValue("SELECT MAX(id) FROM sync WHERE isSynced = 1")
maxSyncId: await sql.getValue("SELECT COALESCE(MAX(id), 0) FROM sync WHERE isSynced = 1")
};
}
@@ -68,7 +68,7 @@ async function loginToProtectedSession(req) {
const protectedSessionId = protectedSessionService.setDataKey(decryptedDataKey);
// this is set here so that event handlers have access to the protected session
cls.namespace.set('protectedSessionId', protectedSessionId);
cls.set('protectedSessionId', protectedSessionId);
await eventService.emit(eventService.ENTER_PROTECTED_SESSION);

View File

@@ -17,8 +17,9 @@ async function getNote(req) {
if (note.isStringNote()) {
note.content = await note.getContent();
if (note.type === 'file') {
note.content = note.content.substr(0, 10000);
if (note.type === 'file' && note.content.length > 10000) {
note.content = note.content.substr(0, 10000)
+ `\r\n\r\n... and ${note.content.length - 10000} more characters.`;
}
}
@@ -164,10 +165,16 @@ async function changeTitle(req) {
return [400, `Note ${noteId} is not available for change`];
}
const noteTitleChanged = note.title !== title;
note.title = title;
await note.save();
if (noteTitleChanged) {
await noteService.triggerNoteTitleChanged(note);
}
return note;
}
@@ -189,4 +196,4 @@ module.exports = {
getRelationMap,
changeTitle,
duplicateNote
};
};

View File

@@ -8,69 +8,55 @@ const noteCacheService = require('../../services/note_cache');
async function getRecentChanges(req) {
const {ancestorNoteId} = req.params;
const noteRows = await sql.getRows(
`
SELECT * FROM (
SELECT note_revisions.noteId,
note_revisions.noteRevisionId,
note_revisions.dateLastEdited AS date
FROM note_revisions
ORDER BY note_revisions.dateLastEdited DESC
)
UNION ALL SELECT * FROM (
SELECT
notes.noteId,
NULL AS noteRevisionId,
dateModified AS date
FROM notes
ORDER BY dateModified DESC
)
ORDER BY date DESC`);
let recentChanges = [];
const recentChanges = [];
const noteRevisions = await 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,
note_revisions.title,
note_revisions.utcDateCreated AS utcDate,
note_revisions.dateCreated AS date
FROM
note_revisions
JOIN notes USING(noteId)`);
for (const noteRow of noteRows) {
if (!noteCacheService.isInAncestor(noteRow.noteId, ancestorNoteId)) {
continue;
}
if (noteRow.noteRevisionId) {
recentChanges.push(await sql.getRow(`
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,
note_revisions.title,
note_revisions.dateCreated AS date
FROM
note_revisions
JOIN notes USING(noteId)
WHERE noteRevisionId = ?`, [noteRow.noteRevisionId]));
}
else {
recentChanges.push(await sql.getRow(`
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.dateModified AS date
FROM
notes
WHERE noteId = ?`, [noteRow.noteId]));
}
if (recentChanges.length >= 200) {
break;
for (const noteRevision of noteRevisions) {
if (noteCacheService.isInAncestor(noteRevision.noteId, ancestorNoteId)) {
recentChanges.push(noteRevision);
}
}
const notes = await 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`);
for (const note of notes) {
if (noteCacheService.isInAncestor(note.noteId, ancestorNoteId)) {
recentChanges.push(note);
}
}
recentChanges.sort((a, b) => a.utcDate > b.utcDate ? -1 : 1);
recentChanges = recentChanges.slice(0, Math.min(500, recentChanges.length));
console.log(recentChanges);
for (const change of recentChanges) {
if (change.current_isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
@@ -102,4 +88,4 @@ async function getRecentChanges(req) {
module.exports = {
getRecentChanges
};
};

View File

@@ -50,11 +50,13 @@ async function getStats() {
async function checkSync() {
return {
entityHashes: await contentHashService.getEntityHashes(),
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync WHERE isSynced = 1')
maxSyncId: await sql.getValue('SELECT COALESCE(MAX(id), 0) FROM sync WHERE isSynced = 1')
};
}
async function syncNow() {
log.info("Received request to trigger sync now.");
return await syncService.sync();
}
@@ -122,7 +124,7 @@ async function getChanged(req) {
const ret = {
syncs: await syncService.getSyncRecords(syncs),
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync WHERE isSynced = 1')
maxSyncId: await sql.getValue('SELECT COALESCE(MAX(id), 0) FROM sync WHERE isSynced = 1')
};
if (ret.syncs.length > 0) {
@@ -168,4 +170,4 @@ module.exports = {
getStats,
syncFinished,
queueSector
};
};

View File

@@ -2,67 +2,76 @@ const repository = require('../services/repository');
const log = require('../services/log');
const fileUploadService = require('./api/files.js');
const scriptService = require('../services/script');
const cls = require('../services/cls');
async function handleRequest(req, res) {
// express puts content after first slash into 0 index element
const path = req.params.path + req.params[0];
const attrs = await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')");
for (const attr of attrs) {
const regex = new RegExp(attr.value);
let match;
try {
match = path.match(regex);
}
catch (e) {
log.error(`Testing path for label ${attr.attributeId}, regex=${attr.value} failed with error ` + e.stack);
continue;
}
if (!match) {
continue;
}
if (attr.name === 'customRequestHandler') {
const note = await attr.getNote();
log.info(`Handling custom request "${path}" with note ${note.noteId}`);
try {
await scriptService.executeNote(note, {
pathParams: match.slice(1),
req,
res
});
}
catch (e) {
log.error(`Custom handler ${note.noteId} failed with ${e.message}`);
res.status(500).send(e.message);
}
}
else if (attr.name === 'customResourceProvider') {
await fileUploadService.downloadNoteFile(attr.noteId, res);
}
else {
throw new Error("Unrecognized attribute name " + attr.name);
}
return; // only first handler is executed
}
const message = `No handler matched for custom ${path} request.`;
log.info(message);
res.status(404).send(message);
}
function register(router) {
// explicitly no CSRF middleware since it's meant to allow integration from external services
router.all('/custom/:path*', async (req, res, next) => {
// express puts content after first slash into 0 index element
const path = req.params.path + req.params[0];
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
const attrs = await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')");
for (const attr of attrs) {
const regex = new RegExp(attr.value);
let match;
try {
match = path.match(regex);
}
catch (e) {
log.error(`Testing path for label ${attr.attributeId}, regex=${attr.value} failed with error ` + e.stack);
continue;
}
if (!match) {
continue;
}
if (attr.name === 'customRequestHandler') {
const note = await attr.getNote();
log.info(`Handling custom request "${path}" with note ${note.noteId}`);
try {
await scriptService.executeNote(note, {
pathParams: match.slice(1),
req,
res
});
}
catch (e) {
log.error(`Custom handler ${note.noteId} failed with ${e.message}`);
res.status(500).send(e.message);
}
}
else if (attr.name === 'customResourceProvider') {
await fileUploadService.downloadNoteFile(attr.noteId, res);
}
else {
throw new Error("Unrecognized attribute name " + attr.name);
}
return; // only first handler is executed
}
const message = `No handler matched for custom ${path} request.`;
log.info(message);
res.status(404).send(message);
cls.init(() => handleRequest(req, res));
});
}
module.exports = {
register
};
};

View File

@@ -23,7 +23,7 @@ async function index(req, res) {
treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize),
sourceId: await sourceIdService.generateSourceId(),
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"),
maxSyncIdAtLoad: await sql.getValue("SELECT COALESCE(MAX(id), 0) FROM sync"),
instanceName: config.General ? config.General.instanceName : null,
appCssNoteIds: await getAppCssNoteIds(),
isDev: env.isDev(),

View File

@@ -81,9 +81,12 @@ function apiRoute(method, path, routeHandler) {
function route(method, path, middleware, routeHandler, resultHandler, transactional = true) {
router[method](path, ...middleware, async (req, res, next) => {
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
const result = await cls.init(async () => {
cls.namespace.set('sourceId', req.headers['trilium-source-id']);
cls.namespace.set('localNowDateTime', req.headers['`trilium-local-now-datetime`']);
cls.set('sourceId', req.headers['trilium-source-id']);
cls.set('localNowDateTime', req.headers['`trilium-local-now-datetime`']);
protectedSessionService.setProtectedSessionId(req);
if (transactional) {

View File

@@ -4,7 +4,7 @@ const build = require('./build');
const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 158;
const APP_DB_VERSION = 159;
const SYNC_VERSION = 14;
const CLIPPER_PROTOCOL_VERSION = "1.0";
@@ -16,4 +16,4 @@ module.exports = {
buildRevision: build.buildRevision,
dataDirectory: TRILIUM_DATA_DIR,
clipperProtocolVersion: CLIPPER_PROTOCOL_VERSION
};
};

View File

@@ -27,9 +27,9 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'label', name: 'customRequestHandler', isDangerous: true },
{ type: 'label', name: 'customResourceProvider', isDangerous: true },
{ type: 'label', name: 'bookZoomLevel', isDangerous: false },
{ type: 'label', name: 'widget', isDangerous: true },
// relation names
{ type: 'relation', name: 'runOnNoteView', isDangerous: true },
{ type: 'relation', name: 'runOnNoteCreation', isDangerous: true },
{ type: 'relation', name: 'runOnNoteTitleChange', isDangerous: true },
{ type: 'relation', name: 'runOnNoteChange', isDangerous: true },
@@ -115,13 +115,24 @@ function isAttributeType(type) {
}
function isAttributeDangerous(type, name) {
return BUILTIN_ATTRIBUTES.some(attr =>
attr.type === attr.type &&
return BUILTIN_ATTRIBUTES.some(attr =>
attr.type === attr.type &&
attr.name.toLowerCase() === name.trim().toLowerCase() &&
attr.isDangerous
);
}
function getBuiltinAttributeNames() {
return BUILTIN_ATTRIBUTES
.map(attr => attr.name)
.concat([
'internalLink',
'imageLink',
'includeNoteLink',
'relationMapLink'
]);
}
module.exports = {
getNotesWithLabel,
getNotesWithLabels,
@@ -131,5 +142,6 @@ module.exports = {
createAttribute,
getAttributeNames,
isAttributeType,
isAttributeDangerous
};
isAttributeDangerous,
getBuiltinAttributeNames
};

View File

@@ -7,7 +7,9 @@ const dataDir = require('./data_dir');
const log = require('./log');
const sqlInit = require('./sql_init');
const syncMutexService = require('./sync_mutex');
const attributeService = require('./attributes');
const cls = require('./cls');
const utils = require('./utils');
const sqlite = require('sqlite');
const sqlite3 = require('sqlite3');
@@ -45,7 +47,7 @@ async function copyFile(backupFile) {
for (; attemptCount < COPY_ATTEMPT_COUNT && !success; attemptCount++) {
try {
await sql.executeNoWrap(`VACUUM INTO '${backupFile}'`);
await sql.executeWithoutTransaction(`VACUUM INTO '${backupFile}'`);
success = true;
} catch (e) {
@@ -96,15 +98,22 @@ async function anonymize() {
await db.run("UPDATE api_tokens SET token = 'API token value'");
await db.run("UPDATE notes SET title = 'title'");
await db.run("UPDATE note_contents SET content = 'text'");
await db.run("UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL");
await db.run("UPDATE note_revisions SET title = 'title'");
await db.run("UPDATE note_revision_contents SET content = 'title'");
await db.run("UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label'");
await db.run("UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name != 'template'");
await db.run("UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL");
// we want to delete all non-builtin attributes because they can contain sensitive names and values
// on the other hand builtin/system attrs should not contain any sensitive info
const builtinAttrs = attributeService.getBuiltinAttributeNames().map(name => "'" + utils.sanitizeSql(name) + "'").join(', ');
await db.run(`UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN(${builtinAttrs})`);
await db.run(`UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN (${builtinAttrs})`);
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
('documentId', 'documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
'passwordVerificationSalt', 'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')`);
('documentId', 'documentSecret', 'encryptedDataKey',
'passwordVerificationHash', 'passwordVerificationSalt',
'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')
AND value != ''`);
await db.run("VACUUM");
await db.close();

View File

@@ -1 +1 @@
module.exports = { buildDate:"2020-06-03T14:30:07+02:00", buildRevision: "c1fd9825aa6087b5061cdede5dba3f7f9dc62c31" };
module.exports = { buildDate:"2020-08-27T23:58:58+02:00", buildRevision: "dc288fb18c7622f6e08c574888dc0e8c90e544c2" };

View File

@@ -9,6 +9,14 @@ function wrap(callback) {
return async () => await init(callback);
}
function get(key) {
return namespace.get(key);
}
function set(key, value) {
namespace.set(key, value);
}
function getSourceId() {
return namespace.get('sourceId');
}
@@ -52,6 +60,8 @@ function setEntityToCache(entityName, entityId, entity) {
module.exports = {
init,
wrap,
get,
set,
namespace,
getSourceId,
getLocalNowDateTime,
@@ -62,4 +72,4 @@ module.exports = {
addSyncRow,
getEntityFromCache,
setEntityToCache
};
};

View File

@@ -3,7 +3,6 @@
const html = require('html');
const repository = require('../repository');
const dateUtils = require('../date_utils');
const zip = require('tar-stream');
const path = require('path');
const mimeTypes = require('mime-types');
const mdService = require('./md');
@@ -444,4 +443,4 @@ ${content}
module.exports = {
exportToZip
};
};

View File

@@ -68,6 +68,9 @@ eventService.subscribe(eventService.ENTITY_CREATED, async ({ entityName, entity
await note.setContent(await targetNote.getContent());
}
else if (entity.type === 'label' && entity.name === 'sorted') {
await treeService.sortNotesAlphabetically(entity.noteId);
}
}
else if (entityName === 'notes') {
await runAttachedRelations(entity, 'runOnNoteCreation', entity);
@@ -135,4 +138,4 @@ eventService.subscribe(eventService.ENTITY_DELETED, async ({ entityName, entity
targetNote.invalidateAttributeCache();
}
});
});
});

View File

@@ -98,7 +98,17 @@ async function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSw
async function shrinkImage(buffer, originalName) {
// we do resizing with max (100) quality which will be trimmed during optimization step next
const resizedImage = await resize(buffer, 100);
let resizedImage;
try {
resizedImage = await resize(buffer, 100);
}
catch (e) {
log.error("Failed to resize image '" + originalName + "'\nStack: " + e.stack);
resizedImage = buffer;
}
let finalImageBuffer;
const jpegQuality = await optionService.getOptionInt('imageJpegQuality');
@@ -107,7 +117,15 @@ async function shrinkImage(buffer, originalName) {
finalImageBuffer = await optimize(resizedImage, jpegQuality);
} catch (e) {
log.error("Failed to optimize image '" + originalName + "'\nStack: " + e.stack);
finalImageBuffer = await resize(buffer, jpegQuality);
try {
finalImageBuffer = await resize(buffer, jpegQuality);
}
catch (e) {
log.error("Failed to resize image '" + originalName + "'\nStack: " + e.stack);
finalImageBuffer = buffer;
}
}
// if resizing & shrinking did not help with size then save the original

View File

@@ -24,7 +24,7 @@ async function importSingleFile(taskContext, file, parentNote) {
return await importCodeNote(taskContext, file, parentNote);
}
if (["image/jpeg", "image/gif", "image/png", "image/webp"].includes(mime)) {
if (mime.startsWith("image/")) {
return await importImage(file, parentNote, taskContext);
}
@@ -166,4 +166,4 @@ function getFileNameWithoutExtension(filePath) {
module.exports = {
importSingleFile
};
};

View File

@@ -47,7 +47,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
return noteIdMap[origNoteId];
}
function getMeta(filePath) {
if (!metaFile) {
return {};
@@ -425,7 +425,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
}
for (const noteId in createdNoteIds) { // now the noteIds are unique
await noteService.scanForLinks(await repository.getNotes(noteId));
await noteService.scanForLinks(await repository.getNote(noteId));
if (!metaFile) {
// if there's no meta file then the notes are created based on the order in that tar file but that
@@ -459,4 +459,4 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
module.exports = {
importTar
};
};

View File

@@ -454,7 +454,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
});
for (const noteId in createdNoteIds) { // now the noteIds are unique
await noteService.scanForLinks(await repository.getNotes(noteId));
await noteService.scanForLinks(await repository.getNote(noteId));
if (!metaFile) {
// if there's no meta file then the notes are created based on the order in that tar file but that
@@ -481,4 +481,4 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
module.exports = {
importZip
};
};

View File

@@ -327,7 +327,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
{
actionName: "printActiveNote",
defaultShortcuts: [],
scope: "note-detail"
scope: "window"
},
{
actionName: "runActiveNote",

View File

@@ -279,20 +279,30 @@ const downloadImagePromises = {};
function replaceUrl(content, url, imageNote) {
const quotedUrl = utils.quoteRegex(url);
return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`);
return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "ig"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`);
}
async function downloadImages(noteId, content) {
const re = /<img[^>]*?\ssrc=['"]([^'">]+)['"]/ig;
let match;
const imageRe = /<img[^>]*?\ssrc=['"]([^'">]+)['"]/ig;
let imageMatch;
const origContent = content;
while (imageMatch = imageRe.exec(content)) {
const url = imageMatch[1];
const inlineImageMatch = /^data:image\/[a-z]+;base64,/.exec(url);
while (match = re.exec(origContent)) {
const url = match[1];
if (inlineImageMatch) {
const imageBase64 = url.substr(inlineImageMatch[0].length);
const imageBuffer = Buffer.from(imageBase64, 'base64');
if (!url.includes('api/images/')
// this is and exception for the web clipper's "imageId"
const imageService = require('../services/image');
const {note} = await imageService.saveImage(noteId, imageBuffer, "inline image", true);
content = content.substr(0, imageMatch.index)
+ `<img src="api/images/${note.noteId}/${note.title}"`
+ content.substr(imageMatch.index + imageMatch[0].length);
}
else if (!url.includes('api/images/')
// this is an exception for the web clipper's "imageId"
&& (url.length !== 20 || url.toLowerCase().startsWith('http'))) {
if (url in imageUrlToNoteIdMapping) {
@@ -303,7 +313,6 @@ async function downloadImages(noteId, content) {
}
else {
content = replaceUrl(content, url, imageNote);
continue;
}
}
@@ -315,7 +324,6 @@ async function downloadImages(noteId, content) {
imageUrlToNoteIdMapping[url] = existingImage.noteId;
content = replaceUrl(content, url, existingImage);
continue;
}
@@ -778,5 +786,6 @@ module.exports = {
protectNoteRecursively,
scanForLinks,
duplicateNote,
getUndeletedParentBranches
getUndeletedParentBranches,
triggerNoteTitleChanged
};

View File

@@ -51,7 +51,7 @@ async function initNotSyncedOptions(initialized, startNotePath = 'root', opts =
await optionService.createOption('theme', opts.theme || 'white', false);
await optionService.createOption('syncServerHost', opts.syncServerHost || '', false);
await optionService.createOption('syncServerTimeout', '5000', false);
await optionService.createOption('syncServerTimeout', '60000', false);
await optionService.createOption('syncProxy', opts.syncProxy || '', false);
}
@@ -116,4 +116,4 @@ module.exports = {
initSyncedOptions,
initNotSyncedOptions,
initStartupOptions
};
};

View File

@@ -15,11 +15,11 @@ function setDataKey(decryptedDataKey) {
}
function setProtectedSessionId(req) {
cls.namespace.set('protectedSessionId', req.cookies.protectedSessionId);
cls.set('protectedSessionId', req.cookies.protectedSessionId);
}
function getProtectedSessionId() {
return cls.namespace.get('protectedSessionId');
return cls.get('protectedSessionId');
}
function getDataKey() {
@@ -63,4 +63,4 @@ module.exports = {
decryptString,
decryptNotes,
setProtectedSessionId
};
};

View File

@@ -140,10 +140,6 @@ async function updateEntity(entity) {
await eventService.emit(entity.isDeleted ? eventService.ENTITY_DELETED : eventService.ENTITY_CHANGED, eventPayload);
}
}
if (entity.afterSaving) {
await entity.afterSaving();
}
});
}
@@ -159,4 +155,4 @@ module.exports = {
getOption,
updateEntity,
setEntityConstructor
};
};

View File

@@ -9,12 +9,13 @@ const syncOptions = require('./sync_options');
// this allows to support system proxy
function exec(opts) {
const client = getClient(opts);
// hack for cases where electron.net does not work but we don't want to set proxy
if (opts.proxy === 'noproxy') {
opts.proxy = null;
}
const client = getClient(opts);
const proxyAgent = getProxyAgent(opts);
const parsedTargetUrl = url.parse(opts.url);
@@ -40,7 +41,7 @@ function exec(opts) {
host: parsedTargetUrl.hostname,
port: parsedTargetUrl.port,
path: parsedTargetUrl.path,
timeout: opts.timeout,
timeout: opts.timeout, // works only for node.js client
headers,
agent: proxyAgent
});
@@ -83,10 +84,11 @@ function exec(opts) {
}
async function getImage(imageUrl) {
const proxyConf = await syncOptions.getSyncProxy();
const opts = {
method: 'GET',
url: imageUrl,
proxy: await syncOptions.getSyncProxy()
proxy: proxyConf !== "noproxy" ? proxyConf : null
};
const client = getClient(opts);
@@ -104,13 +106,15 @@ async function getImage(imageUrl) {
host: parsedTargetUrl.hostname,
port: parsedTargetUrl.port,
path: parsedTargetUrl.path,
timeout: opts.timeout,
timeout: opts.timeout, // works only for node client
headers: {},
agent: proxyAgent
});
request.on('error', err => reject(generateError(opts, err)));
request.on('abort', err => reject(generateError(opts, err)));
request.on('response', response => {
if (![200, 201, 204].includes(response.statusCode)) {
reject(generateError(opts, response.statusCode + ' ' + response.statusMessage));
@@ -173,4 +177,4 @@ function generateError(opts, message) {
module.exports = {
exec,
getImage
};
};

View File

@@ -31,7 +31,7 @@ async function executeBundle(bundle, apiParams = {}) {
apiParams.startNote = bundle.note;
}
cls.namespace.set('sourceId', 'script');
cls.set('sourceId', 'script');
// last \r\n is necessary if script contains line comment on its last line
const script = "async function() {\r\n" + bundle.script + "\r\n}";
@@ -187,4 +187,4 @@ module.exports = {
executeNoteNoException,
executeScript,
getScriptBundleForFrontend
};
};

View File

@@ -6,6 +6,7 @@ const optionService = require('./options');
const syncOptions = require('./sync_options');
const request = require('./request');
const appInfo = require('./app_info');
const utils = require('./utils');
async function hasSyncServerSchemaAndSeed() {
const response = await requestToSyncServer('GET', '/api/setup/status');
@@ -43,13 +44,15 @@ async function sendSeedToSyncServer() {
}
async function requestToSyncServer(method, path, body = null) {
return await request.exec({
const timeout = await syncOptions.getSyncTimeout();
return utils.timeLimit(request.exec({
method,
url: await syncOptions.getSyncServerHost() + path,
body,
proxy: await syncOptions.getSyncProxy(),
timeout: await syncOptions.getSyncTimeout()
});
timeout: timeout
}), timeout);
}
async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, password) {
@@ -115,4 +118,4 @@ module.exports = {
sendSeedToSyncServer,
setupSyncFromSyncServer,
getSyncSeedOptions
};
};

View File

@@ -9,7 +9,7 @@ function setDbConnection(connection) {
dbConnection = connection;
}
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `uncaughtException`, `SIGTERM`].forEach(eventType => {
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
process.on(eventType, () => {
if (dbConnection) {
// closing connection is especially important to fold -wal file into the main DB file
@@ -64,15 +64,15 @@ async function upsert(tableName, primaryKey, rec) {
}
async function beginTransaction() {
return await execute("BEGIN");
return await dbConnection.run("BEGIN");
}
async function commit() {
return await execute("COMMIT");
return await dbConnection.run("COMMIT");
}
async function rollback() {
return await execute("ROLLBACK");
return await dbConnection.run("ROLLBACK");
}
async function getRow(query, params = []) {
@@ -150,19 +150,25 @@ async function getColumn(query, params = []) {
}
async function execute(query, params = []) {
await startTransactionIfNecessary();
return await wrap(async db => db.run(query, ...params), query);
}
async function executeNoWrap(query, params = []) {
async function executeWithoutTransaction(query, params = []) {
await dbConnection.run(query, ...params);
}
async function executeMany(query, params) {
await startTransactionIfNecessary();
// essentially just alias
await getManyRows(query, params);
}
async function executeScript(query) {
await startTransactionIfNecessary();
return await wrap(async db => db.exec(query), query);
}
@@ -199,61 +205,68 @@ async function wrap(func, query) {
}
}
// true if transaction is active globally.
// cls.namespace.get('isTransactional') OTOH indicates active transaction in active CLS
let transactionActive = false;
// resolves when current transaction ends with either COMMIT or ROLLBACK
let transactionPromise = null;
let transactionPromiseResolve = null;
async function transactional(func) {
if (cls.namespace.get('isInTransaction')) {
return await func();
async function startTransactionIfNecessary() {
if (!cls.get('isTransactional')
|| cls.get('isInTransaction')) {
return;
}
while (transactionActive) {
await transactionPromise;
}
let ret = null;
const thisError = new Error(); // to capture correct stack trace in case of exception
// first set semaphore (atomic operation and only then start transaction
transactionActive = true;
transactionPromise = new Promise(async (resolve, reject) => {
try {
await beginTransaction();
transactionPromise = new Promise(res => transactionPromiseResolve = res);
cls.set('isInTransaction', true);
cls.namespace.set('isInTransaction', true);
await beginTransaction();
}
ret = await func();
async function transactional(func) {
// if the CLS is already transactional then the whole transaction is handled by higher level transactional() call
if (cls.get('isTransactional')) {
return await func();
}
cls.set('isTransactional', true); // this signals that transaction will be needed if there's a write operation
try {
const ret = await func();
if (cls.get('isInTransaction')) {
await commit();
// note that sync rows sent from this action will be sent again by scheduled periodic ping
require('./ws.js').sendPingToAllClients();
transactionActive = false;
resolve();
setTimeout(() => require('./ws').sendPingToAllClients(), 50);
}
catch (e) {
if (transactionActive) {
log.error("Error executing transaction, executing rollback. Inner stack: " + e.stack + "\nOutside stack: " + thisError.stack);
await rollback();
transactionActive = false;
}
reject(e);
}
finally {
cls.namespace.set('isInTransaction', false);
}
});
if (transactionActive) {
await transactionPromise;
return ret;
}
catch (e) {
if (cls.get('isInTransaction')) {
await rollback();
}
return ret;
throw e;
}
finally {
cls.namespace.set('isTransactional', false);
if (cls.namespace.get('isInTransaction')) {
transactionActive = false;
cls.namespace.set('isInTransaction', false);
// resolving even for rollback since this is just semaphore for allowing another write transaction to proceed
transactionPromiseResolve();
}
}
}
module.exports = {
@@ -268,7 +281,7 @@ module.exports = {
getMap,
getColumn,
execute,
executeNoWrap,
executeWithoutTransaction,
executeMany,
executeScript,
transactional,

View File

@@ -70,7 +70,7 @@ async function sync() {
};
}
else {
log.info("sync failed: " + e.message + e.stack);
log.info("sync failed: " + e.message + "\nstack: " + e.stack);
return {
success: false,
@@ -97,7 +97,6 @@ async function doLogin() {
const hash = utils.hmac(documentSecret, timestamp);
const syncContext = { cookieJar: {} };
const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', {
timestamp: timestamp,
syncVersion: appInfo.syncVersion,
@@ -124,7 +123,7 @@ async function doLogin() {
}
async function pullSync(syncContext) {
let appliedPulls = 0;
let atLeastOnePullApplied = false;
while (true) {
const lastSyncedPull = await getLastSyncedPull();
@@ -133,6 +132,9 @@ async function pullSync(syncContext) {
const startDate = Date.now();
const resp = await syncRequest(syncContext, 'GET', changesUri);
const pulledDate = Date.now();
stats.outstandingPulls = resp.maxSyncId - lastSyncedPull;
if (stats.outstandingPulls < 0) {
@@ -148,10 +150,10 @@ async function pullSync(syncContext) {
await sql.transactional(async () => {
for (const {sync, entity} of rows) {
if (!sourceIdService.isLocalSourceId(sync.sourceId)) {
if (appliedPulls === 0 && sync.entity !== 'recent_notes') { // send only for first
if (!atLeastOnePullApplied && sync.entity !== 'recent_notes') { // send only for first
ws.syncPullInProgress();
appliedPulls++;
atLeastOnePullApplied = true;
}
await syncUpdateService.updateEntity(sync, entity, syncContext.sourceId);
@@ -163,10 +165,10 @@ async function pullSync(syncContext) {
await setLastSyncedPull(rows[rows.length - 1].sync.id);
});
log.info(`Pulled and updated ${rows.length} changes from ${changesUri} in ${Date.now() - startDate}ms`);
log.info(`Pulled ${rows.length} changes starting at syncId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${stats.outstandingPulls} outstanding pulls`);
}
if (appliedPulls > 0) {
if (atLeastOnePullApplied) {
ws.syncPullFinished();
}
@@ -259,14 +261,18 @@ async function checkContentHash(syncContext) {
}
async function syncRequest(syncContext, method, requestPath, body) {
return await request.exec({
const timeout = await syncOptions.getSyncTimeout();
const opts = {
method,
url: await syncOptions.getSyncServerHost() + requestPath,
cookieJar: syncContext.cookieJar,
timeout: await syncOptions.getSyncTimeout(),
timeout: timeout,
body,
proxy: proxyToggle ? await syncOptions.getSyncProxy() : null
});
};
return await utils.timeLimit(request.exec(opts), timeout);
}
const primaryKeys = {
@@ -362,14 +368,14 @@ async function updatePushStats() {
}
async function getMaxSyncId() {
return await sql.getValue('SELECT MAX(id) FROM sync');
return await sql.getValue('SELECT COALESCE(MAX(id), 0) FROM sync');
}
sqlInit.dbReady.then(async () => {
setInterval(cls.wrap(sync), 60000);
// kickoff initial sync immediately
setTimeout(cls.wrap(sync), 1000);
setTimeout(cls.wrap(sync), 3000);
setInterval(cls.wrap(updatePushStats), 1000);
});
@@ -380,4 +386,4 @@ module.exports = {
getSyncRecords,
stats,
getMaxSyncId
};
};

View File

@@ -82,7 +82,8 @@ async function fillSyncRows(entityName, entityPrimaryKey, condition = '') {
entityName: entityName,
entityId: entityId,
sourceId: "SYNC_FILL",
utcSyncDate: dateUtils.utcNowDateTime()
utcSyncDate: dateUtils.utcNowDateTime(),
isSynced: true
});
}
}
@@ -127,4 +128,4 @@ module.exports = {
fillAllSyncRows,
addEntitySyncsForSector,
getMaxSyncId: () => maxSyncId
};
};

View File

@@ -159,7 +159,11 @@ function getContentDisposition(filename) {
return `file; filename="${sanitizedFilename}"; filename*=UTF-8''${sanitizedFilename}`;
}
const STRING_MIME_TYPES = ["application/x-javascript", "image/svg+xml"];
const STRING_MIME_TYPES = [
"application/javascript",
"application/x-javascript",
"image/svg+xml"
];
function isStringNote(type, mime) {
return ["text", "code", "relation-map", "search"].includes(type)
@@ -205,10 +209,39 @@ function formatDownloadTitle(filename, type, mime) {
}
}
if (mime === 'application/octet-stream') {
// we didn't find any good guess for this one, it will be better to just return
// the current name without fake extension. It's possible that the title still preserves to correct
// extension too
return filename;
}
return filename + '.' + extensions[0];
}
}
function timeLimit(promise, limitMs) {
// better stack trace if created outside of promise
const error = new Error('Process exceeded time limit ' + limitMs);
return new Promise((res, rej) => {
let resolved = false;
promise.then(result => {
resolved = true;
res(result);
});
setTimeout(() => {
if (!resolved) {
rej(error);
}
}, limitMs);
});
}
module.exports = {
randomSecureToken,
randomString,
@@ -237,5 +270,6 @@ module.exports = {
isStringNote,
quoteRegex,
replaceAll,
formatDownloadTitle
formatDownloadTitle,
timeLimit
};

View File

@@ -36,7 +36,7 @@ function init(httpServer, sessionParser) {
const message = JSON.parse(messageJson);
if (message.type === 'log-error') {
log.error('JS Error: ' + message.error);
log.info('JS Error: ' + message.error + '\r\nStack: ' + message.stack);
}
else if (message.type === 'ping') {
lastAcceptedSyncIds[ws.id] = message.lastSyncId;
@@ -141,4 +141,4 @@ module.exports = {
syncPullInProgress,
syncPullFinished,
sendPingToAllClients
};
};

View File

@@ -5,7 +5,7 @@
<link rel="shortcut icon" href="favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Trilium Notes</title>
<link rel="apple-touch-icon" sizes="180x180" href="images/app-icons/ios/apple-touch-icon.png">
<link rel="manifest" href="manifest.webmanifest">
<style>
.lds-roller {
@@ -140,4 +140,4 @@
<link rel="stylesheet" type="text/css" href="libraries/boxicons/css/boxicons.min.css">
</body>
</html>
</html>

View File

@@ -193,10 +193,11 @@
</div>
<script type="text/javascript">
const glob = {
window.glob = {
sourceId: ''
};
const syncInProgress = <%= syncInProgress ? 'true' : 'false' %>;
window.syncInProgress = <%= syncInProgress ? 'true' : 'false' %>;
</script>
<!-- Required for correct loading of scripts in Electron -->