Compare commits

...

32 Commits

Author SHA1 Message Date
zadam
515c5411a6 release 0.61.5-beta 2023-08-16 23:02:15 +02:00
zadam
3f7a5504c7 prettier config 2023-08-16 20:31:14 +02:00
zadam
8c7c37cf98 updates 2023-08-16 20:12:22 +02:00
zadam
c0f48c0e99 mermaid 10.3.1 2023-08-15 10:30:30 +02:00
zadam
abedf2bba4 fix pasting into ckeditor, upgrade to v39.0.1 2023-08-15 10:25:23 +02:00
zadam
bb0137b2fd fix displaying options / book on firefox, closes #4165 2023-08-15 10:25:23 +02:00
zadam
6c54c7d17d save enex images as attachment, fixes #4163 2023-08-15 10:25:23 +02:00
zadam
90255ac55b release 0.61.4-beta 2023-08-10 23:49:37 +02:00
zadam
e741c2826c release 0.61.4-beta 2023-08-10 23:48:27 +02:00
zadam
026992db78 release 0.61.4-beta 2023-08-10 23:46:42 +02:00
zadam
33780c1e17 fix note tooltip code rendering 2023-08-10 13:50:25 +02:00
zadam
ede9c43f67 API docs 2023-08-10 13:40:26 +02:00
zadam
5c12ac4eee fix CKEditor crashing 2023-08-10 13:40:15 +02:00
zadam
522518cf0d disambiguous query 2023-08-09 23:12:31 +02:00
zadam
1d869d25c2 fix #clipperInbox, closes #4153 2023-08-09 23:00:42 +02:00
zadam
a9cdd93cb4 added options to disable auto-opening of promoted attributes and edited notes ribbon tabs, closes #4151 2023-08-09 22:50:41 +02:00
zadam
4240da349d add shared info to mobile layout, closes #4147 2023-08-09 00:02:45 +02:00
zadam
c257bc07a8 clipper now creates notes with image attachments instead of image notes 2023-08-09 00:01:31 +02:00
zadam
00eaa16985 clipper now creates notes with image attachments instead of image notes 2023-08-08 23:42:47 +02:00
zadam
fefb059564 clipper now creates notes with image attachments instead of image notes 2023-08-08 23:07:59 +02:00
zadam
9166765ced fix include note sizing when in readonly mode, closes #4135 2023-08-08 22:56:45 +02:00
zadam
6ae7661603 note path validation 2023-08-02 23:23:31 +02:00
zadam
30e75056bd release 0.61.3-beta 2023-07-31 23:03:45 +02:00
zadam
530e56dcb5 fixed comment 2023-07-31 22:59:47 +02:00
zadam
63675bfbae make migration 223 NOOP 2023-07-30 21:54:01 +02:00
zadam
696ce38083 ckeditor 38.1.1 2023-07-30 00:32:16 +02:00
zadam
12014b9f4d sync fixes and refactorings 2023-07-29 23:35:08 +02:00
zadam
e8b52f9e6c sync fixes and refactorings 2023-07-29 23:25:02 +02:00
zadam
04b125afc0 sync fixes and refactorings 2023-07-29 21:59:20 +02:00
zadam
2a7fe85020 VACUUM database after migration 2023-07-28 16:22:10 +02:00
zadam
119050e355 fix migration 2023-07-28 16:13:31 +02:00
zadam
72122d0f95 fix blobIds - they shouldn't contain + and / characters 2023-07-28 15:55:26 +02:00
60 changed files with 2087 additions and 1829 deletions

View File

@@ -1,11 +1,13 @@
//https://prettier.io/docs/en/options.html //https://prettier.io/docs/en/options.html
module.exports = { module.exports = {
semi: true, semi: true,
trailingComma: 'es5', trailingComma: 'none',
singleQuote: true, singleQuote: true,
printWidth: 120, printWidth: 100,
tabWidth: 4, tabWidth: 4,
// useTabs: false, useTabs: false,
// bracketSpacing: true, quoteProps: "as-needed",
bracketSpacing: true,
arrowParens: "avoid"
// htmlWhitespaceSensitivity: 'ignore', // htmlWhitespaceSensitivity: 'ignore',
}; };

View File

@@ -0,0 +1 @@
SELECT 1;

View File

@@ -1,3 +0,0 @@
CREATE INDEX IDX_notes_blobId on notes (blobId);
CREATE INDEX IDX_revisions_blobId on revisions (blobId);
CREATE INDEX IDX_attachments_blobId on attachments (blobId);

View File

@@ -0,0 +1,14 @@
UPDATE blobs SET blobId = REPLACE(blobId, '+', 'X');
UPDATE blobs SET blobId = REPLACE(blobId, '/', 'Y');
UPDATE notes SET blobId = REPLACE(blobId, '+', 'X');
UPDATE notes SET blobId = REPLACE(blobId, '/', 'Y');
UPDATE attachments SET blobId = REPLACE(blobId, '+', 'X');
UPDATE attachments SET blobId = REPLACE(blobId, '/', 'Y');
UPDATE revisions SET blobId = REPLACE(blobId, '+', 'X');
UPDATE revisions SET blobId = REPLACE(blobId, '/', 'Y');
UPDATE entity_changes SET entityId = REPLACE(entityId, '+', 'X') WHERE entityName = 'blobs';
UPDATE entity_changes SET entityId = REPLACE(entityId, '/', 'Y') WHERE entityName = 'blobs';

View File

@@ -0,0 +1,3 @@
CREATE INDEX IF NOT EXISTS IDX_notes_blobId on notes (blobId);
CREATE INDEX IF NOT EXISTS IDX_revisions_blobId on revisions (blobId);
CREATE INDEX IF NOT EXISTS IDX_attachments_blobId on attachments (blobId);

View File

@@ -259,7 +259,7 @@
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line244">line 244</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line246">line 246</a>
</li></ul></dd> </li></ul></dd>
@@ -394,90 +394,6 @@
<h4 class="name" id="addEntityChange"><span class="type-signature">(protected) </span>addEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd>
</dl>
@@ -1022,7 +938,91 @@ This is a low-level method, for notes and branches use `note.deleteNote()` and '
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line261">line 261</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line263">line 263</a>
</li></ul></dd>
</dl>
<h4 class="name" id="putEntityChange"><span class="type-signature">(protected) </span>putEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd> </li></ul></dd>

View File

@@ -825,7 +825,7 @@ and relation (representing named relationship between source and target note)</d
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line244">line 244</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line246">line 246</a>
</li></ul></dd> </li></ul></dd>
@@ -965,95 +965,6 @@ and relation (representing named relationship between source and target note)</d
<h4 class="name" id="addEntityChange"><span class="type-signature">(protected) </span>addEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#addEntityChange">AbstractBeccaEntity#addEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd>
</dl>
@@ -1940,7 +1851,96 @@ This is a low-level method, for notes and branches use `note.deleteNote()` and '
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line261">line 261</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line263">line 263</a>
</li></ul></dd>
</dl>
<h4 class="name" id="putEntityChange"><span class="type-signature">(protected) </span>putEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#putEntityChange">AbstractBeccaEntity#putEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd> </li></ul></dd>

View File

@@ -945,7 +945,7 @@ of deletion should not act as a clone.
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line244">line 244</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line246">line 246</a>
</li></ul></dd> </li></ul></dd>
@@ -1085,95 +1085,6 @@ of deletion should not act as a clone.
<h4 class="name" id="addEntityChange"><span class="type-signature">(protected) </span>addEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#addEntityChange">AbstractBeccaEntity#addEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd>
</dl>
@@ -2054,7 +1965,96 @@ This is a low-level method, for notes and branches use `note.deleteNote()` and '
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line261">line 261</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line263">line 263</a>
</li></ul></dd>
</dl>
<h4 class="name" id="putEntityChange"><span class="type-signature">(protected) </span>putEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#putEntityChange">AbstractBeccaEntity#putEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd> </li></ul></dd>

View File

@@ -694,7 +694,7 @@ from tokenHash and token.</div>
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line244">line 244</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line246">line 246</a>
</li></ul></dd> </li></ul></dd>
@@ -834,95 +834,6 @@ from tokenHash and token.</div>
<h4 class="name" id="addEntityChange"><span class="type-signature">(protected) </span>addEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#addEntityChange">AbstractBeccaEntity#addEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd>
</dl>
@@ -1497,7 +1408,96 @@ This is a low-level method, for notes and branches use `note.deleteNote()` and '
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line261">line 261</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line263">line 263</a>
</li></ul></dd>
</dl>
<h4 class="name" id="putEntityChange"><span class="type-signature">(protected) </span>putEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#putEntityChange">AbstractBeccaEntity#putEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd> </li></ul></dd>

View File

@@ -1171,7 +1171,7 @@
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line244">line 244</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line246">line 246</a>
</li></ul></dd> </li></ul></dd>
@@ -1636,95 +1636,6 @@ See addLabel, addRelation for more specific methods.
<h4 class="name" id="addEntityChange"><span class="type-signature">(protected) </span>addEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#addEntityChange">AbstractBeccaEntity#addEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd>
</dl>
@@ -12325,7 +12236,96 @@ This is a low-level method, for notes and branches use `note.deleteNote()` and '
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line261">line 261</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line263">line 263</a>
</li></ul></dd>
</dl>
<h4 class="name" id="putEntityChange"><span class="type-signature">(protected) </span>putEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#putEntityChange">AbstractBeccaEntity#putEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd> </li></ul></dd>

View File

@@ -552,7 +552,7 @@
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line244">line 244</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line246">line 246</a>
</li></ul></dd> </li></ul></dd>
@@ -692,95 +692,6 @@
<h4 class="name" id="addEntityChange"><span class="type-signature">(protected) </span>addEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#addEntityChange">AbstractBeccaEntity#addEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd>
</dl>
@@ -1355,7 +1266,96 @@ This is a low-level method, for notes and branches use `note.deleteNote()` and '
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line261">line 261</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line263">line 263</a>
</li></ul></dd>
</dl>
<h4 class="name" id="putEntityChange"><span class="type-signature">(protected) </span>putEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#putEntityChange">AbstractBeccaEntity#putEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd> </li></ul></dd>

View File

@@ -484,7 +484,7 @@
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line244">line 244</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line246">line 246</a>
</li></ul></dd> </li></ul></dd>
@@ -624,95 +624,6 @@
<h4 class="name" id="addEntityChange"><span class="type-signature">(protected) </span>addEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#addEntityChange">AbstractBeccaEntity#addEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd>
</dl>
@@ -1287,7 +1198,96 @@ This is a low-level method, for notes and branches use `note.deleteNote()` and '
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line261">line 261</a> <a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line263">line 263</a>
</li></ul></dd>
</dl>
<h4 class="name" id="putEntityChange"><span class="type-signature">(protected) </span>putEntityChange<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-overrides">Overrides:</dt>
<dd class="tag-overrides"><ul class="dummy"><li>
<a href="AbstractBeccaEntity.html#putEntityChange">AbstractBeccaEntity#putEntityChange</a>
</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="becca_entities_abstract_becca_entity.js.html">becca/entities/abstract_becca_entity.js</a>, <a href="becca_entities_abstract_becca_entity.js.html#line64">line 64</a>
</li></ul></dd> </li></ul></dd>

View File

@@ -89,8 +89,8 @@ class AbstractBeccaEntity {
} }
/** @protected */ /** @protected */
addEntityChange(isDeleted = false) { putEntityChange(isDeleted = false) {
entityChangesService.addEntityChange({ entityChangesService.putEntityChange({
entityName: this.constructor.entityName, entityName: this.constructor.entityName,
entityId: this[this.constructor.primaryKeyName], entityId: this[this.constructor.primaryKeyName],
hash: this.generateHash(isDeleted), hash: this.generateHash(isDeleted),
@@ -129,7 +129,7 @@ class AbstractBeccaEntity {
return; return;
} }
this.addEntityChange(false); this.putEntityChange(false);
if (!cls.isEntityEventsDisabled()) { if (!cls.isEntityEventsDisabled()) {
const eventPayload = { const eventPayload = {
@@ -203,6 +203,8 @@ class AbstractBeccaEntity {
} }
sql.execute("DELETE FROM blobs WHERE blobId = ?", [oldBlobId]); sql.execute("DELETE FROM blobs WHERE blobId = ?", [oldBlobId]);
// blobs are not marked as erased in entity_changes, they are just purged completely
// this is because technically every keystroke can create a new blob and there would be just too many
sql.execute("DELETE FROM entity_changes WHERE entityName = 'blobs' AND entityId = ?", [oldBlobId]); sql.execute("DELETE FROM entity_changes WHERE entityName = 'blobs' AND entityId = ?", [oldBlobId]);
} }
@@ -245,7 +247,7 @@ class AbstractBeccaEntity {
// access to the decrypted content // access to the decrypted content
const hash = blobService.calculateContentHash(pojo); const hash = blobService.calculateContentHash(pojo);
entityChangesService.addEntityChange({ entityChangesService.putEntityChange({
entityName: 'blobs', entityName: 'blobs',
entityId: newBlobId, entityId: newBlobId,
hash: hash, hash: hash,
@@ -305,7 +307,7 @@ class AbstractBeccaEntity {
log.info(`Marking ${entityName} ${entityId} as deleted`); log.info(`Marking ${entityName} ${entityId} as deleted`);
this.addEntityChange(true); this.putEntityChange(true);
eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this }); eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this });
} }
@@ -322,7 +324,7 @@ class AbstractBeccaEntity {
log.info(`Marking ${entityName} ${entityId} as deleted`); log.info(`Marking ${entityName} ${entityId} as deleted`);
this.addEntityChange(true); this.putEntityChange(true);
eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this }); eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this });
} }

View File

@@ -5,8 +5,8 @@
} }
/* /*
* CKEditor 5 (v38.0.1) content styles. * CKEditor 5 (v39.0.1) content styles.
* Generated on Thu, 25 May 2023 13:25:51 GMT. * Generated on Thu, 10 Aug 2023 09:21:07 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html
*/ */
@@ -15,8 +15,8 @@
--ck-color-image-caption-text: hsl(0, 0%, 20%); --ck-color-image-caption-text: hsl(0, 0%, 20%);
--ck-color-mention-background: hsla(341, 100%, 30%, 0.1); --ck-color-mention-background: hsla(341, 100%, 30%, 0.1);
--ck-color-mention-text: hsl(341, 100%, 30%); --ck-color-mention-text: hsl(341, 100%, 30%);
--ck-color-table-caption-background: hsl(0, 0%, 97%); --ck-color-selector-caption-background: hsl(0, 0%, 97%);
--ck-color-table-caption-text: hsl(0, 0%, 20%); --ck-color-selector-caption-text: hsl(0, 0%, 20%);
--ck-highlight-marker-blue: hsl(201, 97%, 72%); --ck-highlight-marker-blue: hsl(201, 97%, 72%);
--ck-highlight-marker-green: hsl(120, 93%, 68%); --ck-highlight-marker-green: hsl(120, 93%, 68%);
--ck-highlight-marker-pink: hsl(345, 96%, 73%); --ck-highlight-marker-pink: hsl(345, 96%, 73%);
@@ -28,227 +28,107 @@
--ck-todo-list-checkmark-size: 16px; --ck-todo-list-checkmark-size: 16px;
} }
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */ /* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content blockquote { .ck-content .table .ck-table-resized {
table-layout: fixed;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table table {
overflow: hidden; 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%);
} }
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */ /* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content[dir="rtl"] blockquote { .ck-content .table td,
border-left: 0; .ck-content .table th {
border-right: solid 5px hsl(0, 0%, 80%); overflow-wrap: break-word;
position: relative;
} }
/* @ckeditor/ckeditor5-basic-styles/theme/code.css */ /* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content code { .ck-content .table {
background-color: hsla(0, 0%, 78%, 0.3); margin: 0.9em auto;
padding: .15em; display: table;
border-radius: 2px;
} }
/* @ckeditor/ckeditor5-font/theme/fontsize.css */ /* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .text-tiny { .ck-content .table table {
font-size: .7em; border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
} }
/* @ckeditor/ckeditor5-font/theme/fontsize.css */ /* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .text-small { .ck-content .table table td,
font-size: .85em; .ck-content .table table th {
min-width: 2em;
padding: .4em;
border: 1px solid hsl(0, 0%, 75%);
} }
/* @ckeditor/ckeditor5-font/theme/fontsize.css */ /* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .text-big { .ck-content .table table th {
font-size: 1.4em; font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
} }
/* @ckeditor/ckeditor5-font/theme/fontsize.css */ /* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .text-huge { .ck-content[dir="rtl"] .table th {
font-size: 1.8em; text-align: right;
} }
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ /* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .marker-yellow { .ck-content[dir="ltr"] .table th {
background-color: var(--ck-highlight-marker-yellow); text-align: left;
} }
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ /* @ckeditor/ckeditor5-table/theme/tablecaption.css */
.ck-content .marker-green { .ck-content .table > figcaption {
background-color: var(--ck-highlight-marker-green);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* @ckeditor/ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption; display: table-caption;
caption-side: bottom; caption-side: top;
word-break: break-word; word-break: break-word;
color: var(--ck-color-image-caption-text); text-align: center;
background-color: var(--ck-color-image-caption-background); color: var(--ck-color-selector-caption-text);
background-color: var(--ck-color-selector-caption-background);
padding: .6em; padding: .6em;
font-size: .75em; font-size: .75em;
outline-offset: -1px; outline-offset: -1px;
} }
/* @ckeditor/ckeditor5-image/theme/image.css */ /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .image { .ck-content .page-break {
display: table; position: relative;
clear: both; clear: both;
text-align: center; padding: 5px 0;
margin: 0.9em auto;
min-width: 50px;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline {
/*
* Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).;
* Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root.
* This strange behavior does not happen with inline-flex.
*/
display: inline-flex;
max-width: 100%;
align-items: flex-start;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline picture {
display: flex; display: flex;
align-items: center;
justify-content: center;
} }
/* @ckeditor/ckeditor5-image/theme/image.css */ /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .image-inline picture, .ck-content .page-break::after {
.ck-content .image-inline img { content: '';
flex-grow: 1; position: absolute;
flex-shrink: 1; border-bottom: 2px dashed hsl(0, 0%, 77%);
max-width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized img {
width: 100%; width: 100%;
} }
/* @ckeditor/ckeditor5-image/theme/imageresize.css */ /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .image.image_resized > figcaption { .ck-content .page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block; 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;
} }
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ /* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .image-style-block-align-left, .ck-content .media {
.ck-content .image-style-block-align-right { clear: both;
max-width: calc(100% - var(--ck-image-style-spacing)); margin: 0.9em 0;
} display: block;
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ min-width: 15em;
.ck-content .image-style-align-left,
.ck-content .image-style-align-right {
clear: none;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
max-width: 50%;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left {
float: left;
margin-right: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-center {
margin-left: auto;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-right {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-right {
margin-right: 0;
margin-left: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left {
margin-left: 0;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content p + .image-style-align-left,
.ck-content p + .image-style-align-right,
.ck-content p + .image-style-side {
margin-top: 0;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left,
.ck-content .image-inline.image-style-align-right {
margin-top: var(--ck-inline-image-style-spacing);
margin-bottom: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left {
margin-right: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-right {
margin-left: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol {
list-style-type: decimal;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol {
list-style-type: lower-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol {
list-style-type: lower-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol {
list-style-type: upper-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol ol {
list-style-type: upper-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul {
list-style-type: disc;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul {
list-style-type: circle;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul ul {
list-style-type: square;
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list { .ck-content .todo-list {
@@ -317,107 +197,240 @@
.ck-content .todo-list .todo-list__label .todo-list__label__description { .ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle; vertical-align: middle;
} }
/* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */ /* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .media { .ck-content .image {
clear: both;
margin: 0.9em 0;
display: block;
min-width: 15em;
}
/* @ckeditor/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;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* @ckeditor/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;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table .ck-table-resized {
table-layout: fixed;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table table {
overflow: hidden;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table td,
.ck-content .table th {
position: relative;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 0.9em auto;
display: table; display: table;
} clear: both;
/* @ckeditor/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%);
}
/* @ckeditor/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%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content[dir="rtl"] .table th {
text-align: right;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content[dir="ltr"] .table th {
text-align: left;
}
/* @ckeditor/ckeditor5-table/theme/tablecaption.css */
.ck-content .table > figcaption {
display: table-caption;
caption-side: top;
word-break: break-word;
text-align: center; text-align: center;
color: var(--ck-color-table-caption-text); margin: 0.9em auto;
background-color: var(--ck-color-table-caption-background); min-width: 50px;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline {
/*
* Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).;
* Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root.
* This strange behavior does not happen with inline-flex.
*/
display: inline-flex;
max-width: 100%;
align-items: flex-start;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline picture {
display: flex;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline picture,
.ck-content .image-inline img {
flex-grow: 1;
flex-shrink: 1;
max-width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized img {
width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized > figcaption {
display: block;
}
/* @ckeditor/ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: var(--ck-color-image-caption-text);
background-color: var(--ck-color-image-caption-background);
padding: .6em; padding: .6em;
font-size: .75em; font-size: .75em;
outline-offset: -1px; outline-offset: -1px;
} }
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol {
list-style-type: decimal;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol {
list-style-type: lower-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol {
list-style-type: lower-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol {
list-style-type: upper-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol ol {
list-style-type: upper-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul {
list-style-type: disc;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul {
list-style-type: circle;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left,
.ck-content .image-style-block-align-right {
max-width: calc(100% - var(--ck-image-style-spacing));
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left,
.ck-content .image-style-align-right {
clear: none;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
max-width: 50%;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left {
float: left;
margin-right: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-center {
margin-left: auto;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-right {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-right {
margin-right: 0;
margin-left: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left {
margin-left: 0;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content p + .image-style-align-left,
.ck-content p + .image-style-align-right,
.ck-content p + .image-style-side {
margin-top: 0;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left,
.ck-content .image-inline.image-style-align-right {
margin-top: var(--ck-inline-image-style-spacing);
margin-bottom: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left {
margin-right: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-right {
margin-left: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/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%);
}
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* @ckeditor/ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny {
font-size: .7em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-small {
font-size: .85em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-big {
font-size: 1.4em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-huge {
font-size: 1.8em;
}
/* @ckeditor/ckeditor5-mention/theme/mention.css */
.ck-content .mention {
background: var(--ck-color-mention-background);
color: var(--ck-color-mention-text);
}
/* @ckeditor/ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */ /* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
.ck-content pre { .ck-content pre {
padding: 1em; padding: 1em;
@@ -438,18 +451,6 @@
padding: 0; padding: 0;
border-radius: 0; border-radius: 0;
} }
/* @ckeditor/ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* @ckeditor/ckeditor5-mention/theme/mention.css */
.ck-content .mention {
background: var(--ck-color-mention-background);
color: var(--ck-color-mention-text);
}
@media print { @media print {
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break { .ck-content .page-break {

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

1032
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.61.2-beta", "version": "0.61.5-beta",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@@ -31,9 +31,9 @@
"prepare": "husky install || echo 'Husky install failed, expected on flatpak build'" "prepare": "husky install || echo 'Husky install failed, expected on flatpak build'"
}, },
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "6.0.2", "@braintree/sanitize-url": "6.0.4",
"@electron/remote": "2.0.10", "@electron/remote": "2.0.10",
"@excalidraw/excalidraw": "0.15.2", "@excalidraw/excalidraw": "0.15.3",
"archiver": "5.3.1", "archiver": "5.3.1",
"async-mutex": "0.4.0", "async-mutex": "0.4.0",
"axios": "1.4.0", "axios": "1.4.0",
@@ -53,7 +53,7 @@
"escape-html": "1.0.3", "escape-html": "1.0.3",
"express": "4.18.2", "express": "4.18.2",
"express-partial-content": "1.0.2", "express-partial-content": "1.0.2",
"express-rate-limit": "6.8.1", "express-rate-limit": "6.9.0",
"express-session": "1.17.3", "express-session": "1.17.3",
"fs-extra": "11.1.1", "fs-extra": "11.1.1",
"helmet": "7.0.0", "helmet": "7.0.0",
@@ -68,10 +68,10 @@
"jimp": "0.22.10", "jimp": "0.22.10",
"joplin-turndown-plugin-gfm": "1.0.12", "joplin-turndown-plugin-gfm": "1.0.12",
"jsdom": "22.1.0", "jsdom": "22.1.0",
"marked": "5.1.2", "marked": "7.0.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"node-abi": "3.45.0", "node-abi": "3.46.0",
"normalize-strings": "1.1.1", "normalize-strings": "1.1.1",
"open": "8.4.1", "open": "8.4.1",
"rand-token": "1.0.1", "rand-token": "1.0.1",
@@ -97,14 +97,14 @@
}, },
"devDependencies": { "devDependencies": {
"cross-env": "7.0.3", "cross-env": "7.0.3",
"electron": "25.3.2", "electron": "25.5.0",
"electron-builder": "24.6.3", "electron-builder": "24.6.3",
"electron-packager": "17.1.1", "electron-packager": "17.1.1",
"electron-rebuild": "3.2.9", "electron-rebuild": "3.2.9",
"eslint": "8.45.0", "eslint": "8.47.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "8.9.0", "eslint-config-prettier": "9.0.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.28.0",
"eslint-plugin-jsonc": "2.9.0", "eslint-plugin-jsonc": "2.9.0",
"eslint-plugin-prettier": "5.0.0", "eslint-plugin-prettier": "5.0.0",
"esm": "3.2.25", "esm": "3.2.25",
@@ -112,11 +112,11 @@
"jasmine": "5.1.0", "jasmine": "5.1.0",
"jsdoc": "4.0.2", "jsdoc": "4.0.2",
"jsonc-eslint-parser": "2.3.0", "jsonc-eslint-parser": "2.3.0",
"lint-staged": "13.2.3", "lint-staged": "14.0.0",
"lorem-ipsum": "2.0.8", "lorem-ipsum": "2.0.8",
"nodemon": "3.0.1", "nodemon": "3.0.1",
"prettier": "3.0.0", "prettier": "3.0.2",
"rcedit": "3.0.1", "rcedit": "3.1.0",
"webpack": "5.88.2", "webpack": "5.88.2",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
}, },

View File

@@ -61,8 +61,8 @@ class AbstractBeccaEntity {
} }
/** @protected */ /** @protected */
addEntityChange(isDeleted = false) { putEntityChange(isDeleted = false) {
entityChangesService.addEntityChange({ entityChangesService.putEntityChange({
entityName: this.constructor.entityName, entityName: this.constructor.entityName,
entityId: this[this.constructor.primaryKeyName], entityId: this[this.constructor.primaryKeyName],
hash: this.generateHash(isDeleted), hash: this.generateHash(isDeleted),
@@ -101,7 +101,7 @@ class AbstractBeccaEntity {
return; return;
} }
this.addEntityChange(false); this.putEntityChange(false);
if (!cls.isEntityEventsDisabled()) { if (!cls.isEntityEventsDisabled()) {
const eventPayload = { const eventPayload = {
@@ -219,7 +219,7 @@ class AbstractBeccaEntity {
// access to the decrypted content // access to the decrypted content
const hash = blobService.calculateContentHash(pojo); const hash = blobService.calculateContentHash(pojo);
entityChangesService.addEntityChange({ entityChangesService.putEntityChange({
entityName: 'blobs', entityName: 'blobs',
entityId: newBlobId, entityId: newBlobId,
hash: hash, hash: hash,
@@ -279,7 +279,7 @@ class AbstractBeccaEntity {
log.info(`Marking ${entityName} ${entityId} as deleted`); log.info(`Marking ${entityName} ${entityId} as deleted`);
this.addEntityChange(true); this.putEntityChange(true);
eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this }); eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this });
} }
@@ -296,7 +296,7 @@ class AbstractBeccaEntity {
log.info(`Marking ${entityName} ${entityId} as deleted`); log.info(`Marking ${entityName} ${entityId} as deleted`);
this.addEntityChange(true); this.putEntityChange(true);
eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this }); eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this });
} }

View File

@@ -75,7 +75,7 @@ function register(router) {
eu.route(router, 'post' ,'/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => { eu.route(router, 'post' ,'/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => {
eu.getAndCheckNote(req.params.parentNoteId); eu.getAndCheckNote(req.params.parentNoteId);
entityChangesService.addNoteReorderingEntityChange(req.params.parentNoteId, "etapi"); entityChangesService.putNoteReorderingEntityChange(req.params.parentNoteId, "etapi");
res.sendStatus(204); res.sendStatus(204);
}); });

View File

@@ -21,6 +21,7 @@ import NoteListWidget from "../widgets/note_list.js";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js"; import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import LauncherContainer from "../widgets/containers/launcher_container.js"; import LauncherContainer from "../widgets/containers/launcher_container.js";
import RootContainer from "../widgets/containers/root_container.js"; import RootContainer from "../widgets/containers/root_container.js";
import SharedInfoWidget from "../widgets/shared_info.js";
const MOBILE_CSS = ` const MOBILE_CSS = `
<style> <style>
@@ -144,6 +145,7 @@ export default class MobileLayout {
.css("top: 5px;") .css("top: 5px;")
) )
.child(new CloseDetailButtonWidget().contentSized())) .child(new CloseDetailButtonWidget().contentSized()))
.child(new SharedInfoWidget())
.child(new FloatingButtons() .child(new FloatingButtons()
.child(new EditButton()) .child(new EditButton())
.child(new RelationMapButtons()) .child(new RelationMapButtons())

View File

@@ -67,7 +67,6 @@ export default class TreeContextMenu {
{ title: "Advanced", uiIcon: "bx bx-empty", enabled: true, items: [ { title: "Advanced", uiIcon: "bx bx-empty", enabled: true, items: [
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes }, { title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes }, { title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "bx bx-refresh", enabled: noSelectedNotes },
{ title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch }, { title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes }, { title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes },
{ title: 'Convert to attachment', command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted } { title: 'Convert to attachment', command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted }

View File

@@ -31,7 +31,7 @@ async function getRenderedContent(entity, options = {}) {
await renderText(entity, $renderedContent); await renderText(entity, $renderedContent);
} }
else if (type === 'code') { else if (type === 'code') {
await renderCode(entity, options, $renderedContent); await renderCode(entity, $renderedContent);
} }
else if (type === 'image') { else if (type === 'image') {
renderImage(entity, $renderedContent, options); renderImage(entity, $renderedContent, options);

View File

@@ -148,6 +148,11 @@ function parseNavigationStateFromUrl(url) {
const hash = url.substr(hashIdx + 1); // strip also the initial '#' const hash = url.substr(hashIdx + 1); // strip also the initial '#'
const [notePath, paramString] = hash.split("?"); const [notePath, paramString] = hash.split("?");
if (!notePath.match(/^[_a-z0-9]{4,}(\/[_a-z0-9]{4,})*$/i)) {
return {};
}
const viewScope = { const viewScope = {
viewMode: 'default' viewMode: 'default'
}; };

View File

@@ -259,8 +259,6 @@ function init() {
}; };
$.fn.setSelectedExternalLink = function (externalLink) { $.fn.setSelectedExternalLink = function (externalLink) {
console.trace("setSelectedExternalLink");
if (externalLink) { if (externalLink) {
$(this) $(this)
.closest(".input-group") .closest(".input-group")

View File

@@ -18,13 +18,6 @@ async function syncNow(ignoreNotConfigured = false) {
} }
} }
async function forceNoteSync(noteId) {
await server.post(`sync/force-note-sync/${noteId}`);
toastService.showMessage("Note added to sync queue.");
}
export default { export default {
syncNow, syncNow
forceNoteSync
}; };

View File

@@ -37,7 +37,9 @@ export default class NoteListWidget extends NoteContextAwareWidget {
threshold: 0.1 threshold: 0.1
}); });
observer.observe(this.$widget[0]); // there seems to be a race condition on Firefox which triggers the observer only before the widget is visible
// (intersection is false). https://github.com/zadam/trilium/issues/4165
setTimeout(() => observer.observe(this.$widget[0]), 10);
} }
checkRenderStatus() { checkRenderStatus() {

View File

@@ -1564,10 +1564,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.triggerCommand("showImportDialog", {noteId: node.data.noteId}); this.triggerCommand("showImportDialog", {noteId: node.data.noteId});
} }
forceNoteSyncCommand({node}) {
syncService.forceNoteSync(node.data.noteId);
}
editNoteTitleCommand({node}) { editNoteTitleCommand({node}) {
appContext.triggerCommand('focusOnTitle'); appContext.triggerCommand('focusOnTitle');
} }

View File

@@ -2,6 +2,7 @@ import linkService from "../../services/link.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js"; import NoteContextAwareWidget from "../note_context_aware_widget.js";
import options from "../../services/options.js";
const TPL = ` const TPL = `
<div class="edited-notes-widget"> <div class="edited-notes-widget">
@@ -34,7 +35,9 @@ export default class EditedNotesWidget extends NoteContextAwareWidget {
return { return {
show: this.isEnabled(), show: this.isEnabled(),
// promoted attributes have priority over edited notes // promoted attributes have priority over edited notes
activate: this.note.getPromotedDefinitionAttributes().length === 0, activate:
(this.note.getPromotedDefinitionAttributes().length === 0 || !options.is('promotedAttributesOpenInRibbon'))
&& options.is('editedNotesOpenInRibbon'),
title: 'Edited Notes', title: 'Edited Notes',
icon: 'bx bx-calendar-edit' icon: 'bx bx-calendar-edit'
}; };

View File

@@ -4,6 +4,7 @@ import treeService from "../../services/tree.js";
import noteAutocompleteService from "../../services/note_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js"; import NoteContextAwareWidget from "../note_context_aware_widget.js";
import attributeService from "../../services/attributes.js"; import attributeService from "../../services/attributes.js";
import options from "../../services/options.js";
const TPL = ` const TPL = `
<div> <div>
@@ -62,7 +63,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
return { return {
show: true, show: true,
activate: true, activate: options.is('promotedAttributesOpenInRibbon'),
title: "Promoted Attributes", title: "Promoted Attributes",
icon: "bx bx-table" icon: "bx bx-table"
}; };

View File

@@ -31,6 +31,7 @@ import VacuumDatabaseOptions from "./options/advanced/vacuum_database.js";
import DatabaseAnonymizationOptions from "./options/advanced/database_anonymization.js"; import DatabaseAnonymizationOptions from "./options/advanced/database_anonymization.js";
import BackendLogWidget from "./content/backend_log.js"; import BackendLogWidget from "./content/backend_log.js";
import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js"; import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js";
import RibbonOptions from "./options/appearance/ribbon.js";
const TPL = `<div class="note-detail-content-widget note-detail-printable"> const TPL = `<div class="note-detail-content-widget note-detail-printable">
<style> <style>
@@ -57,7 +58,8 @@ const CONTENT_WIDGETS = {
FontsOptions, FontsOptions,
ZoomFactorOptions, ZoomFactorOptions,
NativeTitleBarOptions, NativeTitleBarOptions,
MaxContentWidthOptions MaxContentWidthOptions,
RibbonOptions
], ],
_optionsShortcuts: [ KeyboardShortcutsOptions ], _optionsShortcuts: [ KeyboardShortcutsOptions ],
_optionsTextNotes: [ _optionsTextNotes: [

View File

@@ -0,0 +1,34 @@
import OptionsWidget from "../options_widget.js";
const TPL = `
<div class="options-section">
<h4>Ribbon widgets</h4>
<label>
<input type="checkbox" class="promoted-attributes-open-in-ribbon">
Promoted Attributes ribbon tab will automatically open if promoted attributes are present on the note
</label>
<label>
<input type="checkbox" class="edited-notes-open-in-ribbon">
Edited Notes ribbon tab will automatically open on day notes
</label>
</div>`;
export default class RibbonOptions extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
this.$promotedAttributesOpenInRibbon = this.$widget.find(".promoted-attributes-open-in-ribbon");
this.$promotedAttributesOpenInRibbon.on('change', () =>
this.updateCheckboxOption('promotedAttributesOpenInRibbon', this.$promotedAttributesOpenInRibbon));
this.$editedNotesOpenInRibbon = this.$widget.find(".edited-notes-open-in-ribbon");
this.$editedNotesOpenInRibbon.on('change', () =>
this.updateCheckboxOption('editedNotesOpenInRibbon', this.$editedNotesOpenInRibbon));
}
async optionsLoaded(options) {
this.setCheckboxState(this.$promotedAttributesOpenInRibbon, options.promotedAttributesOpenInRibbon);
this.setCheckboxState(this.$editedNotesOpenInRibbon, options.editedNotesOpenInRibbon);
}
}

View File

@@ -722,25 +722,26 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
content: '' !important; content: '' !important;
} }
.include-note.box-size-small .include-note-content { /* Using data- attribute to support both CKEditor and readonly view */
.include-note[data-box-size=small] .include-note-content {
max-height: 10em; max-height: 10em;
overflow: auto; overflow: auto;
} }
.include-note.box-size-small .include-note-content.type-pdf { .include-note[data-box-size=small] .include-note-content.type-pdf {
height: 10em; /* PDF is rendered in iframe and must be sized absolutely */ height: 10em; /* PDF is rendered in iframe and must be sized absolutely */
} }
.include-note.box-size-medium .include-note-content { .include-note[data-box-size=medium] .include-note-content {
max-height: 20em; max-height: 20em;
overflow: auto; overflow: auto;
} }
.include-note.box-size-medium .include-note-content.type-pdf .rendered-content { .include-note[data-box-size=medium] .include-note-content.type-pdf .rendered-content {
height: 20em; /* PDF is rendered in iframe and must be sized absolutely */ height: 20em; /* PDF is rendered in iframe and must be sized absolutely */
} }
.include-note.box-size-full .include-note-content.type-pdf .rendered-content { .include-note[data-box-size=full] .include-note-content.type-pdf .rendered-content {
height: 50em; /* PDF is rendered in iframe and it's not possible to put full height so at least a large height */ height: 50em; /* PDF is rendered in iframe and it's not possible to put full height so at least a large height */
} }

View File

@@ -72,7 +72,7 @@ function moveBranchBeforeNote(req) {
treeService.sortNotesIfNeeded(parentNote.noteId); treeService.sortNotesIfNeeded(parentNote.noteId);
// if sorting is not needed, then still the ordering might have changed above manually // if sorting is not needed, then still the ordering might have changed above manually
entityChangesService.addNoteReorderingEntityChange(parentNote.noteId); entityChangesService.putNoteReorderingEntityChange(parentNote.noteId);
log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} before note ${beforeBranch.noteId}, branch ${beforeBranchId}`); log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} before note ${beforeBranch.noteId}, branch ${beforeBranchId}`);
@@ -123,7 +123,7 @@ function moveBranchAfterNote(req) {
treeService.sortNotesIfNeeded(parentNote.noteId); treeService.sortNotesIfNeeded(parentNote.noteId);
// if sorting is not needed, then still the ordering might have changed above manually // if sorting is not needed, then still the ordering might have changed above manually
entityChangesService.addNoteReorderingEntityChange(parentNote.noteId); entityChangesService.putNoteReorderingEntityChange(parentNote.noteId);
log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} after note ${afterNote.noteId}, branch ${afterBranchId}`); log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} after note ${afterNote.noteId}, branch ${afterBranchId}`);

View File

@@ -11,28 +11,26 @@ const ws = require('../../services/ws');
const log = require('../../services/log'); const log = require('../../services/log');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const path = require('path'); const path = require('path');
const BAttribute = require('../../becca/entities/battribute');
const htmlSanitizer = require('../../services/html_sanitizer'); const htmlSanitizer = require('../../services/html_sanitizer');
const {formatAttrForSearch} = require("../../services/attribute_formatter"); const {formatAttrForSearch} = require("../../services/attribute_formatter");
const jsdom = require("jsdom"); const jsdom = require("jsdom");
const { JSDOM } = jsdom; const { JSDOM } = jsdom;
function addClipping(req) { function addClipping(req) {
// if a note under the clipperInbox as the same 'pageUrl' attribute, // if a note under the clipperInbox has the same 'pageUrl' attribute,
// add the content to that note and clone it under today's inbox // add the content to that note and clone it under today's inbox
// otherwise just create a new note under today's inbox // otherwise just create a new note under today's inbox
let {title, content, pageUrl, images} = req.body; let {title, content, pageUrl, images} = req.body;
const clipType = 'clippings'; const clipType = 'clippings';
const clipperInbox = getClipperInboxNote(); const clipperInbox = getClipperInboxNote();
const dailyNote = dateNoteService.getDayNote(dateUtils.localNowDate());
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl); pageUrl = htmlSanitizer.sanitizeUrl(pageUrl);
let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType); let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType);
if (!clippingNote) { if (!clippingNote) {
clippingNote = noteService.createNewNote({ clippingNote = noteService.createNewNote({
parentNoteId: dailyNote.noteId, parentNoteId: clipperInbox.noteId,
title: title, title: title,
content: '', content: '',
type: 'text' type: 'text'
@@ -49,8 +47,8 @@ function addClipping(req) {
clippingNote.setContent(`${existingContent}${existingContent.trim() ? "<br>" : ""}${rewrittenContent}`); clippingNote.setContent(`${existingContent}${existingContent.trim() ? "<br>" : ""}${rewrittenContent}`);
if (clippingNote.parentNoteId !== dailyNote.noteId) { if (clippingNote.parentNoteId !== clipperInbox.noteId) {
cloneService.cloneNoteToParentNote(clippingNote.noteId, dailyNote.noteId); cloneService.cloneNoteToParentNote(clippingNote.noteId, clipperInbox.noteId);
} }
return { return {
@@ -80,7 +78,7 @@ function getClipperInboxNote() {
let clipperInbox = attributeService.getNoteWithLabel('clipperInbox'); let clipperInbox = attributeService.getNoteWithLabel('clipperInbox');
if (!clipperInbox) { if (!clipperInbox) {
clipperInbox = dateNoteService.getRootCalendarNote(); clipperInbox = dateNoteService.getDayNote(dateUtils.localNowDate());
} }
return clipperInbox; return clipperInbox;
@@ -127,7 +125,10 @@ function createNote(req) {
const existingContent = note.getContent(); const existingContent = note.getContent();
const rewrittenContent = processContent(images, note, content); const rewrittenContent = processContent(images, note, content);
note.setContent(`${existingContent}${existingContent.trim() ? "<br/>" : ""}${rewrittenContent}`); const newContent = `${existingContent}${existingContent.trim() ? "<br/>" : ""}${rewrittenContent}`;
note.setContent(newContent);
noteService.asyncPostProcessContent(note, newContent); // to mark attachments as used
return { return {
noteId: note.noteId noteId: note.noteId
@@ -152,20 +153,9 @@ function processContent(images, note, content) {
const buffer = Buffer.from(dataUrl.split(",")[1], 'base64'); const buffer = Buffer.from(dataUrl.split(",")[1], 'base64');
const {note: imageNote, url} = imageService.saveImage(note.noteId, buffer, filename, true); const attachment = imageService.saveImageToAttachment(note.noteId, buffer, filename, true);
const sanitizedTitle = attachment.title.replace(/[^a-z0-9-.]/gi, "");
new BAttribute({ const url = `api/attachments/${attachment.attachmentId}/image/${sanitizedTitle}`;
noteId: imageNote.noteId,
type: 'label',
name: 'archived'
}).save(); // so that these image notes don't show up in search / autocomplete
new BAttribute({
noteId: note.noteId,
type: 'relation',
name: 'imageLink',
value: imageNote.noteId
}).save();
log.info(`Replacing '${imageId}' with '${url}' in note '${note.noteId}'`); log.info(`Replacing '${imageId}' with '${url}' in note '${note.noteId}'`);

View File

@@ -3,7 +3,7 @@
const options = require('../../services/options'); const options = require('../../services/options');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
const instanceId = require('../../services/member_id'); const instanceId = require('../../services/instance_id');
const passwordEncryptionService = require('../../services/encryption/password_encryption'); const passwordEncryptionService = require('../../services/encryption/password_encryption');
const protectedSessionService = require('../../services/protected_session'); const protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info'); const appInfo = require('../../services/app_info');

View File

@@ -55,7 +55,9 @@ const ALLOWED_OPTIONS = new Set([
'eraseUnusedAttachmentsAfterSeconds', 'eraseUnusedAttachmentsAfterSeconds',
'disableTray', 'disableTray',
'customSearchEngineName', 'customSearchEngineName',
'customSearchEngineUrl' 'customSearchEngineUrl',
'promotedAttributesOpenInRibbon',
'editedNotesOpenInRibbon'
]); ]);
function getOptions() { function getOptions() {

View File

@@ -21,8 +21,8 @@ function getRevisions(req) {
LENGTH(blobs.content) AS contentLength LENGTH(blobs.content) AS contentLength
FROM revisions FROM revisions
JOIN blobs ON revisions.blobId = blobs.blobId JOIN blobs ON revisions.blobId = blobs.blobId
WHERE noteId = ? WHERE revisions.noteId = ?
ORDER BY utcDateCreated DESC`, [req.params.noteId]); ORDER BY revisions.utcDateCreated DESC`, [req.params.noteId]);
} }
function getRevision(req) { function getRevision(req) {

View File

@@ -9,10 +9,8 @@ const optionService = require('../../services/options');
const contentHashService = require('../../services/content_hash'); const contentHashService = require('../../services/content_hash');
const log = require('../../services/log'); const log = require('../../services/log');
const syncOptions = require('../../services/sync_options'); const syncOptions = require('../../services/sync_options');
const dateUtils = require('../../services/date_utils');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const ws = require('../../services/ws'); const ws = require('../../services/ws');
const becca = require("../../becca/becca");
async function testSync() { async function testSync() {
try { try {
@@ -84,54 +82,14 @@ function forceFullSync() {
syncService.sync(); syncService.sync();
} }
function forceNoteSync(req) {
const noteId = req.params.noteId;
const note = becca.getNote(noteId);
const now = dateUtils.utcNowDateTime();
sql.execute(`UPDATE notes SET utcDateModified = ? WHERE noteId = ?`, [now, noteId]);
entityChangesService.moveEntityChangeToTop('notes', noteId);
sql.execute(`UPDATE blobs SET utcDateModified = ? WHERE blobId = ?`, [now, note.blobId]);
entityChangesService.moveEntityChangeToTop('blobs', note.blobId);
for (const branchId of sql.getColumn("SELECT branchId FROM branches WHERE noteId = ?", [noteId])) {
sql.execute(`UPDATE branches SET utcDateModified = ? WHERE branchId = ?`, [now, branchId]);
entityChangesService.moveEntityChangeToTop('branches', branchId);
}
for (const attributeId of sql.getColumn("SELECT attributeId FROM attributes WHERE noteId = ?", [noteId])) {
sql.execute(`UPDATE attributes SET utcDateModified = ? WHERE attributeId = ?`, [now, attributeId]);
entityChangesService.moveEntityChangeToTop('attributes', attributeId);
}
for (const revisionId of sql.getColumn("SELECT revisionId FROM revisions WHERE noteId = ?", [noteId])) {
sql.execute(`UPDATE revisions SET utcDateModified = ? WHERE revisionId = ?`, [now, revisionId]);
entityChangesService.moveEntityChangeToTop('revisions', revisionId);
}
for (const attachmentId of sql.getColumn("SELECT attachmentId FROM attachments WHERE noteId = ?", [noteId])) {
sql.execute(`UPDATE attachments SET utcDateModified = ? WHERE attachmentId = ?`, [now, attachmentId]);
entityChangesService.moveEntityChangeToTop('attachments', attachmentId);
}
log.info(`Forcing note sync for ${noteId}`);
// not awaiting for the job to finish (will probably take a long time)
syncService.sync();
}
function getChanged(req) { function getChanged(req) {
const startTime = Date.now(); const startTime = Date.now();
let lastEntityChangeId = parseInt(req.query.lastEntityChangeId); let lastEntityChangeId = parseInt(req.query.lastEntityChangeId);
const clientinstanceId = req.query.instanceId; const clientInstanceId = req.query.instanceId;
let filteredEntityChanges = []; let filteredEntityChanges = [];
while (filteredEntityChanges.length === 0) { do {
const entityChanges = sql.getRows(` const entityChanges = sql.getRows(`
SELECT * SELECT *
FROM entity_changes FROM entity_changes
@@ -144,20 +102,22 @@ function getChanged(req) {
break; break;
} }
filteredEntityChanges = entityChanges.filter(ec => ec.instanceId !== clientinstanceId); filteredEntityChanges = entityChanges.filter(ec => ec.instanceId !== clientInstanceId);
if (filteredEntityChanges.length === 0) { if (filteredEntityChanges.length === 0) {
lastEntityChangeId = entityChanges[entityChanges.length - 1].id; lastEntityChangeId = entityChanges[entityChanges.length - 1].id;
} }
} } while (filteredEntityChanges.length === 0);
const entityChangeRecords = syncService.getEntityChangeRecords(filteredEntityChanges); const entityChangeRecords = syncService.getEntityChangeRecords(filteredEntityChanges);
if (entityChangeRecords.length > 0) { if (entityChangeRecords.length > 0) {
lastEntityChangeId = entityChangeRecords[entityChangeRecords.length - 1].entityChange.id; lastEntityChangeId = entityChangeRecords[entityChangeRecords.length - 1].entityChange.id;
log.info(`Returning ${entityChangeRecords.length} entity changes in ${Date.now() - startTime}ms`);
} }
const ret = { return {
entityChanges: entityChangeRecords, entityChanges: entityChangeRecords,
lastEntityChangeId, lastEntityChangeId,
outstandingPullCount: sql.getValue(` outstandingPullCount: sql.getValue(`
@@ -165,14 +125,8 @@ function getChanged(req) {
FROM entity_changes FROM entity_changes
WHERE isSynced = 1 WHERE isSynced = 1
AND instanceId != ? AND instanceId != ?
AND id > ?`, [clientinstanceId, lastEntityChangeId]) AND id > ?`, [clientInstanceId, lastEntityChangeId])
}; };
if (ret.entityChanges.length > 0) {
log.info(`Returning ${ret.entityChanges.length} entity changes in ${Date.now() - startTime}ms`);
}
return ret;
} }
const partialRequests = {}; const partialRequests = {};
@@ -194,12 +148,12 @@ function update(req) {
} }
if (!partialRequests[requestId]) { if (!partialRequests[requestId]) {
throw new Error(`Partial request ${requestId}, index ${pageIndex} of ${pageCount} of pages does not have expected record.`); throw new Error(`Partial request ${requestId}, page ${pageIndex + 1} of ${pageCount} of pages does not have expected record.`);
} }
partialRequests[requestId].payload += req.body; partialRequests[requestId].payload += req.body;
log.info(`Receiving partial request ${requestId}, page index ${pageIndex} out of ${pageCount} pages.`); log.info(`Receiving a partial request ${requestId}, page ${pageIndex + 1} out of ${pageCount} pages.`);
if (pageIndex !== pageCount - 1) { if (pageIndex !== pageCount - 1) {
return; return;
@@ -212,9 +166,11 @@ function update(req) {
const {entities, instanceId} = body; const {entities, instanceId} = body;
sql.transactional(() => {
for (const {entityChange, entity} of entities) { for (const {entityChange, entity} of entities) {
syncUpdateService.updateEntity(entityChange, entity, instanceId); syncUpdateService.updateEntity(entityChange, entity, instanceId);
} }
});
} }
setInterval(() => { setInterval(() => {
@@ -241,8 +197,7 @@ function queueSector(req) {
} }
function checkEntityChanges() { function checkEntityChanges() {
const consistencyChecks = require("../../services/consistency_checks"); require("../../services/consistency_checks").runEntityChangesChecks();
consistencyChecks.runEntityChangesChecks();
} }
module.exports = { module.exports = {
@@ -251,7 +206,6 @@ module.exports = {
syncNow, syncNow,
fillEntityChanges, fillEntityChanges,
forceFullSync, forceFullSync,
forceNoteSync,
getChanged, getChanged,
update, update,
getStats, getStats,

View File

@@ -216,7 +216,6 @@ function register(app) {
apiRoute(PST, '/api/sync/now', syncApiRoute.syncNow); apiRoute(PST, '/api/sync/now', syncApiRoute.syncNow);
apiRoute(PST, '/api/sync/fill-entity-changes', syncApiRoute.fillEntityChanges); apiRoute(PST, '/api/sync/fill-entity-changes', syncApiRoute.fillEntityChanges);
apiRoute(PST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); apiRoute(PST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync);
apiRoute(PST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync);
route(GET, '/api/sync/check', [auth.checkApiAuth], syncApiRoute.checkSync, apiResultHandler); route(GET, '/api/sync/check', [auth.checkApiAuth], syncApiRoute.checkSync, apiResultHandler);
route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler); route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler);
route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler); route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler);

View File

@@ -4,8 +4,8 @@ const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir'); const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 223; const APP_DB_VERSION = 225;
const SYNC_VERSION = 30; const SYNC_VERSION = 31;
const CLIPPER_PROTOCOL_VERSION = "1.0"; const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = { module.exports = {

View File

@@ -1 +1 @@
module.exports = { buildDate:"2023-07-28T00:06:23+02:00", buildRevision: "ce3834eb9ecd710bde0e6843bd52bb306faf55ce" }; module.exports = { buildDate:"2023-08-16T23:02:15+02:00", buildRevision: "3f7a5504c77263a7118cede5c0d9b450ba37f424" };

View File

@@ -65,6 +65,7 @@ module.exports = [
{ type: 'label', name: 'executeButton'}, { type: 'label', name: 'executeButton'},
{ type: 'label', name: 'executeDescription'}, { type: 'label', name: 'executeDescription'},
{ type: 'label', name: 'newNotesOnTop'}, { type: 'label', name: 'newNotesOnTop'},
{ type: 'label', name: 'clipperInbox'},
// relation names // relation names
{ type: 'relation', name: 'internalLink' }, { type: 'relation', name: 'internalLink' },

View File

@@ -161,7 +161,7 @@ function cloneNoteAfter(noteId, afterBranchId) {
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
[afterNote.parentNoteId, afterNote.notePosition]); [afterNote.parentNoteId, afterNote.notePosition]);
eventChangesService.addNoteReorderingEntityChange(afterNote.parentNoteId); eventChangesService.putNoteReorderingEntityChange(afterNote.parentNoteId);
const branch = new BBranch({ const branch = new BBranch({
noteId: noteId, noteId: noteId,

View File

@@ -56,7 +56,7 @@ function getAndClearEntityChangeIds() {
return entityChangeIds; return entityChangeIds;
} }
function addEntityChange(entityChange) { function putEntityChange(entityChange) {
if (namespace.get('ignoreEntityChangeIds')) { if (namespace.get('ignoreEntityChangeIds')) {
return; return;
} }
@@ -91,6 +91,6 @@ module.exports = {
isEntityEventsDisabled, isEntityEventsDisabled,
reset, reset,
getAndClearEntityChangeIds, getAndClearEntityChangeIds,
addEntityChange, putEntityChange,
ignoreEntityChangeIds, ignoreEntityChangeIds,
}; };

View File

@@ -414,7 +414,7 @@ class ConsistencyChecks {
const hash = utils.hash(utils.randomString(10)); const hash = utils.hash(utils.randomString(10));
entityChangesService.addEntityChange({ entityChangesService.putEntityChange({
entityName: 'blobs', entityName: 'blobs',
entityId: blobId, entityId: blobId,
hash: hash, hash: hash,
@@ -597,23 +597,19 @@ class ConsistencyChecks {
runEntityChangeChecks(entityName, key) { runEntityChangeChecks(entityName, key) {
this.findAndFixIssues(` this.findAndFixIssues(`
SELECT SELECT ${key} as entityId
${key} as entityId FROM ${entityName}
FROM LEFT JOIN entity_changes ec ON ec.entityName = '${entityName}' AND ec.entityId = ${entityName}.${key}
${entityName} WHERE ec.id IS NULL`,
LEFT JOIN entity_changes ON entity_changes.entityName = '${entityName}'
AND entity_changes.entityId = ${key}
WHERE
entity_changes.id IS NULL`,
({entityId}) => { ({entityId}) => {
const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]); const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
if (this.autoFix) { if (this.autoFix) {
entityChangesService.addEntityChange({ entityChangesService.putEntityChange({
entityName, entityName,
entityId, entityId,
hash: utils.randomString(10), // doesn't matter, will force sync, but that's OK hash: utils.randomString(10), // doesn't matter, will force sync, but that's OK
isErased: !!entityRow.isErased, isErased: false,
utcDateChanged: entityRow.utcDateModified || entityRow.utcDateCreated, utcDateChanged: entityRow.utcDateModified || entityRow.utcDateCreated,
isSynced: entityName !== 'options' || entityRow.isSynced isSynced: entityName !== 'options' || entityRow.isSynced
}); });
@@ -625,15 +621,13 @@ class ConsistencyChecks {
}); });
this.findAndFixIssues(` this.findAndFixIssues(`
SELECT SELECT id, entityId
id, entityId FROM entity_changes
FROM LEFT JOIN ${entityName} ON entityId = ${entityName}.${key}
entity_changes
LEFT JOIN ${entityName} ON entityId = ${key}
WHERE WHERE
entity_changes.isErased = 0 entity_changes.isErased = 0
AND entity_changes.entityName = '${entityName}' AND entity_changes.entityName = '${entityName}'
AND ${key} IS NULL`, AND ${entityName}.${key} IS NULL`,
({id, entityId}) => { ({id, entityId}) => {
if (this.autoFix) { if (this.autoFix) {
sql.execute("DELETE FROM entity_changes WHERE entityName = ? AND entityId = ?", [entityName, entityId]); sql.execute("DELETE FROM entity_changes WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
@@ -645,11 +639,9 @@ class ConsistencyChecks {
}); });
this.findAndFixIssues(` this.findAndFixIssues(`
SELECT SELECT id, entityId
id, entityId FROM entity_changes
FROM JOIN ${entityName} ON entityId = ${entityName}.${key}
entity_changes
JOIN ${entityName} ON entityId = ${key}
WHERE WHERE
entity_changes.isErased = 1 entity_changes.isErased = 1
AND entity_changes.entityName = '${entityName}'`, AND entity_changes.entityName = '${entityName}'`,

View File

@@ -14,7 +14,8 @@ function getEntityHashes() {
const hashRows = sql.getRawRows(` const hashRows = sql.getRawRows(`
SELECT entityName, SELECT entityName,
entityId, entityId,
hash hash,
isErased
FROM entity_changes FROM entity_changes
WHERE isSynced = 1 WHERE isSynced = 1
AND entityName != 'note_reordering'`); AND entityName != 'note_reordering'`);
@@ -25,12 +26,13 @@ function getEntityHashes() {
const hashMap = {}; const hashMap = {};
for (const [entityName, entityId, hash] of hashRows) { for (const [entityName, entityId, hash, isErased] of hashRows) {
const entityHashMap = hashMap[entityName] = hashMap[entityName] || {}; const entityHashMap = hashMap[entityName] = hashMap[entityName] || {};
const sector = entityId[0]; const sector = entityId[0];
entityHashMap[sector] = (entityHashMap[sector] || "") + hash // if the entity is erased, its hash is not updated, so it has to be added extra
entityHashMap[sector] = (entityHashMap[sector] || "") + hash + isErased;
} }
for (const entityHashMap of Object.values(hashMap)) { for (const entityHashMap of Object.values(hashMap)) {

View File

@@ -3,19 +3,19 @@ const dateUtils = require('./date_utils');
const log = require('./log'); const log = require('./log');
const cls = require('./cls'); const cls = require('./cls');
const utils = require('./utils'); const utils = require('./utils');
const instanceId = require('./member_id'); const instanceId = require('./instance_id');
const becca = require("../becca/becca"); const becca = require("../becca/becca");
const blobService = require("../services/blob"); const blobService = require("../services/blob");
let maxEntityChangeId = 0; let maxEntityChangeId = 0;
function addEntityChangeWithInstanceId(origEntityChange, instanceId) { function putEntityChangeWithInstanceId(origEntityChange, instanceId) {
const ec = {...origEntityChange, instanceId}; const ec = {...origEntityChange, instanceId};
return addEntityChange(ec); putEntityChange(ec);
} }
function addEntityChange(origEntityChange) { function putEntityChange(origEntityChange) {
const ec = {...origEntityChange}; const ec = {...origEntityChange};
delete ec.id; delete ec.id;
@@ -32,11 +32,11 @@ function addEntityChange(origEntityChange) {
maxEntityChangeId = Math.max(maxEntityChangeId, ec.id); maxEntityChangeId = Math.max(maxEntityChangeId, ec.id);
cls.addEntityChange(ec); cls.putEntityChange(ec);
} }
function addNoteReorderingEntityChange(parentNoteId, componentId) { function putNoteReorderingEntityChange(parentNoteId, componentId) {
addEntityChange({ putEntityChange({
entityName: "note_reordering", entityName: "note_reordering",
entityId: parentNoteId, entityId: parentNoteId,
hash: 'N/A', hash: 'N/A',
@@ -55,24 +55,24 @@ function addNoteReorderingEntityChange(parentNoteId, componentId) {
}); });
} }
function moveEntityChangeToTop(entityName, entityId) { function putEntityChangeForOtherInstances(ec) {
const ec = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [entityName, entityId]); putEntityChange({
...ec,
addEntityChange(ec); changeId: null,
instanceId: null
});
} }
function addEntityChangesForSector(entityName, sector) { function addEntityChangesForSector(entityName, sector) {
const startTime = Date.now();
const entityChanges = sql.getRows(`SELECT * FROM entity_changes WHERE entityName = ? AND SUBSTR(entityId, 1, 1) = ?`, [entityName, sector]); const entityChanges = sql.getRows(`SELECT * FROM entity_changes WHERE entityName = ? AND SUBSTR(entityId, 1, 1) = ?`, [entityName, sector]);
sql.transactional(() => { sql.transactional(() => {
for (const ec of entityChanges) { for (const ec of entityChanges) {
addEntityChange(ec); putEntityChange(ec);
} }
}); });
log.info(`Added sector ${sector} of '${entityName}' (${entityChanges.length} entities) to sync queue in ${Date.now() - startTime}ms.`); log.info(`Added sector ${sector} of '${entityName}' (${entityChanges.length} entities) to the sync queue.`);
} }
function cleanupEntityChangesForMissingEntities(entityName, entityPrimaryKey) { function cleanupEntityChangesForMissingEntities(entityName, entityPrimaryKey) {
@@ -103,39 +103,34 @@ function fillEntityChanges(entityName, entityPrimaryKey, condition = '') {
createdCount++; createdCount++;
let hash; const ec = {
let utcDateChanged; entityName,
let isSynced; entityId,
isErased: false
};
if (entityName === 'blobs') { if (entityName === 'blobs') {
const blob = sql.getRow("SELECT blobId, content, utcDateModified FROM blobs WHERE blobId = ?", [entityId]); const blob = sql.getRow("SELECT blobId, content, utcDateModified FROM blobs WHERE blobId = ?", [entityId]);
hash = blobService.calculateContentHash(blob); ec.hash = blobService.calculateContentHash(blob);
utcDateChanged = blob.utcDateModified; ec.utcDateChanged = blob.utcDateModified;
isSynced = true; // blobs are always synced ec.isSynced = true; // blobs are always synced
} else { } else {
const entity = becca.getEntity(entityName, entityId); const entity = becca.getEntity(entityName, entityId);
if (entity) { if (entity) {
hash = entity?.generateHash() || "|deleted"; ec.hash = entity.generateHash() || "|deleted";
utcDateChanged = entity?.getUtcDateChanged() || dateUtils.utcNowDateTime(); ec.utcDateChanged = entity.getUtcDateChanged() || dateUtils.utcNowDateTime();
isSynced = entityName !== 'options' || !!entity?.isSynced; ec.isSynced = entityName !== 'options' || !!entity.isSynced;
} else { } else {
// entity might be null (not present in becca) when it's deleted // entity might be null (not present in becca) when it's deleted
// FIXME: hacky, not sure if it might cause some problems // FIXME: hacky, not sure if it might cause some problems
hash = "deleted"; ec.hash = "deleted";
utcDateChanged = dateUtils.utcNowDateTime(); ec.utcDateChanged = dateUtils.utcNowDateTime();
isSynced = true; // deletable (the ones with isDeleted) entities are synced ec.isSynced = true; // deletable (the ones with isDeleted) entities are synced
} }
} }
addEntityChange({ putEntityChange(ec);
entityName,
entityId,
hash: hash,
isErased: false,
utcDateChanged: utcDateChanged,
isSynced: isSynced
});
} }
if (createdCount > 0) { if (createdCount > 0) {
@@ -164,10 +159,10 @@ function recalculateMaxEntityChangeId() {
} }
module.exports = { module.exports = {
addNoteReorderingEntityChange, putNoteReorderingEntityChange,
moveEntityChangeToTop, putEntityChangeForOtherInstances,
addEntityChange, putEntityChange,
addEntityChangeWithInstanceId, putEntityChangeWithInstanceId,
fillAllEntityChanges, fillAllEntityChanges,
addEntityChangesForSector, addEntityChangesForSector,
getMaxEntityChangeId: () => maxEntityChangeId, getMaxEntityChangeId: () => maxEntityChangeId,

View File

@@ -37,8 +37,9 @@ function eraseNotes(noteIdsToErase) {
function setEntityChangesAsErased(entityChanges) { function setEntityChangesAsErased(entityChanges) {
for (const ec of entityChanges) { for (const ec of entityChanges) {
ec.isErased = true; ec.isErased = true;
ec.utcDateChanged = dateUtils.utcNowDateTime();
entityChangesService.addEntityChange(ec); entityChangesService.putEntityChange(ec);
} }
} }

View File

@@ -301,16 +301,10 @@ function importEnex(taskContext, file, parentNote) {
? resource.title ? resource.title
: `image.${resource.mime.substr(6)}`; // default if real name is not present : `image.${resource.mime.substr(6)}`; // default if real name is not present
const {url, note: imageNote} = imageService.saveImage(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages); const attachment = imageService.saveImageToAttachment(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages);
for (const attr of resource.attributes) {
if (attr.name !== 'originalFileName') { // this one is already saved in imageService
imageNote.addAttribute(attr.type, attr.name, attr.value);
}
}
updateDates(imageNote, utcDateCreated, utcDateModified);
const sanitizedTitle = attachment.title.replace(/[^a-z0-9-.]/gi, "");
const url = `api/attachments/${attachment.attachmentId}/image/${sanitizedTitle}`;
const imageLink = `<img src="${url}">`; const imageLink = `<img src="${url}">`;
content = content.replace(mediaRegex, imageLink); content = content.replace(mediaRegex, imageLink);

View File

@@ -18,7 +18,7 @@ async function migrate() {
// backup before attempting migration // backup before attempting migration
await backupService.backupNow( await backupService.backupNow(
// special name for the pre-0.60 migration to prevent later overwrite // creating a special backup for versions 0.60.X and older, the changes in 0.61 are major.
currentDbVersion < 214 currentDbVersion < 214
? `before-migration-v${currentDbVersion}` ? `before-migration-v${currentDbVersion}`
: 'before-migration' : 'before-migration'
@@ -72,6 +72,9 @@ async function migrate() {
} }
} }
}); });
log.info("VACUUMing database, this might take a while ...");
sql.execute("VACUUM");
} }
function executeMigration(mig) { function executeMigration(mig) {

View File

@@ -265,7 +265,7 @@ function createNewNoteWithTarget(target, targetBranchId, params) {
const retObject = createNewNote(params); const retObject = createNewNote(params);
entityChangesService.addNoteReorderingEntityChange(params.parentNoteId); entityChangesService.putNoteReorderingEntityChange(params.parentNoteId);
return retObject; return retObject;
} }

View File

@@ -88,7 +88,9 @@ const defaultOptions = [
{ name: 'disableTray', value: 'false', isSynced: false }, { name: 'disableTray', value: 'false', isSynced: false },
{ name: 'eraseUnusedAttachmentsAfterSeconds', value: '2592000', isSynced: true }, { name: 'eraseUnusedAttachmentsAfterSeconds', value: '2592000', isSynced: true },
{ name: 'customSearchEngineName', value: 'DuckDuckGo', isSynced: true }, { name: 'customSearchEngineName', value: 'DuckDuckGo', isSynced: true },
{ name: 'customSearchEngineUrl', value: 'https://duckduckgo.com/?q={keyword}', isSynced: true } { name: 'customSearchEngineUrl', value: 'https://duckduckgo.com/?q={keyword}', isSynced: true },
{ name: 'promotedAttributesOpenInRibbon', value: 'true', isSynced: true },
{ name: 'editedNotesOpenInRibbon', value: 'true', isSynced: true }
]; ];
function initStartupOptions() { function initStartupOptions() {

View File

@@ -3,6 +3,7 @@
const log = require('./log'); const log = require('./log');
const sql = require('./sql'); const sql = require('./sql');
const protectedSessionService = require("./protected_session"); const protectedSessionService = require("./protected_session");
const dateUtils = require("./date_utils");
/** /**
* @param {BNote} note * @param {BNote} note
@@ -40,7 +41,7 @@ function eraseRevisions(revisionIdsToErase) {
log.info(`Removing note revisions: ${JSON.stringify(revisionIdsToErase)}`); log.info(`Removing note revisions: ${JSON.stringify(revisionIdsToErase)}`);
sql.executeMany(`DELETE FROM revisions WHERE revisionId IN (???)`, revisionIdsToErase); sql.executeMany(`DELETE FROM revisions WHERE revisionId IN (???)`, revisionIdsToErase);
sql.executeMany(`UPDATE entity_changes SET isErased = 1 WHERE entityName = 'revisions' AND entityId IN (???)`, revisionIdsToErase); sql.executeMany(`UPDATE entity_changes SET isErased = 1, utcDateChanged = '${dateUtils.utcNowDateTime()}' WHERE entityName = 'revisions' AND entityId IN (???)`, revisionIdsToErase);
} }
module.exports = { module.exports = {

View File

@@ -4,7 +4,7 @@ const log = require('./log');
const sql = require('./sql'); const sql = require('./sql');
const optionService = require('./options'); const optionService = require('./options');
const utils = require('./utils'); const utils = require('./utils');
const instanceId = require('./member_id'); const instanceId = require('./instance_id');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const syncUpdateService = require('./sync_update'); const syncUpdateService = require('./sync_update');
const contentHashService = require('./content_hash'); const contentHashService = require('./content_hash');
@@ -54,12 +54,12 @@ async function sync() {
}); });
} }
catch (e) { catch (e) {
// we're dynamically switching whether we're using proxy or not based on whether we encountered error with the current method
proxyToggle = !proxyToggle; proxyToggle = !proxyToggle;
if (e.message && if (e.message?.includes('ECONNREFUSED') ||
(e.message.includes('ECONNREFUSED') || e.message?.includes('ERR_') || // node network errors
e.message.includes('ERR_') || // node network errors e.message?.includes('Bad Gateway')) {
e.message.includes('Bad Gateway'))) {
ws.syncFailed(); ws.syncFailed();
@@ -108,7 +108,7 @@ async function doLogin() {
}); });
if (resp.instanceId === instanceId) { if (resp.instanceId === instanceId) {
throw new Error(`Sync server has member ID '${resp.instanceId}' which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`); throw new Error(`Sync server has instance ID '${resp.instanceId}' which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`);
} }
syncContext.instanceId = resp.instanceId; syncContext.instanceId = resp.instanceId;
@@ -146,9 +146,12 @@ async function pullChanges(syncContext) {
sql.transactional(() => { sql.transactional(() => {
for (const {entityChange, entity} of entityChanges) { for (const {entityChange, entity} of entityChanges) {
const changeAppliedAlready = entityChange.changeId const changeAppliedAlready = entityChange.changeId
&& !!sql.getValue("SELECT id FROM entity_changes WHERE changeId = ?", [entityChange.changeId]); && !!sql.getValue("SELECT 1 FROM entity_changes WHERE changeId = ?", [entityChange.changeId]);
if (changeAppliedAlready) {
continue;
}
if (!changeAppliedAlready) {
if (!atLeastOnePullApplied) { // send only for first if (!atLeastOnePullApplied) { // send only for first
ws.syncPullInProgress(); ws.syncPullInProgress();
@@ -157,7 +160,6 @@ async function pullChanges(syncContext) {
syncUpdateService.updateEntity(entityChange, entity, syncContext.instanceId); syncUpdateService.updateEntity(entityChange, entity, syncContext.instanceId);
} }
}
if (lastSyncedPull !== lastEntityChangeId) { if (lastSyncedPull !== lastEntityChangeId) {
setLastSyncedPull(lastEntityChangeId); setLastSyncedPull(lastEntityChangeId);
@@ -254,7 +256,7 @@ async function checkContentHash(syncContext) {
const failedChecks = contentHashService.checkContentHashes(resp.entityHashes); const failedChecks = contentHashService.checkContentHashes(resp.entityHashes);
if (failedChecks.length > 0) { if (failedChecks.length > 0) {
// before requeuing sectors, make sure the entity changes are correct // before re-queuing sectors, make sure the entity changes are correct
const consistencyChecks = require("./consistency_checks"); const consistencyChecks = require("./consistency_checks");
consistencyChecks.runEntityChangesChecks(); consistencyChecks.runEntityChangesChecks();
@@ -351,7 +353,8 @@ function getEntityChangeRecords(entityChanges) {
length += JSON.stringify(record).length; length += JSON.stringify(record).length;
if (length > 1000000) { if (length > 1_000_000) {
// each sync request/response should have at most ~1 MB.
break; break;
} }
} }

View File

@@ -4,98 +4,83 @@ const entityChangesService = require('./entity_changes');
const eventService = require('./events'); const eventService = require('./events');
const entityConstructor = require("../becca/entity_constructor"); const entityConstructor = require("../becca/entity_constructor");
function updateEntity(entityChange, entityRow, instanceId) { function updateEntity(remoteEC, remoteEntityRow, instanceId) {
// can be undefined for options with isSynced=false if (!remoteEntityRow && remoteEC.entityName === 'options') {
if (!entityRow) { return; // can be undefined for options with isSynced=false
if (entityChange.isSynced) {
if (entityChange.isErased) {
eraseEntity(entityChange, instanceId);
}
else {
log.info(`Encountered synced non-erased entity change without entity: ${JSON.stringify(entityChange)}`);
}
}
else if (entityChange.entityName !== 'options') {
log.info(`Encountered unsynced non-option entity change without entity: ${JSON.stringify(entityChange)}`);
} }
return; const updated = remoteEC.entityName === 'note_reordering'
} ? updateNoteReordering(remoteEC, remoteEntityRow, instanceId)
: updateNormalEntity(remoteEC, remoteEntityRow, instanceId);
const updated = entityChange.entityName === 'note_reordering'
? updateNoteReordering(entityChange, entityRow, instanceId)
: updateNormalEntity(entityChange, entityRow, instanceId);
if (updated) { if (updated) {
if (entityRow.isDeleted) { if (remoteEntityRow?.isDeleted) {
eventService.emit(eventService.ENTITY_DELETE_SYNCED, { eventService.emit(eventService.ENTITY_DELETE_SYNCED, {
entityName: entityChange.entityName, entityName: remoteEC.entityName,
entityId: entityChange.entityId entityId: remoteEC.entityId
}); });
} }
else if (!entityChange.isErased) { else if (!remoteEC.isErased) {
eventService.emit(eventService.ENTITY_CHANGE_SYNCED, { eventService.emit(eventService.ENTITY_CHANGE_SYNCED, {
entityName: entityChange.entityName, entityName: remoteEC.entityName,
entityRow entityRow: remoteEntityRow
}); });
} }
} }
} }
function updateNormalEntity(remoteEntityChange, remoteEntityRow, instanceId) { function updateNormalEntity(remoteEC, remoteEntityRow, instanceId) {
const localEntityChange = sql.getRow(` const localEC = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [remoteEC.entityName, remoteEC.entityId]);
SELECT utcDateChanged, hash, isErased
FROM entity_changes
WHERE entityName = ? AND entityId = ?`, [remoteEntityChange.entityName, remoteEntityChange.entityId]);
if (localEntityChange && !localEntityChange.isErased && remoteEntityChange.isErased) { if (!localEC?.isErased && remoteEC.isErased) {
sql.transactional(() => { eraseEntity(remoteEC, instanceId);
const primaryKey = entityConstructor.getEntityFromEntityName(remoteEntityChange.entityName).primaryKeyName;
sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId);
entityChangesService.addEntityChangeWithInstanceId(remoteEntityChange, instanceId);
});
return true; return true;
} else if (localEC?.isErased && !remoteEC.isErased) {
// on this side, we can't unerase the entity, so force the entity to be erased on the other side.
entityChangesService.putEntityChangeForOtherInstances(localEC);
return false;
} }
if (!localEntityChange if (!localEC
|| localEntityChange.utcDateChanged < remoteEntityChange.utcDateChanged || localEC.utcDateChanged < remoteEC.utcDateChanged
|| localEntityChange.hash !== remoteEntityChange.hash // sync error, we should still update || (localEC.utcDateChanged === remoteEC.utcDateChanged && localEC.hash !== remoteEC.hash) // sync error, we should still update
) { ) {
if (remoteEntityChange.entityName === 'blobs') { if (remoteEC.entityName === 'blobs' && remoteEntityRow.content !== null) {
// we always use a Buffer object which is different from normal saving - there we use a simple string type for // we always use a Buffer object which is different from normal saving - there we use a simple string type for
// "string notes". The problem is that in general, it's not possible to detect whether a blob content // "string notes". The problem is that in general, it's not possible to detect whether a blob content
// is string note or note (syncs can arrive out of order) // is string note or note (syncs can arrive out of order)
remoteEntityRow.content = remoteEntityRow.content === null ? null : Buffer.from(remoteEntityRow.content, 'base64'); remoteEntityRow.content = Buffer.from(remoteEntityRow.content, 'base64');
if (remoteEntityRow.content?.byteLength === 0) { if (remoteEntityRow.content.byteLength === 0) {
// there seems to be a bug which causes empty buffer to be stored as NULL which is then picked up as inconsistency // there seems to be a bug which causes empty buffer to be stored as NULL which is then picked up as inconsistency
// (possibly not a problem anymore with the newer better-sqlite3)
remoteEntityRow.content = ""; remoteEntityRow.content = "";
} }
} }
sql.transactional(() => { sql.replace(remoteEC.entityName, remoteEntityRow);
sql.replace(remoteEntityChange.entityName, remoteEntityRow);
entityChangesService.addEntityChangeWithInstanceId(remoteEntityChange, instanceId); entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
});
return true; return true;
} else if (localEC.hash !== remoteEC.hash && localEC.utcDateChanged > remoteEC.utcDateChanged) {
// the change on our side is newer than on the other side, so the other side should update
entityChangesService.putEntityChangeForOtherInstances(localEC);
return false;
} }
return false; return false;
} }
function updateNoteReordering(entityChange, entity, instanceId) { function updateNoteReordering(remoteEC, remoteEntityRow, instanceId) {
sql.transactional(() => { for (const key in remoteEntityRow) {
for (const key in entity) { sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remoteEntityRow[key], key]);
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]);
} }
entityChangesService.addEntityChangeWithInstanceId(entityChange, instanceId); entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
});
return true; return true;
} }
@@ -109,21 +94,19 @@ function eraseEntity(entityChange, instanceId) {
"attributes", "attributes",
"revisions", "revisions",
"attachments", "attachments",
"blobs", "blobs"
]; ];
if (!entityNames.includes(entityName)) { if (!entityNames.includes(entityName)) {
log.error(`Cannot erase entity '${entityName}', id '${entityId}'`); log.error(`Cannot erase entity '${entityName}', id '${entityId}'.`);
return; return;
} }
const keyName = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; const primaryKeyName = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
sql.execute(`DELETE FROM ${entityName} WHERE ${keyName} = ?`, [entityId]); sql.execute(`DELETE FROM ${entityName} WHERE ${primaryKeyName} = ?`, [entityId]);
eventService.emit(eventService.ENTITY_DELETE_SYNCED, { entityName, entityId }); entityChangesService.putEntityChangeWithInstanceId(entityChange, instanceId);
entityChangesService.addEntityChangeWithInstanceId(entityChange, instanceId);
} }
module.exports = { module.exports = {

View File

@@ -165,7 +165,7 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder
} }
if (someBranchUpdated) { if (someBranchUpdated) {
entityChangesService.addNoteReorderingEntityChange(parentNoteId); entityChangesService.putNoteReorderingEntityChange(parentNoteId);
} }
}); });
} }

View File

@@ -28,8 +28,13 @@ function hashedBlobId(content) {
// sha512 is faster than sha256 // sha512 is faster than sha256
const base64Hash = crypto.createHash('sha512').update(content).digest('base64'); const base64Hash = crypto.createHash('sha512').update(content).digest('base64');
// 20 characters of base64 gives us 120 bit of entropy which is plenty enough // we don't want such + and / in the IDs
return base64Hash.substr(0, 20); const kindaBase62Hash = base64Hash
.replace('+', 'X')
.replace('/', 'Y');
// 20 characters of base62 gives us ~120 bit of entropy which is plenty enough
return kindaBase62Hash.substr(0, 20);
} }
function toBase64(plainText) { function toBase64(plainText) {