mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 16:26:31 +01:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
443f389d73 | ||
|
|
08edc521e4 | ||
|
|
f54f6d09b0 | ||
|
|
3219441fdf | ||
|
|
1b0a2b41da | ||
|
|
582429e762 | ||
|
|
238df0fb40 | ||
|
|
89356918f1 | ||
|
|
263b65997c | ||
|
|
2b757bfccd | ||
|
|
c78ca4c9db | ||
|
|
a0395e9866 | ||
|
|
89aa4fbc73 | ||
|
|
74a7802088 | ||
|
|
a89b6711d1 | ||
|
|
b2549b2834 | ||
|
|
959c4cbe64 | ||
|
|
d03d3603d2 | ||
|
|
e1c2573778 | ||
|
|
2af2b45062 | ||
|
|
eabe4775bd | ||
|
|
da9b321aa0 | ||
|
|
c18d8d2d1b | ||
|
|
5f2361ebd5 | ||
|
|
9791dab97d | ||
|
|
85d986534d | ||
|
|
00faf758e8 | ||
|
|
6ba2e5cf73 | ||
|
|
fb3876d28b | ||
|
|
fb975849b9 | ||
|
|
16fef78344 | ||
|
|
e0b4b369dc | ||
|
|
0df7851214 | ||
|
|
5d47c2b23e | ||
|
|
d09b021487 | ||
|
|
910bda860c | ||
|
|
2d92b4931a | ||
|
|
212b719ee9 | ||
|
|
1db892d22f | ||
|
|
535dcb6d12 | ||
|
|
4426362799 | ||
|
|
2c609e8136 | ||
|
|
11b73b79ed | ||
|
|
e70c862e72 | ||
|
|
b3e66d5a83 | ||
|
|
e8cd821e57 | ||
|
|
be7ac74235 | ||
|
|
58fa0832f6 | ||
|
|
1502b9ce66 | ||
|
|
7307ca385f | ||
|
|
c1fd9825aa | ||
|
|
9de7d3fc53 | ||
|
|
3c5db844ba | ||
|
|
e7330c1104 | ||
|
|
ec4586b164 | ||
|
|
91e5f24798 | ||
|
|
38723e0189 | ||
|
|
8c88ce6f65 |
@@ -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
|
||||
|
||||
BIN
db/demo.zip
BIN
db/demo.zip
Binary file not shown.
@@ -1,167 +1,20 @@
|
||||
/*
|
||||
* !!!!!!! This stylesheet is heavily modified compared to the original for similarity with in-editor look !!!!!!!
|
||||
* This is used for printing and tar HTML export
|
||||
|
||||
* CKEditor 5 (v17.0.0) content styles.
|
||||
* Generated on Fri, 13 Mar 2020 13:27:10 GMT.
|
||||
* CKEditor 5 (v19.1.1) content styles.
|
||||
* Generated on Fri, 19 Jun 2020 01:26:44 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-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/imageresize.css */
|
||||
.ck-content .image.image_resized {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* ckeditor5-image/theme/imageresize.css */
|
||||
.ck-content .image.image_resized img {
|
||||
width: 100%;
|
||||
}
|
||||
/* ckeditor5-image/theme/imageresize.css */
|
||||
.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);
|
||||
}
|
||||
/* ckeditor5-image/theme/imagestyle.css */
|
||||
.ck-content .image-style-align-left {
|
||||
float: left;
|
||||
margin-right: var(--ck-image-style-spacing);
|
||||
}
|
||||
/* ckeditor5-image/theme/imagestyle.css */
|
||||
.ck-content .image-style-align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
/* ckeditor5-image/theme/imagestyle.css */
|
||||
.ck-content .image-style-align-right {
|
||||
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;
|
||||
padding-right: 1.5em;
|
||||
padding-left: 1.5em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
font-style: italic;
|
||||
border-left: solid 5px hsl(0, 0%, 80%);
|
||||
}
|
||||
/* ckeditor5-block-quote/theme/blockquote.css */
|
||||
.ck-content[dir="rtl"] blockquote {
|
||||
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,6 +82,95 @@
|
||||
.ck-content .todo-list .todo-list__label .todo-list__label__description {
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* ckeditor5-media-embed/theme/mediaembed.css */
|
||||
.ck-content .media {
|
||||
clear: both;
|
||||
margin: 1em 0;
|
||||
display: block;
|
||||
min-width: 15em;
|
||||
}
|
||||
/* ckeditor5-basic-styles/theme/code.css */
|
||||
.ck-content code {
|
||||
background-color: hsla(0, 0%, 78%, 0.3);
|
||||
padding: .15em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
/* ckeditor5-block-quote/theme/blockquote.css */
|
||||
.ck-content blockquote {
|
||||
overflow: hidden;
|
||||
padding-right: 1.5em;
|
||||
padding-left: 1.5em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
font-style: italic;
|
||||
border-left: solid 5px hsl(0, 0%, 80%);
|
||||
}
|
||||
/* ckeditor5-block-quote/theme/blockquote.css */
|
||||
.ck-content[dir="rtl"] blockquote {
|
||||
border-left: 0;
|
||||
border-right: solid 5px hsl(0, 0%, 80%);
|
||||
}
|
||||
/* 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-image/theme/imageresize.css */
|
||||
.ck-content .image.image_resized {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* ckeditor5-image/theme/imageresize.css */
|
||||
.ck-content .image.image_resized img {
|
||||
width: 100%;
|
||||
}
|
||||
/* ckeditor5-image/theme/imageresize.css */
|
||||
.ck-content .image.image_resized > figcaption {
|
||||
display: block;
|
||||
}
|
||||
/* 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;
|
||||
@@ -266,17 +208,89 @@
|
||||
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-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);
|
||||
}
|
||||
/* ckeditor5-image/theme/imagestyle.css */
|
||||
.ck-content .image-style-align-left {
|
||||
float: left;
|
||||
margin-right: var(--ck-image-style-spacing);
|
||||
}
|
||||
/* ckeditor5-image/theme/imagestyle.css */
|
||||
.ck-content .image-style-align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
/* ckeditor5-image/theme/imagestyle.css */
|
||||
.ck-content .image-style-align-right {
|
||||
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: 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-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-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 +307,13 @@
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
/* ckeditor5-horizontal-line/theme/horizontalline.css */
|
||||
.ck-content hr {
|
||||
margin: 15px 0;
|
||||
height: 4px;
|
||||
background: hsl(0, 0%, 87%);
|
||||
border: 0;
|
||||
}
|
||||
@media print {
|
||||
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||
.ck-content .page-break {
|
||||
@@ -302,4 +323,10 @@
|
||||
.ck-content .page-break::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
|
||||
|
||||
.ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
|
||||
display: none;
|
||||
}
|
||||
|
||||
2
libraries/ckeditor/ckeditor.js
vendored
2
libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.42.2",
|
||||
"version": "0.43.0-beta",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -3345,9 +3345,9 @@
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0.tgz",
|
||||
"integrity": "sha512-JsaSQNPh+XDYkLj8APtVKTtvpb86KIG57W5OOss4TNrn8L3isC9LsCITwfnVmGIXHhvX6oY/weCtN5hAAytjVg==",
|
||||
"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",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.42.4",
|
||||
"version": "0.43.2",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -78,7 +78,7 @@
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "9.0.0",
|
||||
"electron": "9.0.5",
|
||||
"electron-builder": "22.6.0",
|
||||
"electron-packager": "14.2.1",
|
||||
"electron-rebuild": "1.10.1",
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
const anonymizationService = require('./services/anonymization');
|
||||
const backupService = require('./services/backup');
|
||||
const sqlInit = require('./services/sql_init');
|
||||
require('./entities/entity_constructor');
|
||||
|
||||
anonymizationService.anonymize().then(filePath => {
|
||||
console.log("Anonymized file has been saved to:", filePath);
|
||||
sqlInit.dbReady.then(async () => {
|
||||
try {
|
||||
console.log("Starting anonymization...");
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
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.");
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e.message, e.stack);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
13
src/app.js
13
src/app.js
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -541,12 +541,13 @@ class Note extends Entity {
|
||||
/**
|
||||
* @return {Promise<Attribute>}
|
||||
*/
|
||||
async addAttribute(type, name, value = "") {
|
||||
async addAttribute(type, name, value = "", isInheritable = false) {
|
||||
const attr = new Attribute({
|
||||
noteId: this.noteId,
|
||||
type: type,
|
||||
name: name,
|
||||
value: value
|
||||
value: value,
|
||||
isInheritable: isInheritable
|
||||
});
|
||||
|
||||
await attr.save();
|
||||
@@ -556,12 +557,12 @@ class Note extends Entity {
|
||||
return attr;
|
||||
}
|
||||
|
||||
async addLabel(name, value = "") {
|
||||
return await this.addAttribute(LABEL, name, value);
|
||||
async addLabel(name, value = "", isInheritable = false) {
|
||||
return await this.addAttribute(LABEL, name, value, isInheritable);
|
||||
}
|
||||
|
||||
async addRelation(name, targetNoteId) {
|
||||
return await this.addAttribute(RELATION, name, targetNoteId);
|
||||
async addRelation(name, targetNoteId, isInheritable = false) {
|
||||
return await this.addAttribute(RELATION, name, targetNoteId, isInheritable);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -39,13 +39,14 @@ export async function showDialog(noteIds) {
|
||||
}
|
||||
|
||||
async function cloneNotesTo(notePath) {
|
||||
const targetNoteId = treeService.getNoteIdFromNotePath(notePath);
|
||||
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
|
||||
const targetBranchId = await treeCache.getBranchId(parentNoteId, noteId);
|
||||
|
||||
for (const cloneNoteId of clonedNoteIds) {
|
||||
await branchService.cloneNoteTo(cloneNoteId, targetNoteId, $clonePrefix.val());
|
||||
await branchService.cloneNoteTo(cloneNoteId, targetBranchId, $clonePrefix.val());
|
||||
|
||||
const clonedNote = await treeCache.getNote(cloneNoteId);
|
||||
const targetNote = await treeCache.getNote(targetNoteId);
|
||||
const targetNote = await treeCache.getBranch(targetBranchId).getNote();
|
||||
|
||||
toastService.showMessage(`Note "${clonedNote.title}" has been cloned into ${targetNote.title}`);
|
||||
}
|
||||
@@ -64,4 +65,4 @@ $form.on('submit', () => {
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,10 +32,11 @@ export async function showDialog(branchIds) {
|
||||
noteAutocompleteService.showRecentNotes($noteAutoComplete);
|
||||
}
|
||||
|
||||
async function moveNotesTo(parentNoteId) {
|
||||
await branchService.moveToParentNote(movedBranchIds, parentNoteId);
|
||||
async function moveNotesTo(parentBranchId) {
|
||||
await branchService.moveToParentNote(movedBranchIds, parentBranchId);
|
||||
|
||||
const parentNote = await treeCache.getNote(parentNoteId);
|
||||
const parentBranch = treeCache.getBranch(parentBranchId);
|
||||
const parentNote = await parentBranch.getNote();
|
||||
|
||||
toastService.showMessage(`Selected notes have been moved into ${parentNote.title}`);
|
||||
}
|
||||
@@ -46,13 +47,12 @@ $form.on('submit', () => {
|
||||
if (notePath) {
|
||||
$dialog.modal('hide');
|
||||
|
||||
const noteId = treeService.getNoteIdFromNotePath(notePath);
|
||||
|
||||
moveNotesTo(noteId);
|
||||
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
|
||||
treeCache.getBranchId(parentNoteId, noteId).then(branchId => moveNotesTo(branchId));
|
||||
}
|
||||
else {
|
||||
console.error("No path to move to.");
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,19 +17,18 @@ const TPL = `
|
||||
|
||||
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button><br/><br/>
|
||||
|
||||
<h4>Debugging</h4>
|
||||
<h4>Anonymize database</h4>
|
||||
|
||||
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and some non-sensitive metadata)
|
||||
for sharing online for debugging purposes without fear of leaking your personal data.</p>
|
||||
|
||||
<button id="anonymize-button" class="btn">Save anonymized database</button><br/><br/>
|
||||
|
||||
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
|
||||
for sharing online for debugging purposes without fear of leaking your personal data.</p>
|
||||
|
||||
<h4>Backup database</h4>
|
||||
|
||||
<button id="backup-database-button" class="btn">Backup database</button>
|
||||
<p>Trilium has automatic backup (daily, weekly, monthly), but you can also trigger backup manually here.</p>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<button id="backup-database-button" class="btn">Backup database now</button><br/><br/>
|
||||
|
||||
<h4>Vacuum database</h4>
|
||||
|
||||
@@ -61,9 +60,14 @@ export default class AdvancedOptions {
|
||||
});
|
||||
|
||||
this.$anonymizeButton.on('click', async () => {
|
||||
await server.post('anonymization/anonymize');
|
||||
const resp = await server.post('database/anonymize');
|
||||
|
||||
toastService.showMessage("Created anonymized database");
|
||||
if (!resp.success) {
|
||||
toastService.showError("Could not create anonymized database, check backend logs for details");
|
||||
}
|
||||
else {
|
||||
toastService.showMessage(`Created anonymized database in ${resp.anonymizedFilePath}`, 10000);
|
||||
}
|
||||
});
|
||||
|
||||
this.$backupDatabaseButton.on('click', async () => {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,34 @@ 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) {
|
||||
const attrNote = this.getNote();
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -198,8 +198,8 @@ ws.subscribeToMessages(async message => {
|
||||
}
|
||||
});
|
||||
|
||||
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
|
||||
const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
|
||||
async function cloneNoteTo(childNoteId, parentBranchId, prefix) {
|
||||
const resp = await server.put(`notes/${childNoteId}/clone-to/${parentBranchId}`, {
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,13 +33,13 @@ async function pasteAfter(afterBranchId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function pasteInto(parentNoteId) {
|
||||
async function pasteInto(parentBranchId) {
|
||||
if (isClipboardEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clipboardMode === 'cut') {
|
||||
await branchService.moveToParentNote(clipboardBranchIds, parentNoteId);
|
||||
await branchService.moveToParentNote(clipboardBranchIds, parentBranchId);
|
||||
|
||||
clipboardBranchIds = [];
|
||||
clipboardMode = null;
|
||||
@@ -50,7 +50,7 @@ async function pasteInto(parentNoteId) {
|
||||
for (const clipboardBranch of clipboardBranches) {
|
||||
const clipboardNote = await clipboardBranch.getNote();
|
||||
|
||||
await branchService.cloneNoteTo(clipboardNote.noteId, parentNoteId);
|
||||
await branchService.cloneNoteTo(clipboardNote.noteId, parentBranchId);
|
||||
}
|
||||
|
||||
// copy will keep clipboardBranchIds and clipboardMode so it's possible to paste into multiple places
|
||||
@@ -89,4 +89,4 @@ export default {
|
||||
cut,
|
||||
copy,
|
||||
isClipboardEmpty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -91,4 +88,4 @@ function setupGlobs() {
|
||||
|
||||
export default {
|
||||
setupGlobs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ let protectedSessionDeferred = null;
|
||||
|
||||
async function leaveProtectedSession() {
|
||||
if (protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||
utils.reloadApp();
|
||||
protectedSessionHolder.resetProtectedSession();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,4 +113,4 @@ export default {
|
||||
enterProtectedSession,
|
||||
leaveProtectedSession,
|
||||
setupProtectedSession
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -272,6 +276,9 @@ class TreeCache {
|
||||
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));
|
||||
@@ -283,4 +290,4 @@ class TreeCache {
|
||||
|
||||
const treeCache = new TreeCache();
|
||||
|
||||
export default treeCache;
|
||||
export default treeCache;
|
||||
|
||||
@@ -64,8 +64,19 @@ function assertArguments() {
|
||||
}
|
||||
}
|
||||
|
||||
const entityMap = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
|
||||
function escapeHtml(str) {
|
||||
return $('<div/>').text(str).html();
|
||||
return str.replace(/[&<>"'`=\/]/g, s => entityMap[s]);
|
||||
}
|
||||
|
||||
async function stopWatch(what, func) {
|
||||
@@ -316,6 +327,24 @@ function dynamicRequire(moduleName) {
|
||||
}
|
||||
}
|
||||
|
||||
function timeLimit(promise, limitMs) {
|
||||
return new Promise((res, rej) => {
|
||||
let resolved = false;
|
||||
|
||||
promise.then(result => {
|
||||
resolved = true;
|
||||
|
||||
res(result);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
rej(new Error('Process exceeded time limit ' + limitMs));
|
||||
}
|
||||
}, limitMs);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
reloadApp,
|
||||
parseDate,
|
||||
@@ -355,5 +384,6 @@ export default {
|
||||
normalizeShortcut,
|
||||
copySelectionToClipboard,
|
||||
isCKEditorInitialized,
|
||||
dynamicRequire
|
||||
};
|
||||
dynamicRequire,
|
||||
timeLimit
|
||||
};
|
||||
|
||||
@@ -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.`);
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -100,4 +100,4 @@ export default class NoteTitleWidget extends TabAwareWidget {
|
||||
beforeUnloadEvent() {
|
||||
this.spacedUpdate.updateNowIfNecessary();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,72 @@ const TPL = `
|
||||
width: 320px;
|
||||
border-radius: 10px 0 10px 10px;
|
||||
}
|
||||
|
||||
ul.fancytree-container {
|
||||
outline: none !important;
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
.fancytree-custom-icon {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
span.fancytree-title {
|
||||
color: inherit !important;
|
||||
background: inherit !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
span.fancytree-node.protected > span.fancytree-custom-icon {
|
||||
filter: drop-shadow(2px 2px 2px var(--main-text-color));
|
||||
}
|
||||
|
||||
span.fancytree-node.multiple-parents .fancytree-title::after {
|
||||
content: " *"
|
||||
}
|
||||
|
||||
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
|
||||
.ui-fancytree > li > ul {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-active .fancytree-title {
|
||||
font-weight: bold;
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||
border-style: dashed !important;
|
||||
}
|
||||
|
||||
span.fancytree-focused .fancytree-title, span.fancytree-focused.fancytree-selected .fancytree-title {
|
||||
color: var(--active-item-text-color) !important;
|
||||
background-color: var(--active-item-background-color) !important;
|
||||
border-color: var(--main-background-color) !important; /* invisible border */
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-selected .fancytree-title {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--main-background-color) !important; /* invisible border */
|
||||
border-radius: 5px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
span.fancytree-node:hover span.fancytree-title {
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-node.archived {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button class="btn btn-sm icon-button bx bx-cog tree-settings-button" title="Tree settings"></button>
|
||||
@@ -183,9 +249,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);
|
||||
}
|
||||
@@ -206,6 +302,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
const treeData = [await this.prepareRootNode()];
|
||||
|
||||
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"],
|
||||
@@ -509,7 +606,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);
|
||||
@@ -533,6 +640,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;
|
||||
}
|
||||
|
||||
@@ -651,27 +763,30 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
const activeContext = appContext.tabManager.getActiveTabContext();
|
||||
|
||||
if (activeContext && activeContext.notePath) {
|
||||
this.tree.setFocus();
|
||||
this.tree.setFocus(true);
|
||||
|
||||
const node = await this.expandToNote(activeContext.notePath);
|
||||
|
||||
await node.makeVisible({scrollIntoView: true});
|
||||
node.setFocus();
|
||||
node.setFocus(true);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return {FancytreeNode} */
|
||||
async getNodeFromPath(notePath, expand = false, expandOpts = {}) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -690,7 +805,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
if (expand) {
|
||||
await parentNode.setExpanded(true, expandOpts);
|
||||
await parentNode.setExpanded(true);
|
||||
|
||||
// although previous line should set the expanded status, it seems to happen asynchronously
|
||||
// so we need to make sure it is set properly before calling updateNode which uses this flag
|
||||
const branch = treeCache.getBranch(parentNode.data.branchId);
|
||||
branch.isExpanded = true;
|
||||
}
|
||||
|
||||
this.updateNode(parentNode);
|
||||
@@ -703,7 +823,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;
|
||||
}
|
||||
}
|
||||
@@ -730,8 +853,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
/** @return {FancytreeNode} */
|
||||
async expandToNote(notePath, expandOpts) {
|
||||
return this.getNodeFromPath(notePath, true, expandOpts);
|
||||
async expandToNote(notePath, logErrors = true) {
|
||||
return this.getNodeFromPath(notePath, true, logErrors);
|
||||
}
|
||||
|
||||
updateNode(node) {
|
||||
@@ -739,13 +862,19 @@ 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});
|
||||
}
|
||||
|
||||
node.renderTitle();
|
||||
}
|
||||
|
||||
@@ -777,8 +906,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);
|
||||
}
|
||||
@@ -791,8 +923,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});
|
||||
}
|
||||
}
|
||||
@@ -821,6 +952,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
|
||||
async entitiesReloadedEvent({loadResults}) {
|
||||
const activeNode = this.getActiveNode();
|
||||
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;
|
||||
@@ -931,7 +1063,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
|
||||
@@ -943,19 +1075,23 @@ 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) {
|
||||
this.tree.setFocus();
|
||||
node.setFocus(true);
|
||||
|
||||
await appContext.tabManager.getActiveTabContext().setNote(nextNotePath);
|
||||
}
|
||||
}
|
||||
|
||||
const newActiveNode = this.getActiveNode();
|
||||
|
||||
// return focus if the previously active node was also focused
|
||||
if (newActiveNode && activeNodeFocused) {
|
||||
await newActiveNode.setFocus(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -982,7 +1118,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});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1079,7 +1215,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
const toNode = node.getPrevSibling();
|
||||
|
||||
if (toNode !== null) {
|
||||
branchService.moveToParentNote([node.data.branchId], toNode.data.noteId);
|
||||
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,7 +1300,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
pasteNotesFromClipboardCommand({node}) {
|
||||
clipboard.pasteInto(node.data.noteId);
|
||||
clipboard.pasteInto(node.data.branchId);
|
||||
}
|
||||
|
||||
pasteNotesAfterFromClipboard({node}) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ const mentionSetup = {
|
||||
row.text = row.name = row.noteTitle;
|
||||
row.id = '@' + row.text;
|
||||
row.link = '#' + row.path;
|
||||
row.notePath = row.path;
|
||||
}
|
||||
|
||||
res(rows);
|
||||
@@ -256,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,4 +81,8 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
|
||||
this.loadIncludedNote(noteId, $(el));
|
||||
});
|
||||
}
|
||||
|
||||
async refreshIncludedNoteEvent({noteId}) {
|
||||
this.refreshIncludedNote(this.$content, noteId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,71 +113,6 @@ span.fancytree-node.muted { opacity: 0.6; }
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
ul.fancytree-container {
|
||||
outline: none !important;
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
.fancytree-custom-icon {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
span.fancytree-title {
|
||||
color: inherit !important;
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
span.fancytree-node.protected > span.fancytree-custom-icon {
|
||||
filter: drop-shadow(2px 2px 2px var(--main-text-color));
|
||||
}
|
||||
|
||||
span.fancytree-node.multiple-parents .fancytree-title::after {
|
||||
content: " *"
|
||||
}
|
||||
|
||||
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
|
||||
.ui-fancytree > li > ul {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-active .fancytree-title {
|
||||
font-weight: bold;
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||
border-style: dashed !important;
|
||||
}
|
||||
|
||||
span.fancytree-focused .fancytree-title, span.fancytree-focused.fancytree-selected .fancytree-title {
|
||||
color: var(--active-item-text-color) !important;
|
||||
background-color: var(--active-item-background-color) !important;
|
||||
border-color: var(--main-background-color) !important; /* invisible border */
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-selected .fancytree-title {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--main-background-color) !important; /* invisible border */
|
||||
border-radius: 5px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
span.fancytree-node:hover span.fancytree-title {
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-node.archived {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ui-autocomplete {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
@@ -873,4 +808,4 @@ body {
|
||||
|
||||
.hidden-int, .hidden-ext {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const anonymization = require('../../services/anonymization');
|
||||
|
||||
async function anonymize() {
|
||||
await anonymization.anonymize();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
anonymize
|
||||
};
|
||||
@@ -125,6 +125,7 @@ async function setExpanded(req) {
|
||||
if (branchId !== 'root') {
|
||||
await sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
|
||||
// we don't sync expanded label
|
||||
// also this does not trigger updates to the frontend, this would trigger too many reloads
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
const cloningService = require('../../services/cloning');
|
||||
|
||||
async function cloneNoteToParent(req) {
|
||||
const {noteId, parentNoteId} = req.params;
|
||||
const {noteId, parentBranchId} = req.params;
|
||||
const {prefix} = req.body;
|
||||
|
||||
return await cloningService.cloneNoteToParent(noteId, parentNoteId, prefix);
|
||||
return await cloningService.cloneNoteToParent(noteId, parentBranchId, prefix);
|
||||
}
|
||||
|
||||
async function cloneNoteAfter(req) {
|
||||
@@ -18,4 +18,4 @@ async function cloneNoteAfter(req) {
|
||||
module.exports = {
|
||||
cloneNoteToParent,
|
||||
cloneNoteAfter
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,6 +5,10 @@ const log = require('../../services/log');
|
||||
const backupService = require('../../services/backup');
|
||||
const consistencyChecksService = require('../../services/consistency_checks');
|
||||
|
||||
async function anonymize() {
|
||||
return await backupService.anonymize();
|
||||
}
|
||||
|
||||
async function backupDatabase() {
|
||||
return {
|
||||
backupFile: await backupService.backupNow("now")
|
||||
@@ -24,5 +28,6 @@ async function findAndFixConsistencyIssues() {
|
||||
module.exports = {
|
||||
backupDatabase,
|
||||
vacuumDatabase,
|
||||
findAndFixConsistencyIssues
|
||||
findAndFixConsistencyIssues,
|
||||
anonymize
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -164,10 +164,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 +195,4 @@ module.exports = {
|
||||
getRelationMap,
|
||||
changeTitle,
|
||||
duplicateNote
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -55,6 +55,8 @@ async function checkSync() {
|
||||
}
|
||||
|
||||
async function syncNow() {
|
||||
log.info("Received request to trigger sync now.");
|
||||
|
||||
return await syncService.sync();
|
||||
}
|
||||
|
||||
@@ -168,4 +170,4 @@ module.exports = {
|
||||
getStats,
|
||||
syncFinished,
|
||||
queueSector
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,7 +24,6 @@ const exportRoute = require('./api/export');
|
||||
const importRoute = require('./api/import');
|
||||
const setupApiRoute = require('./api/setup');
|
||||
const sqlRoute = require('./api/sql');
|
||||
const anonymizationRoute = require('./api/anonymization');
|
||||
const databaseRoute = require('./api/database');
|
||||
const imageRoute = require('./api/image');
|
||||
const attributesRoute = require('./api/attributes');
|
||||
@@ -82,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) {
|
||||
@@ -152,7 +154,7 @@ function register(app) {
|
||||
|
||||
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
|
||||
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentNoteId', cloningApiRoute.cloneNoteToParent);
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentBranchId', cloningApiRoute.cloneNoteToParent);
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
|
||||
|
||||
route(GET, '/api/notes/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch);
|
||||
@@ -220,7 +222,7 @@ function register(app) {
|
||||
|
||||
apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema);
|
||||
apiRoute(POST, '/api/sql/execute', sqlRoute.execute);
|
||||
apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
|
||||
route(POST, '/api/database/anonymize', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false);
|
||||
|
||||
// backup requires execution outside of transaction
|
||||
route(POST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false);
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const dataDir = require('./data_dir');
|
||||
const dateUtils = require('./date_utils');
|
||||
const fs = require('fs-extra');
|
||||
const sqlite = require('sqlite');
|
||||
|
||||
async function anonymize() {
|
||||
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
|
||||
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
|
||||
}
|
||||
|
||||
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
|
||||
|
||||
fs.copySync(dataDir.DOCUMENT_PATH, anonymizedFile);
|
||||
|
||||
const db = await sqlite.open(anonymizedFile, {Promise});
|
||||
|
||||
await db.run("UPDATE notes SET title = 'title'");
|
||||
await db.run("UPDATE note_contents SET content = 'text'");
|
||||
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'");
|
||||
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
|
||||
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
|
||||
('documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
|
||||
'passwordVerificationSalt', 'passwordDerivedKeySalt')`);
|
||||
await db.run("VACUUM");
|
||||
|
||||
await db.close();
|
||||
|
||||
return anonymizedFile;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
anonymize
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -7,7 +7,11 @@ 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');
|
||||
|
||||
async function regularBackup() {
|
||||
await periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
|
||||
@@ -28,46 +32,98 @@ async function periodBackup(optionName, fileName, periodInSeconds) {
|
||||
}
|
||||
}
|
||||
|
||||
const BACKUP_ATTEMPT_COUNT = 50;
|
||||
const COPY_ATTEMPT_COUNT = 50;
|
||||
|
||||
async function backupNow(name) {
|
||||
async function copyFile(backupFile) {
|
||||
const sql = require('./sql');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(backupFile);
|
||||
} catch (e) {
|
||||
} // unlink throws exception if the file did not exist
|
||||
|
||||
let success = false;
|
||||
let attemptCount = 0
|
||||
|
||||
for (; attemptCount < COPY_ATTEMPT_COUNT && !success; attemptCount++) {
|
||||
try {
|
||||
await sql.executeWithoutTransaction(`VACUUM INTO '${backupFile}'`);
|
||||
|
||||
success = true;
|
||||
} catch (e) {
|
||||
log.info(`Copy DB attempt ${attemptCount + 1} failed with "${e.message}", retrying...`);
|
||||
}
|
||||
// we re-try since VACUUM is very picky and it can't run if there's any other query currently running
|
||||
// which is difficult to guarantee so we just re-try
|
||||
}
|
||||
|
||||
return attemptCount !== COPY_ATTEMPT_COUNT;
|
||||
}
|
||||
|
||||
async function backupNow(name) {
|
||||
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
|
||||
return await syncMutexService.doExclusively(async () => {
|
||||
const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`;
|
||||
|
||||
try {
|
||||
fs.unlinkSync(backupFile);
|
||||
}
|
||||
catch (e) {} // unlink throws exception if the file did not exist
|
||||
const success = await copyFile(backupFile);
|
||||
|
||||
let success = false;
|
||||
let attemptCount = 0
|
||||
|
||||
for (; attemptCount < BACKUP_ATTEMPT_COUNT && !success; attemptCount++) {
|
||||
try {
|
||||
await sql.executeNoWrap(`VACUUM INTO '${backupFile}'`);
|
||||
success++;
|
||||
}
|
||||
catch (e) {
|
||||
log.info(`Backup attempt ${attemptCount + 1} failed with "${e.message}", retrying...`);
|
||||
}
|
||||
// we re-try since VACUUM is very picky and it can't run if there's any other query currently running
|
||||
// which is difficult to guarantee so we just re-try
|
||||
}
|
||||
|
||||
if (attemptCount === BACKUP_ATTEMPT_COUNT) {
|
||||
log.error(`Creating backup ${backupFile} failed`);
|
||||
if (success) {
|
||||
log.info("Created backup at " + backupFile);
|
||||
}
|
||||
else {
|
||||
log.info("Created backup at " + backupFile);
|
||||
log.error(`Creating backup ${backupFile} failed`);
|
||||
}
|
||||
|
||||
return backupFile;
|
||||
});
|
||||
}
|
||||
|
||||
async function anonymize() {
|
||||
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
|
||||
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
|
||||
}
|
||||
|
||||
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
|
||||
|
||||
const success = await copyFile(anonymizedFile);
|
||||
|
||||
if (!success) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const db = await sqlite.open({
|
||||
filename: anonymizedFile,
|
||||
driver: sqlite3.Database
|
||||
});
|
||||
|
||||
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' WHERE content IS NOT NULL");
|
||||
await db.run("UPDATE note_revisions SET title = 'title'");
|
||||
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')
|
||||
AND value != ''`);
|
||||
await db.run("VACUUM");
|
||||
|
||||
await db.close();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
anonymizedFilePath: anonymizedFile
|
||||
};
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
|
||||
fs.mkdirSync(dataDir.BACKUP_DIR, 0o700);
|
||||
}
|
||||
@@ -80,5 +136,6 @@ sqlInit.dbReady.then(() => {
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
backupNow
|
||||
backupNow,
|
||||
anonymize
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2020-05-31T10:33:12+02:00", buildRevision: "50a28d8c5198606ee5d92696095c1c97397592e8" };
|
||||
module.exports = { buildDate:"2020-07-11T23:58:59+02:00", buildRevision: "08edc521e48ea7c6de96c19290134b6552844313" };
|
||||
|
||||
@@ -9,12 +9,14 @@ const Branch = require('../entities/branch');
|
||||
const TaskContext = require("./task_context.js");
|
||||
const utils = require('./utils');
|
||||
|
||||
async function cloneNoteToParent(noteId, parentNoteId, prefix) {
|
||||
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentNoteId)) {
|
||||
async function cloneNoteToParent(noteId, parentBranchId, prefix) {
|
||||
const parentBranch = await repository.getBranch(parentBranchId);
|
||||
|
||||
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentBranch.noteId)) {
|
||||
return { success: false, message: 'Note is deleted.' };
|
||||
}
|
||||
|
||||
const validationResult = await treeService.validateParentChild(parentNoteId, noteId);
|
||||
const validationResult = await treeService.validateParentChild(parentBranch.noteId, noteId);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return validationResult;
|
||||
@@ -22,12 +24,13 @@ async function cloneNoteToParent(noteId, parentNoteId, prefix) {
|
||||
|
||||
const branch = await new Branch({
|
||||
noteId: noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
parentNoteId: parentBranch.noteId,
|
||||
prefix: prefix,
|
||||
isExpanded: 0
|
||||
}).save();
|
||||
|
||||
await sql.execute("UPDATE branches SET isExpanded = 1 WHERE noteId = ?", [parentNoteId]);
|
||||
parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user
|
||||
await parentBranch.save();
|
||||
|
||||
return { success: true, branchId: branch.branchId };
|
||||
}
|
||||
@@ -111,4 +114,4 @@ module.exports = {
|
||||
ensureNoteIsAbsentFromParent,
|
||||
toggleNoteInParent,
|
||||
cloneNoteAfter
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
{
|
||||
actionName: "scrollToActiveNote",
|
||||
defaultShortcuts: ["CommandOrControl+."],
|
||||
scope: "window" // FIXME - how do we find what note tree should be updated?
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
actionName: "searchNotes",
|
||||
@@ -55,7 +55,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
actionName: "collapseTree",
|
||||
defaultShortcuts: ["Alt+C"],
|
||||
description: "Collapses the complete note tree",
|
||||
scope: "note-tree"
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
actionName: "collapseSubtree",
|
||||
@@ -327,7 +327,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
{
|
||||
actionName: "printActiveNote",
|
||||
defaultShortcuts: [],
|
||||
scope: "note-detail"
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
actionName: "runActiveNote",
|
||||
@@ -425,4 +425,4 @@ async function getKeyboardActions() {
|
||||
module.exports = {
|
||||
DEFAULT_KEYBOARD_ACTIONS,
|
||||
getKeyboardActions
|
||||
};
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ async function createNewNote(params) {
|
||||
const parentNote = await repository.getNote(params.parentNoteId);
|
||||
|
||||
if (!parentNote) {
|
||||
throw new Error(`Parent note ${params.parentNoteId} not found.`);
|
||||
throw new Error(`Parent note "${params.parentNoteId}" not found.`);
|
||||
}
|
||||
|
||||
if (!params.title || params.title.trim().length === 0) {
|
||||
@@ -778,5 +778,6 @@ module.exports = {
|
||||
protectNoteRecursively,
|
||||
scanForLinks,
|
||||
duplicateNote,
|
||||
getUndeletedParentBranches
|
||||
getUndeletedParentBranches,
|
||||
triggerNoteTitleChanged
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -104,13 +105,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 +176,4 @@ function generateError(opts, message) {
|
||||
module.exports = {
|
||||
exec,
|
||||
getImage
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -259,14 +258,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 = {
|
||||
@@ -369,7 +372,7 @@ 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 +383,4 @@ module.exports = {
|
||||
getSyncRecords,
|
||||
stats,
|
||||
getMaxSyncId
|
||||
};
|
||||
};
|
||||
|
||||
@@ -205,10 +205,36 @@ 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) {
|
||||
return new Promise((res, rej) => {
|
||||
let resolved = false;
|
||||
|
||||
promise.then(result => {
|
||||
resolved = true;
|
||||
|
||||
res(result);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
rej(new Error('Process exceeded time limit ' + limitMs));
|
||||
}
|
||||
}, limitMs);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
randomSecureToken,
|
||||
randomString,
|
||||
@@ -237,5 +263,6 @@ module.exports = {
|
||||
isStringNote,
|
||||
quoteRegex,
|
||||
replaceAll,
|
||||
formatDownloadTitle
|
||||
formatDownloadTitle,
|
||||
timeLimit
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user