Compare commits

...

187 Commits

Author SHA1 Message Date
azivner
2d1bc46c04 release 0.20.0 2018-08-27 18:59:54 +02:00
azivner
4bc44605fb don't short child: promoted attributes 2018-08-27 13:35:45 +02:00
azivner
b868990fba using exact versions of packages from now on 2018-08-23 23:42:47 +02:00
azivner
26c06c9826 more API docs 2018-08-23 15:33:19 +02:00
azivner
f5b89432a6 frontend script API documentation 2018-08-23 12:55:45 +02:00
azivner
0e7372adbf backend script API documentation 2018-08-23 10:10:04 +02:00
azivner
d4fbe28517 jsdoc comments on entities 2018-08-22 23:37:06 +02:00
azivner
668528d5eb promoted attributes are visible in tooltip preview, fixes #158 2018-08-22 15:31:36 +02:00
azivner
17348a9cfe fix some issues 2018-08-22 14:40:49 +02:00
azivner
09b610701d removed not unique warning from attributes dialog as it's more common to have multi value 2018-08-21 13:51:35 +02:00
azivner
71e687ad8e child: prefix now copies attributes on new note creation 2018-08-21 13:49:45 +02:00
azivner
171877ce08 renamed inheritAttributes to template 2018-08-21 12:52:11 +02:00
azivner
4f1e6ec70f note API additions 2018-08-21 12:50:43 +02:00
azivner
1938c317c3 fix relation definition in dialog 2018-08-20 10:04:26 +02:00
azivner
99d81059d0 better common JS compatibility 2018-08-19 22:28:32 +02:00
azivner
59d5a86110 fix attachment attributes 2018-08-19 21:42:03 +02:00
azivner
a5e56ea839 display a message if there's no recent change 2018-08-18 15:21:44 +02:00
azivner
44f85224e7 added new label type URL with open button, fixes #156 2018-08-18 15:00:52 +02:00
azivner
0aa08b1c1e relation promoted attribute has clickable button, fixes #155 2018-08-18 14:55:27 +02:00
azivner
406d74c4d7 initial focus on attribute type instead of name 2018-08-18 14:49:25 +02:00
azivner
7f9a8a55ca fix init of synced options in new database 2018-08-17 18:11:03 +02:00
azivner
a42bbba0e5 unprotecting note outside of protected session is not forbidden because it could overwrite previous note 2018-08-17 15:21:59 +02:00
azivner
145efe67c3 better logging and notifications on script errors for easier debugging 2018-08-17 11:31:42 +02:00
azivner
513748836e note autocomplete and full text search should be able to find notes by noteId 2018-08-17 10:06:52 +02:00
azivner
427ce3972e protected notes detail is now marked with shield background instead of just grey background 2018-08-17 09:32:07 +02:00
azivner
02c0f9a6cd shrinkable note title so the design crumbles with smaller width 2018-08-16 23:16:17 +02:00
azivner
208771216e fix in passing originEntity from frontend to backend, some refactorings 2018-08-16 23:00:04 +02:00
azivner
385d97a9b3 recent notes now don't display current note, unification of autocomplete source handling 2018-08-16 21:02:42 +02:00
azivner
e39d1d08ac easier API to add button to toolbar 2018-08-16 20:26:40 +02:00
azivner
0f106fb96f more relation events, events are now not triggered on sync changes 2018-08-15 22:06:49 +02:00
azivner
df9acd0504 relation target noteIds need to be translated into local noteIds 2018-08-15 18:32:06 +02:00
azivner
dbe0eb3f3a fix attribute name autocomplete, no tooltip preview on path selection 2018-08-15 18:22:02 +02:00
azivner
4513651e12 delete attributes when deleting note 2018-08-15 15:27:22 +02:00
azivner
3204291463 update codemirror to 5.39.2 2018-08-15 11:25:30 +02:00
azivner
510704a074 help buttons and existing custom HTML attribute refactoring to data-* 2018-08-15 10:14:14 +02:00
azivner
f440493e45 use ISO dateformat on the frontend instead of european formatting 2018-08-15 08:48:16 +02:00
azivner
b897c6de13 fix note revision saving 2018-08-15 08:44:54 +02:00
azivner
acbd18e8fc links to documentation for attributes, links and search + fix for opening external links 2018-08-14 23:07:50 +02:00
azivner
ff5b84db10 search (note) fixes 2018-08-14 22:50:05 +02:00
azivner
16535f6a73 small changes to attribute dialog 2018-08-14 21:02:42 +02:00
azivner
5b657ad961 minor package upgrades 2018-08-14 20:25:37 +02:00
azivner
bbbc3e9dc4 one more fix for release of pkg 2018-08-14 19:52:17 +02:00
azivner
f43f0e10a1 release 0.19.1 2018-08-14 18:06:31 +02:00
azivner
6d842a65a2 fix DB vacuum, fixes #154 2018-08-14 18:03:36 +02:00
azivner
50c4de021c fix for mysterious 404 not found notes like "appearance" 2018-08-14 18:00:11 +02:00
azivner
936d8449f6 fix & unify "show recent notes" buttons 2018-08-14 17:36:39 +02:00
azivner
462bc0edd5 attribute sync fix 2018-08-14 17:32:15 +02:00
azivner
35ef3c8470 fix migration 2018-08-14 16:09:30 +02:00
azivner
5117d43e29 fix pkg upload to github 2018-08-14 15:32:12 +02:00
azivner
7c9ac488e8 release 0.19.0 2018-08-14 14:19:37 +02:00
azivner
fec1574447 fixed import with attributes 2018-08-14 14:17:10 +02:00
azivner
f7587de452 fixes to multivalued input tabindex 2018-08-14 13:50:04 +02:00
azivner
41a6e777ea attributes coming from inheritAttributes are inherited only if the inheritAttributes relation itself is inheritable 2018-08-14 13:02:17 +02:00
azivner
8fb0de900b createNote API now accepts attributes instead of just labels 2018-08-14 12:54:58 +02:00
azivner
a40bf71fd4 connection lost error is now logged only to the console, it was too annoying while being mostly harmless 2018-08-14 11:42:29 +02:00
azivner
2a53bb03ae fix autocomplete casing issue with first level notes 2018-08-13 21:01:14 +02:00
azivner
a684879b91 primitive attribute caching inside note entity, fixes #149 2018-08-13 17:16:06 +02:00
azivner
ddbd4f73c8 attributes can be inherited through special relation "inheritAttributes" 2018-08-13 17:05:16 +02:00
azivner
b0ed790edf fix multivalue issue of not appearing when no attribute has been saved yet 2018-08-13 15:58:37 +02:00
azivner
3424406ff1 script API changes for task management #140 2018-08-13 13:53:08 +02:00
azivner
ce5c385c15 fix search by attributes was finding also deleted attributes 2018-08-13 11:06:17 +02:00
azivner
cd9eef32b0 support for cssClass label on note 2018-08-13 10:59:31 +02:00
azivner
12d82e3b33 listener exception doesn't stop execution 2018-08-13 09:49:39 +02:00
azivner
f071d3f651 fix validation issue + attribute not triggering change event on delete 2018-08-13 09:39:07 +02:00
azivner
297b536ebc promoted attributes have tabindex following the title and before note content 2018-08-13 09:07:21 +02:00
azivner
7cca2d9247 realoding tree doesn't steal focus (important for promoted attribute changes with attached scripts) 2018-08-13 08:42:37 +02:00
azivner
36dc802d16 updated schema.sql 2018-08-13 07:57:23 +02:00
azivner
c78ddb70cb all events are now synchronous 2018-08-12 20:07:02 +02:00
azivner
9fb0599c45 entities are now changed only if entity hash changed which will limit number of events emitted 2018-08-12 20:04:48 +02:00
azivner
13f524fb39 ENTITY_CHANGED event is emitted synchronously 2018-08-12 13:03:59 +02:00
azivner
27be3b4c90 fixes in tree loading 2018-08-12 12:59:38 +02:00
azivner
af4ea66742 fix shift-up selection, fixes #146 2018-08-11 20:02:48 +02:00
azivner
0f42c396f3 image upload fixes + some API changes 2018-08-11 19:45:55 +02:00
azivner
9e96272eb3 fixed runOnAttributeChange event 2018-08-10 14:31:57 +02:00
azivner
965dbcbc9a renamed workEntity to originEntity 2018-08-10 13:30:20 +02:00
azivner
7ac109e7f7 fix label => attributes omissions 2018-08-09 20:55:16 +02:00
azivner
ac25770c0e added runOnAttributeChange event 2018-08-09 20:08:00 +02:00
azivner
5b15424498 archived label now respects isInheritable flag, fixes #145 2018-08-08 16:14:35 +02:00
azivner
f1240c26bf more cleanup of labels and relations from backend, dropping tables from db 2018-08-07 13:44:51 +02:00
azivner
1c0fd243d1 cleanup of labels and relations from backend 2018-08-07 13:33:10 +02:00
azivner
3491235533 cleanup of labels & relations frontend code 2018-08-07 12:48:11 +02:00
azivner
5f36856571 * refactoring of repository layer to represent booleans as true/false instead of 1/0
* show list of inherited attributes, fixes #136
* properly work with inheritance
2018-08-07 11:38:00 +02:00
azivner
d3e44b37e9 autocomplete for promoted text labels 2018-08-06 22:52:49 +02:00
azivner
90e9297ec5 promoted relation attributes now work correctly, refactoring of note autocomplete code 2018-08-06 22:29:03 +02:00
azivner
c568ef2f8a nice icons for add / remove attribute 2018-08-06 17:53:13 +02:00
azivner
fcf6141cde support for promoted multi value attributes 2018-08-06 17:24:35 +02:00
azivner
21551d7b77 implemented date promoted attribute 2018-08-06 15:58:59 +02:00
azivner
12031d369f displaying and saving number and boolean promoted attributes 2018-08-06 15:23:22 +02:00
azivner
b44c523845 basic support for saving promoted attributes 2018-08-06 14:43:42 +02:00
azivner
49989695ff fix relations in attributes 2018-08-06 11:30:37 +02:00
azivner
a55d3530e9 attribute list on the bottom if there are no promoted attributes 2018-08-06 09:41:01 +02:00
azivner
2aab3ad281 fixes in attribute persistence + WIP on display of promoted attrs 2018-08-06 08:59:26 +02:00
azivner
194ce4f10f fixed UI for relation definition 2018-08-05 20:48:56 +02:00
azivner
2089c32839 attribute UI & saving now fully working 2018-08-05 20:08:56 +02:00
azivner
f437be7af0 attribute definition work in progress 2018-08-03 22:56:49 +02:00
azivner
96dc56098d ckeditor upgrade to 11.0.1 with blocktoolbar 2018-08-03 22:56:23 +02:00
azivner
61987e46f7 work in progress on attributes UI - unification of labels and relations now mostly works 2018-08-03 13:06:56 +02:00
azivner
509093b755 added "type" to attribute dialog, name autocomplete servers according to the choice 2018-08-03 11:11:57 +02:00
azivner
097114c0f2 basic entities for attributes (unification of labels and relations) 2018-08-02 22:48:21 +02:00
azivner
040f9185f8 electron 2.0.6 and some other minor library upgrades 2018-08-02 19:55:20 +02:00
azivner
6dc934abbe refactored targetNote to workNote in the ScriptContext which was very confusing with relation's targetNote 2018-08-01 10:12:54 +02:00
azivner
2d24bf81dd added new label "sorted" which will keep children notes alphabetically sorted, fixes #82 2018-08-01 09:26:02 +02:00
azivner
9452fc236b electron build uses random free port, fixes #142 2018-07-31 19:50:18 +02:00
azivner
365c37604b code note in tooltip needs to be wrapped in <pre> to keep formatting, fixes #137 2018-07-30 16:55:20 +02:00
azivner
01c7e58d47 check if sync is configured every minute, not just at app startup, fixes #138 2018-07-30 16:45:34 +02:00
azivner
d3d49923b1 changed backup to simple scheme with one daily, one weekly and one monthly backup, fixes #15 2018-07-30 16:40:50 +02:00
azivner
263ac299d0 fix pkg building 2018-07-30 14:18:43 +02:00
azivner
3d185a5178 release 0.18.0 2018-07-30 08:18:25 +02:00
azivner
2ff7a890bc script which shows list of edited files for a date, fixes #125 2018-07-29 20:51:28 +02:00
azivner
2eb1a9705f it's now possible to mark relation as inheritable (previously this was hardcoded for specific relation names) 2018-07-29 20:33:42 +02:00
azivner
ed1381103a #125, implementation of inheritable relations 2018-07-29 18:39:10 +02:00
azivner
170d317589 #125, basic infrastructure for scripts attached to notes via relations 2018-07-29 16:06:13 +02:00
azivner
ededc063df fix relation autocomplete 2018-07-29 12:34:40 +02:00
azivner
986eace1be schema update with relations 2018-07-29 11:47:46 +02:00
azivner
29086d8dfe fixed bug with select not firing on recent notes 2018-07-28 18:17:35 +02:00
azivner
9b3f3fde05 #126, relation list in note detail and fixes in saving 2018-07-28 17:59:55 +02:00
azivner
6a50afd952 #126, "show recent notes" now work in relations dialog 2018-07-28 17:02:48 +02:00
azivner
697eee2706 #126, autocomplete + can save relations 2018-07-27 11:28:24 +02:00
azivner
8a95afd756 #126, added skeleton of note relations, copied from similar concept of labels 2018-07-27 10:52:48 +02:00
azivner
4d6eda8fe6 #129, added input icon to trigger recent notes for easy discovery and mouse control 2018-07-27 09:22:25 +02:00
azivner
e4f459fa2b #129, removed recent notes dialog as its not necessary anymore 2018-07-26 17:35:32 +02:00
azivner
f578e001b0 #129, add link autocomplete now displays recent notes as well 2018-07-26 16:24:08 +02:00
azivner
2a08aef885 #129, recent notes are now visible in the jump to dialog 2018-07-26 16:05:09 +02:00
azivner
7564bf388c removed dangerous and unnecessary option to completely remove soft-deleted items 2018-07-26 09:21:52 +02:00
azivner
7e4d70259f soft-deleting note will delete its content and all the revisions content, fixes #132 2018-07-26 09:08:51 +02:00
azivner
5b98c1c0f3 fix context menu JS error on root note 2018-07-26 08:58:20 +02:00
azivner
02dc7b199b #98, better error reporting for sync setup 2018-07-25 22:54:37 +02:00
azivner
d39cdbfada better instructions for desktop => server instance sync setup, plus some sync fixes 2018-07-25 10:57:36 +02:00
azivner
50bb4a47ee fix sync bug 2018-07-25 10:12:34 +02:00
azivner
a4627f2ddb #98 some sync setup refactorings 2018-07-25 09:46:57 +02:00
azivner
c8253caae9 #98 proxy support for sync setup 2018-07-25 08:30:41 +02:00
azivner
0ece9bd1be sync isExpanded when it's a new branch 2018-07-24 22:03:36 +02:00
azivner
b6935abcc9 #98, sync button now shows total number of outstanding syncs instead of just pushes 2018-07-24 21:43:15 +02:00
azivner
37ab7b4641 #98, sync to server now works as well + a lot of related changes 2018-07-24 20:35:03 +02:00
azivner
013714cb5c #98, new option "initialized" which indicates if setup has been finished 2018-07-24 08:12:36 +02:00
azivner
1fe7c62f5a #98, sync setup now doesn't copy the whole DB file, but sets up minimal database and starts off sync 2018-07-23 21:15:32 +02:00
azivner
a06618d851 #98, test sync impl 2018-07-23 10:29:17 +02:00
azivner
e7460ca3a9 #98, sync is now configured in the options 2018-07-22 22:21:16 +02:00
azivner
073300bbcd #98, working sync setup from server to desktop instance + refactoring of DB initialization 2018-07-22 19:56:20 +02:00
azivner
a201661ce5 #98, fixes in the wizard 2018-07-22 14:49:59 +02:00
azivner
6235a3c886 beginning of #98, new multistep wizard, db creation after user enters username and password 2018-07-21 08:55:24 +02:00
azivner
3972c27e7a release 0.17.0 2018-07-09 21:22:12 +02:00
azivner
14cffbbe62 docker commands with sudo 2018-07-09 21:21:28 +02:00
azivner
599c3c04af correct sourceMappingURL which gets rid of error message, fixes #114 2018-07-08 23:23:49 +02:00
azivner
f1412b631d hide log polluting error message 2018-07-08 23:13:56 +02:00
azivner
41908050bb ctrl+u now doesn't trigger show source since it's occupied by underline 2018-07-08 21:45:05 +02:00
azivner
f07033423c ckeditor upgraded to 10.1.0 plus new plugins (table, strikethrough, underline) 2018-07-08 21:21:52 +02:00
azivner
daf96fcbf2 using zeit/pkg to package easy to use linux server edition 2018-07-06 00:05:06 +02:00
azivner
2bca94529e minor upgrades 2018-07-05 23:17:53 +02:00
azivner
b2c9a0da21 fix note sources, fixes #123 2018-07-04 20:37:23 +02:00
azivner
7a9542b4fc release 0.16.0 2018-06-16 13:34:39 -04:00
azivner
3a95c9e1bc all dialogs are now non-modal because of high cpu usage 2018-06-16 13:31:56 -04:00
azivner
3d2ef6be01 remove optionId, closes #117 2018-06-13 19:10:28 -04:00
azivner
d67246699a Introduced separate sync version (previously DB version was used to check sync compatibility), closes #120 2018-06-10 15:55:29 -04:00
azivner
14c704d6db db upgrades are now handled transparently in the background without bothering the user, closes #119 2018-06-10 15:49:22 -04:00
azivner
4c8eeb2e6f added docker build, closes #106 2018-06-10 15:06:52 -04:00
azivner
c1b245c8b1 fix unnecessary change events, closes #118 2018-06-10 11:51:13 -04:00
azivner
74202d67bb got rid of "Trilium Notes" branding - not necessary and takes valuable space 2018-06-10 10:57:45 -04:00
azivner
26066f39b1 chaged "focused mode" - now title is displayed as well and together with content takes whole window 2018-06-10 10:53:39 -04:00
azivner
b255cf190c fixes for zoom factor setting 2018-06-09 10:34:51 -04:00
azivner
bc77b143b0 darker outlines so inverted dark themes are more visible 2018-06-09 09:48:18 -04:00
azivner
9f0ff6ae7a note actions dropdown sizing 2018-06-09 09:44:40 -04:00
azivner
736704c7d6 fix show paths 2018-06-09 09:32:13 -04:00
azivner
654c116c58 use backgrounds for icon buttons so that dark and black themes look better 2018-06-09 09:28:50 -04:00
azivner
89a5cab98f added too options new tab appearance with possibility to change theme (white, black, dark) and zoom factor 2018-06-08 23:18:53 -04:00
azivner
c39d0be8cd refactoring of icon button styles 2018-06-08 22:17:00 -04:00
azivner
e75b4cd848 execute on script note is icon, closes #116 2018-06-08 21:59:40 -04:00
azivner
378e8f35e5 release 0.15.0 2018-06-07 23:09:21 -04:00
azivner
bdb5e2f13f no results also for "add link" 2018-06-07 23:08:41 -04:00
azivner
8211bed449 renamed icons according to their size, fixes #113 2018-06-07 23:02:21 -04:00
azivner
b243632483 usability improvements to autocomplete ("no results" etc.), needs refactoring 2018-06-07 20:18:46 -04:00
azivner
e4d2513451 close search button 2018-06-07 19:50:16 -04:00
azivner
385144451b renamed hideInAutocomplete label to archived 2018-06-07 19:26:28 -04:00
azivner
c8c533844e icons for render & toggle edit since text titles caused unwanted horizontal scrolling in smaller window sizes 2018-06-06 23:37:57 -04:00
azivner
0e69f0c079 fix recent notes issues 2018-06-06 22:38:36 -04:00
azivner
aee60c444f added "show results in full text" 2018-06-05 23:28:10 -04:00
azivner
e7a504c66b fixes and optimizations for search 2018-06-05 22:47:47 -04:00
azivner
45d9c7164c search refactorings 2018-06-05 19:12:52 -04:00
azivner
bd913a63a8 search note fixes 2018-06-04 23:21:45 -04:00
azivner
5a1938c078 better sizing of search pane 2018-06-04 20:22:41 -04:00
azivner
015cd68756 renaming/refactoring of search services 2018-06-04 19:48:02 -04:00
azivner
76c0e5b2b8 new UI for search, closes #108 (still needs cleanup) 2018-06-03 20:42:25 -04:00
azivner
0f8f707acd persisting zoom setting in electron, fixes #112 2018-06-02 13:02:20 -04:00
azivner
083cccea28 better protected/unprotected note indicator, fixes #110 2018-06-02 11:47:16 -04:00
azivner
31b76b23ce release 0.14.1 2018-06-02 09:39:37 -04:00
azivner
af529f82e5 fixed false sync error reporting 2018-06-02 09:39:04 -04:00
azivner
fc6669d254 initialization and schema fixes, closes #111 2018-06-01 22:26:37 -04:00
217 changed files with 10307 additions and 6615 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
npm-debug.log
dist
.idea

3
.gitignore vendored
View File

@@ -6,4 +6,5 @@ yarn-error.log
*.db
config.ini
cert.key
cert.crt
cert.crt
docs/

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataSource name="document.db">
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.9">
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.11">
<root id="1">
<ServerVersion>3.16.1</ServerVersion>
</root>
@@ -12,10 +12,10 @@
<collation id="4" parent="1" name="NOCASE"/>
<collation id="5" parent="1" name="RTRIM"/>
<table id="6" parent="2" name="api_tokens"/>
<table id="7" parent="2" name="branches"/>
<table id="8" parent="2" name="event_log"/>
<table id="9" parent="2" name="images"/>
<table id="10" parent="2" name="labels"/>
<table id="7" parent="2" name="attributes"/>
<table id="8" parent="2" name="branches"/>
<table id="9" parent="2" name="event_log"/>
<table id="10" parent="2" name="images"/>
<table id="11" parent="2" name="note_images"/>
<table id="12" parent="2" name="note_revisions"/>
<table id="13" parent="2" name="notes"/>
@@ -59,7 +59,6 @@
<index id="25" parent="6" name="sqlite_autoindex_api_tokens_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>apiTokenId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="26" parent="6">
@@ -67,7 +66,7 @@
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName>
</key>
<column id="27" parent="7" name="branchId">
<column id="27" parent="7" name="attributeId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
@@ -77,589 +76,574 @@
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="29" parent="7" name="parentNoteId">
<column id="29" parent="7" name="type">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="30" parent="7" name="notePosition">
<column id="30" parent="7" name="name">
<Position>4</Position>
<DataType>INTEGER|0s</DataType>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="31" parent="7" name="prefix">
<column id="31" parent="7" name="value">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="32" parent="7" name="isExpanded">
<column id="32" parent="7" name="position">
<Position>6</Position>
<DataType>BOOLEAN|0s</DataType>
</column>
<column id="33" parent="7" name="isDeleted">
<Position>7</Position>
<DataType>INTEGER|0s</DataType>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="33" parent="7" name="dateCreated">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="34" parent="7" name="dateModified">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="35" parent="7" name="hash">
<column id="35" parent="7" name="isDeleted">
<Position>9</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="36" parent="7" name="hash">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="37" parent="7" name="isInheritable">
<Position>11</Position>
<DataType>int|0s</DataType>
<DefaultExpression>0</DefaultExpression>
</column>
<index id="38" parent="7" name="sqlite_autoindex_attributes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>attributeId</ColNames>
<Unique>1</Unique>
</index>
<key id="39" parent="7">
<ColNames>attributeId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_attributes_1</UnderlyingIndexName>
</key>
<column id="40" parent="8" name="branchId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="41" parent="8" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="42" parent="8" name="parentNoteId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="43" parent="8" name="notePosition">
<Position>4</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="44" parent="8" name="prefix">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="45" parent="8" name="isExpanded">
<Position>6</Position>
<DataType>BOOLEAN|0s</DataType>
</column>
<column id="46" parent="8" name="isDeleted">
<Position>7</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="47" parent="8" name="dateModified">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="48" parent="8" name="hash">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="36" parent="7" name="dateCreated">
<column id="49" parent="8" name="dateCreated">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;1970-01-01T00:00:00.000Z&apos;</DefaultExpression>
</column>
<index id="37" parent="7" name="sqlite_autoindex_branches_1">
<index id="50" parent="8" name="sqlite_autoindex_branches_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="38" parent="7" name="IDX_branches_noteId_parentNoteId">
<index id="51" parent="8" name="IDX_branches_noteId_parentNoteId">
<ColNames>noteId
parentNoteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="39" parent="7" name="IDX_branches_noteId">
<index id="52" parent="8" name="IDX_branches_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="40" parent="7" name="IDX_branches_parentNoteId">
<index id="53" parent="8" name="IDX_branches_parentNoteId">
<ColNames>parentNoteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="41" parent="7">
<key id="54" parent="8">
<ColNames>branchId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
</key>
<column id="42" parent="8" name="id">
<column id="55" parent="9" name="eventId">
<Position>1</Position>
<DataType>INTEGER|0s</DataType>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="43" parent="8" name="noteId">
<column id="56" parent="9" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="44" parent="8" name="comment">
<column id="57" parent="9" name="comment">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="45" parent="8" name="dateCreated">
<column id="58" parent="9" name="dateCreated">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<key id="46" parent="8">
<ColNames>id</ColNames>
<index id="59" parent="9" name="sqlite_autoindex_event_log_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>eventId</ColNames>
<Unique>1</Unique>
</index>
<key id="60" parent="9">
<ColNames>eventId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_event_log_1</UnderlyingIndexName>
</key>
<column id="47" parent="9" name="imageId">
<column id="61" parent="10" name="imageId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="48" parent="9" name="format">
<column id="62" parent="10" name="format">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="49" parent="9" name="checksum">
<column id="63" parent="10" name="checksum">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="50" parent="9" name="name">
<column id="64" parent="10" name="name">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="51" parent="9" name="data">
<column id="65" parent="10" name="data">
<Position>5</Position>
<DataType>BLOB|0s</DataType>
</column>
<column id="52" parent="9" name="isDeleted">
<column id="66" parent="10" name="isDeleted">
<Position>6</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="53" parent="9" name="dateModified">
<column id="67" parent="10" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="54" parent="9" name="dateCreated">
<column id="68" parent="10" name="dateCreated">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="55" parent="9" name="hash">
<column id="69" parent="10" name="hash">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="56" parent="9" name="sqlite_autoindex_images_1">
<index id="70" parent="10" name="sqlite_autoindex_images_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>imageId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="57" parent="9">
<key id="71" parent="10">
<ColNames>imageId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
</key>
<column id="58" parent="10" name="labelId">
<column id="72" parent="11" name="noteImageId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="59" parent="10" name="noteId">
<column id="73" parent="11" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="60" parent="10" name="name">
<column id="74" parent="11" name="imageId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="61" parent="10" name="value">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="62" parent="10" name="position">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="63" parent="10" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="64" parent="10" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="65" parent="10" name="isDeleted">
<Position>8</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="66" parent="10" name="hash">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="67" parent="10" name="sqlite_autoindex_labels_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>labelId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="68" parent="10" name="IDX_labels_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="69" parent="10" name="IDX_labels_name_value">
<ColNames>name
value</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="70" parent="10">
<ColNames>labelId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
</key>
<column id="71" parent="11" name="noteImageId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="72" parent="11" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="73" parent="11" name="imageId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="74" parent="11" name="isDeleted">
<column id="75" parent="11" name="isDeleted">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="75" parent="11" name="dateModified">
<column id="76" parent="11" name="dateModified">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="76" parent="11" name="dateCreated">
<column id="77" parent="11" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="77" parent="11" name="hash">
<column id="78" parent="11" name="hash">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="78" parent="11" name="sqlite_autoindex_note_images_1">
<index id="79" parent="11" name="sqlite_autoindex_note_images_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteImageId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="79" parent="11" name="IDX_note_images_noteId_imageId">
<index id="80" parent="11" name="IDX_note_images_noteId_imageId">
<ColNames>noteId
imageId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="80" parent="11" name="IDX_note_images_noteId">
<index id="81" parent="11" name="IDX_note_images_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="81" parent="11" name="IDX_note_images_imageId">
<index id="82" parent="11" name="IDX_note_images_imageId">
<ColNames>imageId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="82" parent="11">
<key id="83" parent="11">
<ColNames>noteImageId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
</key>
<column id="83" parent="12" name="noteRevisionId">
<column id="84" parent="12" name="noteRevisionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="84" parent="12" name="noteId">
<column id="85" parent="12" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="85" parent="12" name="title">
<column id="86" parent="12" name="title">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="86" parent="12" name="content">
<column id="87" parent="12" name="content">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="87" parent="12" name="isProtected">
<column id="88" parent="12" name="isProtected">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="88" parent="12" name="dateModifiedFrom">
<column id="89" parent="12" name="dateModifiedFrom">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="89" parent="12" name="dateModifiedTo">
<column id="90" parent="12" name="dateModifiedTo">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="90" parent="12" name="type">
<column id="91" parent="12" name="type">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="91" parent="12" name="mime">
<column id="92" parent="12" name="mime">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="92" parent="12" name="hash">
<column id="93" parent="12" name="hash">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="93" parent="12" name="sqlite_autoindex_note_revisions_1">
<index id="94" parent="12" name="sqlite_autoindex_note_revisions_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="94" parent="12" name="IDX_note_revisions_noteId">
<index id="95" parent="12" name="IDX_note_revisions_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="95" parent="12" name="IDX_note_revisions_dateModifiedFrom">
<index id="96" parent="12" name="IDX_note_revisions_dateModifiedFrom">
<ColNames>dateModifiedFrom</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="96" parent="12" name="IDX_note_revisions_dateModifiedTo">
<index id="97" parent="12" name="IDX_note_revisions_dateModifiedTo">
<ColNames>dateModifiedTo</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="97" parent="12">
<key id="98" parent="12">
<ColNames>noteRevisionId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
</key>
<column id="98" parent="13" name="noteId">
<column id="99" parent="13" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="99" parent="13" name="title">
<column id="100" parent="13" name="title">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;unnamed&quot;</DefaultExpression>
</column>
<column id="100" parent="13" name="content">
<column id="101" parent="13" name="content">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="101" parent="13" name="isProtected">
<column id="102" parent="13" name="isProtected">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="102" parent="13" name="isDeleted">
<column id="103" parent="13" name="isDeleted">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="103" parent="13" name="dateCreated">
<column id="104" parent="13" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="104" parent="13" name="dateModified">
<column id="105" parent="13" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="105" parent="13" name="type">
<column id="106" parent="13" name="type">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text&apos;</DefaultExpression>
</column>
<column id="106" parent="13" name="mime">
<column id="107" parent="13" name="mime">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text/html&apos;</DefaultExpression>
</column>
<column id="107" parent="13" name="hash">
<column id="108" parent="13" name="hash">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="108" parent="13" name="sqlite_autoindex_notes_1">
<index id="109" parent="13" name="sqlite_autoindex_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="109" parent="13" name="IDX_notes_type">
<index id="110" parent="13" name="IDX_notes_type">
<ColNames>type</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="110" parent="13">
<key id="111" parent="13">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
</key>
<column id="111" parent="14" name="name">
<column id="112" parent="14" name="name">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="112" parent="14" name="value">
<column id="113" parent="14" name="value">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="113" parent="14" name="dateModified">
<column id="114" parent="14" name="dateModified">
<Position>3</Position>
<DataType>INT|0s</DataType>
</column>
<column id="114" parent="14" name="isSynced">
<column id="115" parent="14" name="isSynced">
<Position>4</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="115" parent="14" name="hash">
<column id="116" parent="14" name="hash">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="116" parent="14" name="dateCreated">
<column id="117" parent="14" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;1970-01-01T00:00:00.000Z&apos;</DefaultExpression>
</column>
<index id="117" parent="14" name="sqlite_autoindex_options_1">
<index id="118" parent="14" name="sqlite_autoindex_options_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>name</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="118" parent="14">
<key id="119" parent="14">
<ColNames>name</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
</key>
<column id="119" parent="15" name="branchId">
<column id="120" parent="15" name="branchId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="120" parent="15" name="notePath">
<column id="121" parent="15" name="notePath">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="121" parent="15" name="dateCreated">
<column id="122" parent="15" name="hash">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="122" parent="15" name="isDeleted">
<Position>4</Position>
<DataType>INT|0s</DataType>
</column>
<column id="123" parent="15" name="hash">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="124" parent="15" name="sqlite_autoindex_recent_notes_1">
<column id="123" parent="15" name="dateCreated">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="124" parent="15" name="isDeleted">
<Position>5</Position>
<DataType>INT|0s</DataType>
</column>
<index id="125" parent="15" name="sqlite_autoindex_recent_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="125" parent="15">
<key id="126" parent="15">
<ColNames>branchId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
</key>
<column id="126" parent="16" name="sourceId">
<column id="127" parent="16" name="sourceId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="127" parent="16" name="dateCreated">
<column id="128" parent="16" name="dateCreated">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="128" parent="16" name="sqlite_autoindex_source_ids_1">
<index id="129" parent="16" name="sqlite_autoindex_source_ids_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>sourceId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="129" parent="16">
<key id="130" parent="16">
<ColNames>sourceId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
</key>
<column id="130" parent="17" name="type">
<column id="131" parent="17" name="type">
<Position>1</Position>
<DataType>text|0s</DataType>
</column>
<column id="131" parent="17" name="name">
<column id="132" parent="17" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
</column>
<column id="132" parent="17" name="tbl_name">
<column id="133" parent="17" name="tbl_name">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="133" parent="17" name="rootpage">
<column id="134" parent="17" name="rootpage">
<Position>4</Position>
<DataType>integer|0s</DataType>
</column>
<column id="134" parent="17" name="sql">
<column id="135" parent="17" name="sql">
<Position>5</Position>
<DataType>text|0s</DataType>
</column>
<column id="135" parent="18" name="name">
<column id="136" parent="18" name="name">
<Position>1</Position>
</column>
<column id="136" parent="18" name="seq">
<column id="137" parent="18" name="seq">
<Position>2</Position>
</column>
<column id="137" parent="19" name="id">
<column id="138" parent="19" name="id">
<Position>1</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="138" parent="19" name="entityName">
<column id="139" parent="19" name="entityName">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="139" parent="19" name="entityId">
<column id="140" parent="19" name="entityId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="140" parent="19" name="sourceId">
<column id="141" parent="19" name="sourceId">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="141" parent="19" name="syncDate">
<column id="142" parent="19" name="syncDate">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="142" parent="19" name="IDX_sync_entityName_entityId">
<index id="143" parent="19" name="IDX_sync_entityName_entityId">
<ColNames>entityName
entityId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="143" parent="19" name="IDX_sync_syncDate">
<index id="144" parent="19" name="IDX_sync_syncDate">
<ColNames>syncDate</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="144" parent="19">
<key id="145" parent="19">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>

6
.idea/sqldialects.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="SQLite" />
</component>
</project>

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM node:8.11.2
RUN apt-get update && apt-get install -y nasm
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install --production
# If you are building your code for production
# RUN npm install --only=production
# Bundle app source
COPY . .
EXPOSE 8080
CMD [ "node", "src/www" ]

8
bin/build-docker.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
if [[ $# -eq 0 ]] ; then
echo "Missing argument of new version"
exit 1
fi
sudo docker build -t zadam/trilium:latest -t zadam/trilium:$1 .

23
bin/build-pkg.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
if [[ $# -eq 0 ]] ; then
echo "Missing argument of new version"
exit 1
fi
VERSION=$1
PKG_DIR=dist/trilium-linux-x64-server
mkdir $PKG_DIR
pkg . --targets node8-linux-x64 --output ${PKG_DIR}/trilium
chmod +x ${PKG_DIR}/trilium
cp node_modules/sqlite3/lib/binding/node-v57-linux-x64/node_sqlite3.node ${PKG_DIR}/
cp node_modules/scrypt/build/Release/scrypt.node ${PKG_DIR}/
cd dist
7z a trilium-linux-x64-${VERSION}-server.7z trilium-linux-x64-server

9
bin/push-docker-image.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
if [[ $# -eq 0 ]] ; then
echo "Missing argument of new version"
exit 1
fi
sudo docker push zadam/trilium:latest
sudo docker push zadam/trilium:$1

View File

@@ -47,6 +47,7 @@ bin/package.sh
LINUX_X64_BUILD=trilium-linux-x64-$VERSION.7z
LINUX_IA32_BUILD=trilium-linux-ia32-$VERSION.7z
WINDOWS_X64_BUILD=trilium-windows-x64-$VERSION.7z
SERVER_BUILD=trilium-linux-x64-server-$VERSION.7z
echo "Creating release in GitHub"
@@ -75,4 +76,21 @@ github-release upload \
--name "$WINDOWS_X64_BUILD" \
--file "dist/$WINDOWS_X64_BUILD"
echo "Packaging server version"
bin/build-pkg.sh $VERSION
github-release upload \
--tag $TAG \
--name "$SERVER_BUILD" \
--file "dist/$SERVER_BUILD"
echo "Building docker image"
bin/build-docker.sh $VERSION
echo "Pushing docker image to dockerhub"
bin/push-docker-image.sh $VERSION
echo "Release finished!"

View File

@@ -3,15 +3,10 @@
instanceName=
[Network]
# port setting is relevant only for web deployments, desktop builds run on random free port
port=8080
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
https=false
# path to certificate (run "bash generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
# path to certificate (run "bash bin/generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
certPath=
keyPath=
[Sync]
syncServerHost=
syncServerTimeout=10000
syncProxy=
syncServerCertificate=

View File

@@ -0,0 +1,2 @@
INSERT INTO options (optionId, name, value, dateCreated, dateModified, isSynced)
VALUES ('zoomFactor_key', 'zoomFactor', '1.0', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);

View File

@@ -0,0 +1 @@
UPDATE labels SET name = 'archived' WHERE name = 'hideInAutocomplete'

View File

@@ -0,0 +1,2 @@
INSERT INTO options (optionId, name, value, dateCreated, dateModified, isSynced)
VALUES ('theme_key', 'theme', 'white', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);

View File

@@ -0,0 +1,15 @@
create table options_mig
(
name TEXT not null PRIMARY KEY,
value TEXT,
dateModified INT,
isSynced INTEGER default 0 not null,
hash TEXT default "" not null,
dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
);
INSERT INTO options_mig (name, value, dateModified, isSynced, hash, dateCreated)
SELECT name, value, dateModified, isSynced, hash, dateCreated FROM options;
DROP TABLE options;
ALTER TABLE options_mig RENAME TO options;

View File

@@ -0,0 +1,8 @@
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('syncServerHost', '', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('syncServerTimeout', '5000', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('syncProxy', '', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);

View File

@@ -0,0 +1,2 @@
DELETE FROM sync WHERE entityName = 'note_tree';
DELETE FROM sync WHERE entityName = 'attributes';

View File

@@ -0,0 +1,2 @@
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('initialized', 'true', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);

View File

@@ -0,0 +1,4 @@
const syncTableService = require('../../src/services/sync_table');
// options has not been filled so far which caused problems with clean-slate sync.
module.exports = async () => await syncTableService.fillAllSyncRows();

View File

@@ -0,0 +1,2 @@
UPDATE notes SET content = '' WHERE isDeleted = 1;
UPDATE note_revisions SET content = '' WHERE (SELECT isDeleted FROM notes WHERE noteId = note_revisions.noteId) = 1;

View File

@@ -0,0 +1,15 @@
CREATE TABLE relations
(
relationId TEXT not null primary key,
sourceNoteId TEXT not null,
name TEXT not null,
targetNoteId TEXT not null,
position INT default 0 not null,
dateCreated TEXT not null,
dateModified TEXT not null,
isDeleted INT not null
, hash TEXT DEFAULT "" NOT NULL);
CREATE INDEX IDX_relation_sourceNoteId
on relations (sourceNoteId);
CREATE INDEX IDX_relation_targetNoteId
on relations (targetNoteId);

View File

@@ -0,0 +1 @@
ALTER TABLE relations ADD isInheritable int DEFAULT 0 NULL;

View File

@@ -0,0 +1,9 @@
UPDATE options SET name = 'lastDailyBackupDate' WHERE name = 'lastBackupDate';
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('lastWeeklyBackupDate', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', 0);
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('lastMonthlyBackupDate', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', 0);
-- these options are not synced so no need to fix sync rows

View File

@@ -0,0 +1,27 @@
create table attributes
(
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
dateCreated TEXT not null,
dateModified TEXT not null,
isDeleted INT not null,
hash TEXT default "" not null);
create index IDX_attributes_name_value
on attributes (name, value);
create index IDX_attributes_value
on attributes (value);
create index IDX_attributes_noteId
on attributes (noteId);
INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash)
SELECT labelId, noteId, 'label', name, value, position, dateCreated, dateModified, isDeleted, hash FROM labels;
INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash)
SELECT relationId, sourceNoteId, 'relation', name, targetNoteId, position, dateCreated, dateModified, isDeleted, hash FROM relations;

View File

@@ -0,0 +1 @@
ALTER TABLE attributes ADD isInheritable int DEFAULT 0 NULL;

View File

@@ -0,0 +1,4 @@
DROP TABLE relations;
DROP TABLE labels;
DELETE FROM sync WHERE entityName = 'relations' OR entityName = 'labels';

View File

@@ -0,0 +1 @@
UPDATE attributes SET name = 'template' WHERE name = 'inheritAttributes';

View File

@@ -1,8 +1,3 @@
CREATE TABLE IF NOT EXISTS "options" (
`name` TEXT NOT NULL PRIMARY KEY,
`value` TEXT,
`dateModified` INT,
isSynced INTEGER NOT NULL DEFAULT 0);
CREATE TABLE IF NOT EXISTS "sync" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
@@ -29,7 +24,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (
`isProtected` INT NOT NULL DEFAULT 0,
`dateModifiedFrom` TEXT NOT NULL,
`dateModifiedTo` TEXT NOT NULL
, type TEXT DEFAULT '' NOT NULL, mime TEXT DEFAULT '' NOT NULL);
, type TEXT DEFAULT '' NOT NULL, mime TEXT DEFAULT '' NOT NULL, hash TEXT DEFAULT "" NOT NULL);
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
`noteId`
);
@@ -49,7 +44,7 @@ CREATE TABLE IF NOT EXISTS "images"
isDeleted INT NOT NULL DEFAULT 0,
dateModified TEXT NOT NULL,
dateCreated TEXT NOT NULL
);
, hash TEXT DEFAULT "" NOT NULL);
CREATE TABLE note_images
(
noteImageId TEXT PRIMARY KEY NOT NULL,
@@ -58,7 +53,7 @@ CREATE TABLE note_images
isDeleted INT NOT NULL DEFAULT 0,
dateModified TEXT NOT NULL,
dateCreated TEXT NOT NULL
);
, hash TEXT DEFAULT "" NOT NULL);
CREATE INDEX IDX_note_images_noteId ON note_images (noteId);
CREATE INDEX IDX_note_images_imageId ON note_images (imageId);
CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId);
@@ -68,7 +63,7 @@ CREATE TABLE IF NOT EXISTS "api_tokens"
token TEXT NOT NULL,
dateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0
);
, hash TEXT DEFAULT "" NOT NULL);
CREATE TABLE IF NOT EXISTS "branches" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
@@ -77,7 +72,7 @@ CREATE TABLE IF NOT EXISTS "branches" (
`prefix` TEXT,
`isExpanded` BOOLEAN,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`dateModified` TEXT NOT NULL,
`dateModified` TEXT NOT NULL, hash TEXT DEFAULT "" NOT NULL, dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z',
PRIMARY KEY(`branchId`)
);
CREATE INDEX `IDX_branches_noteId` ON `branches` (
@@ -87,34 +82,6 @@ CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
`noteId`,
`parentNoteId`
);
CREATE TABLE IF NOT EXISTS "recent_notes" (
`branchId` TEXT NOT NULL PRIMARY KEY,
`notePath` TEXT NOT NULL,
`dateAccessed` TEXT NOT NULL,
isDeleted INT
);
CREATE TABLE labels
(
labelId TEXT not null primary key,
noteId TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
dateCreated TEXT not null,
dateModified TEXT not null,
isDeleted INT not null
);
CREATE INDEX IDX_labels_name_value
on labels (name, value);
CREATE INDEX IDX_labels_noteId
on labels (noteId);
CREATE TABLE IF NOT EXISTS "event_log"
(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
noteId TEXT,
comment TEXT,
dateAdded TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "unnamed",
@@ -124,9 +91,43 @@ CREATE TABLE IF NOT EXISTS "notes" (
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
mime TEXT NOT NULL DEFAULT 'text/html', hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`noteId`)
);
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
CREATE INDEX IDX_notes_type
on notes (type);
CREATE TABLE IF NOT EXISTS "recent_notes" (
`branchId` TEXT NOT NULL PRIMARY KEY,
`notePath` TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
`dateCreated` TEXT NOT NULL,
isDeleted INT
);
CREATE TABLE IF NOT EXISTS "event_log" (
`eventId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT,
`comment` TEXT,
`dateCreated` TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "options"
(
name TEXT not null PRIMARY KEY,
value TEXT,
dateModified INT,
isSynced INTEGER default 0 not null,
hash TEXT default "" not null,
dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
);
CREATE TABLE attributes
(
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
dateCreated TEXT not null,
dateModified TEXT not null,
isDeleted INT not null,
hash TEXT default "" not null, isInheritable int DEFAULT 0 NULL);

View File

@@ -2,9 +2,9 @@
const electron = require('electron');
const path = require('path');
const config = require('./src/services/config');
const log = require('./src/services/log');
const url = require("url");
const port = require('./src/services/port');
const app = electron.app;
const globalShortcut = electron.globalShortcut;
@@ -23,7 +23,7 @@ function onClosed() {
mainWindow = null;
}
function createMainWindow() {
async function createMainWindow() {
const win = new electron.BrowserWindow({
width: 1200,
height: 900,
@@ -31,14 +31,12 @@ function createMainWindow() {
icon: path.join(__dirname, 'src/public/images/app-icons/png/256x256.png')
});
const port = config['Network']['port'] || '3000';
win.setMenu(null);
win.loadURL('http://localhost:' + port);
win.loadURL('http://localhost:' + await port);
win.on('closed', onClosed);
win.webContents.on('new-window', (e, url) => {
if (url !== mainWindow.webContents.getURL()) {
if (url !== win.webContents.getURL()) {
e.preventDefault();
require('electron').shell.openExternal(url);
}

8422
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,12 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.14.0",
"version": "0.20.0",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
"trilium": "./src/www"
},
"repository": {
"type": "git",
"url": "https://github.com/zadam/trilium.git"
@@ -17,60 +20,65 @@
"start-forge": "electron-forge start",
"package-forge": "electron-forge package",
"make-forge": "electron-forge make",
"publish-forge": "electron-forge publish"
"publish-forge": "electron-forge publish",
"build-backend-docs": "jsdoc -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
"build-frontend-docs": "jsdoc -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js",
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs"
},
"dependencies": {
"async-mutex": "^0.1.3",
"axios": "^0.18",
"body-parser": "^1.18.3",
"cls-hooked": "^4.2.2",
"cookie-parser": "~1.4.3",
"debug": "~3.1.0",
"devtron": "^1.4.0",
"ejs": "~2.6.1",
"electron-debug": "^1.5.0",
"electron-dl": "^1.12.0",
"electron-in-page-search": "^1.3.2",
"express": "~4.16.3",
"express-session": "^1.15.6",
"fs-extra": "^6.0.1",
"helmet": "^3.12.1",
"html": "^1.0.0",
"image-type": "^3.0.0",
"imagemin": "^5.3.1",
"imagemin-giflossy": "^5.1.10",
"imagemin-mozjpeg": "^7.0.0",
"imagemin-pngquant": "^5.1.0",
"ini": "^1.3.5",
"jimp": "^0.2.28",
"moment": "^2.22.1",
"multer": "^1.3.0",
"async-mutex": "0.1.3",
"axios": "0.18",
"body-parser": "1.18.3",
"cls-hooked": "4.2.2",
"cookie-parser": "1.4.3",
"debug": "3.1.0",
"devtron": "1.4.0",
"ejs": "2.6.1",
"electron-debug": "2.0.0",
"electron-dl": "1.12.0",
"electron-in-page-search": "1.3.2",
"express": "4.16.3",
"express-session": "1.15.6",
"fs-extra": "7.0.0",
"get-port": "4.0.0",
"helmet": "3.13.0",
"html": "1.0.0",
"image-type": "3.0.0",
"imagemin": "6.0.0",
"imagemin-giflossy": "5.1.10",
"imagemin-mozjpeg": "7.0.0",
"imagemin-pngquant": "6.0.0",
"ini": "1.3.5",
"jimp": "0.3.5",
"moment": "2.22.2",
"multer": "1.3.1",
"open": "0.0.5",
"rand-token": "^0.4.0",
"rcedit": "^1.1.0",
"request": "^2.87.0",
"request-promise": "^4.2.2",
"rimraf": "^2.6.2",
"sanitize-filename": "^1.6.1",
"scrypt": "^6.0.3",
"serve-favicon": "~2.5.0",
"session-file-store": "^1.2.0",
"simple-node-logger": "^0.93.37",
"sqlite": "^2.9.2",
"tar-stream": "^1.6.1",
"unescape": "^1.0.1",
"ws": "^5.2.0",
"xml2js": "^0.4.19"
"rand-token": "0.4.0",
"rcedit": "1.1.0",
"request": "2.88.0",
"request-promise": "4.2.2",
"rimraf": "2.6.2",
"sanitize-filename": "1.6.1",
"scrypt": "6.0.3",
"serve-favicon": "2.5.0",
"session-file-store": "1.2.0",
"simple-node-logger": "0.93.37",
"sqlite": "3.0.0",
"tar-stream": "1.6.1",
"unescape": "1.0.1",
"ws": "6.0.0",
"xml2js": "0.4.19"
},
"devDependencies": {
"electron": "^2.0.1",
"electron-compile": "^6.4.2",
"electron-packager": "^12.1.0",
"electron-prebuilt-compile": "2.0.0",
"electron-rebuild": "^1.7.3",
"lorem-ipsum": "^1.0.4",
"tape": "^4.9.0",
"xo": "^0.21.1"
"electron": "2.0.7",
"electron-compile": "6.4.3",
"electron-packager": "12.1.1",
"electron-prebuilt-compile": "2.0.7",
"electron-rebuild": "1.8.2",
"lorem-ipsum": "1.0.5",
"tape": "4.9.1",
"xo": "0.22.0",
"pkg": "4.3.4"
},
"config": {
"forge": {
@@ -109,5 +117,15 @@
"node",
"browser"
]
},
"pkg": {
"assets": [
"./db/**/*",
"./src/public/**/*",
"./src/views/**/*",
"./node_modules/mozjpeg/vendor/*",
"./node_modules/pngquant-bin/vendor/*",
"./node_modules/giflossy/vendor/*"
]
}
}

View File

@@ -11,6 +11,7 @@ const os = require('os');
const sessionSecret = require('./services/session_secret');
const cls = require('./services/cls');
require('./entities/entity_constructor');
require('./services/handlers');
const app = express();
@@ -47,7 +48,7 @@ const sessionParser = session({
cookie: {
// path: "/",
httpOnly: true,
maxAge: 1800000
maxAge: 24 * 60 * 60 * 1000 // in milliseconds
},
store: new FileStore({
ttl: 30 * 24 * 3600,

View File

@@ -3,14 +3,22 @@
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
/**
* ApiToken is an entity representing token used to authenticate against Trilium API from client applications. Currently used only by Trilium Sender.
*
* @param {string} apiTokenId - primary key
* @param {string} token
* @param {boolean} isDeleted - true if API token is deleted
* @param {string} dateCreated
*
* @extends Entity
*/
class ApiToken extends Entity {
static get tableName() { return "api_tokens"; }
static get entityName() { return "api_tokens"; }
static get primaryKeyName() { return "apiTokenId"; }
static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
beforeSaving() {
super.beforeSaving();
if (!this.isDeleted) {
this.isDeleted = false;
}
@@ -18,6 +26,8 @@ class ApiToken extends Entity {
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
super.beforeSaving();
}
}

93
src/entities/attribute.js Normal file
View File

@@ -0,0 +1,93 @@
"use strict";
const Entity = require('./entity');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
const sql = require('../services/sql');
/**
* Attribute is key value pair owned by a note.
*
* @param {string} attributeId
* @param {string} noteId
* @param {string} type
* @param {string} name
* @param {string} value
* @param {int} position
* @param {boolean} isInheritable
* @param {boolean} isDeleted
* @param {string} dateCreated
* @param {string} dateModified
*
* @extends Entity
*/
class Attribute extends Entity {
static get entityName() { return "attributes"; }
static get primaryKeyName() { return "attributeId"; }
static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable", "isDeleted", "dateCreated"]; }
constructor(row) {
super(row);
this.isInheritable = !!this.isInheritable;
if (this.isDefinition()) {
try {
this.value = JSON.parse(this.value);
}
catch (e) {
}
}
}
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
}
async getTargetNote() {
if (this.type !== 'relation') {
throw new Error(`Attribute ${this.attributeId} is not relation`);
}
if (!this.value) {
return null;
}
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.value]);
}
isDefinition() {
return this.type === 'label-definition' || this.type === 'relation-definition';
}
async beforeSaving() {
if (!this.value) {
// null value isn't allowed
this.value = "";
}
if (this.position === undefined) {
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [this.noteId]);
}
if (!this.isInheritable) {
this.isInheritable = false;
}
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}
module.exports = Attribute;

View File

@@ -5,19 +5,33 @@ const dateUtils = require('../services/date_utils');
const repository = require('../services/repository');
const sql = require('../services/sql');
/**
* Branch represents note's placement in the tree - it's essentially pair of noteId and parentNoteId.
* Each note can have multiple (at least one) branches, meaning it can be placed into multiple places in the tree.
*
* @param {string} branchId - primary key
* @param {string} noteId
* @param {string} parentNoteId
* @param {int} notePosition
* @param {string} prefix
* @param {boolean} isExpanded
* @param {boolean} isDeleted
* @param {string} dateModified
* @param {string} dateCreated
*
* @extends Entity
*/
class Branch extends Entity {
static get tableName() { return "branches"; }
static get entityName() { return "branches"; }
static get primaryKeyName() { return "branchId"; }
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "dateModified", "isDeleted", "prefix"]; }
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "isDeleted", "prefix"]; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
}
async beforeSaving() {
super.beforeSaving();
if (this.notePosition === undefined) {
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 1;
@@ -31,7 +45,11 @@ class Branch extends Entity {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}

View File

@@ -1,13 +1,19 @@
"use strict";
const utils = require('../services/utils');
const repository = require('../services/repository');
class Entity {
/**
* @param {object} [row] - database row representing given entity
*/
constructor(row = {}) {
for (const key in row) {
this[key] = row[key];
}
if ('isDeleted' in this) {
this.isDeleted = !!this.isDeleted;
}
}
beforeSaving() {
@@ -15,20 +21,25 @@ class Entity {
this[this.constructor.primaryKeyName] = utils.newEntityId();
}
const origHash = this.hash;
this.hash = this.generateHash();
this.isChanged = origHash !== this.hash;
}
generateHash() {
let contentToHash = "";
for (const propertyName of this.constructor.hashedProperties) {
contentToHash += "|" + this[propertyName];
}
// this IF is to ease the migration from before hashed options, can be later removed
if (this.constructor.tableName !== 'options' || this.isSynced) {
this["hash"] = utils.hash(contentToHash).substr(0, 10);
}
return utils.hash(contentToHash).substr(0, 10);
}
async save() {
await repository.updateEntity(this);
await require('../services/repository').updateEntity(this);
return this;
}

View File

@@ -3,17 +3,37 @@ const NoteRevision = require('../entities/note_revision');
const Image = require('../entities/image');
const NoteImage = require('../entities/note_image');
const Branch = require('../entities/branch');
const Label = require('../entities/label');
const Attribute = require('../entities/attribute');
const RecentNote = require('../entities/recent_note');
const ApiToken = require('../entities/api_token');
const Option = require('../entities/option');
const repository = require('../services/repository');
const ENTITY_NAME_TO_ENTITY = {
"attributes": Attribute,
"images": Image,
"note_images": NoteImage,
"branches": Branch,
"notes": Note,
"note_revisions": NoteRevision,
"recent_notes": RecentNote,
"options": Option,
"api_tokens": ApiToken
};
function getEntityFromEntityName(entityName) {
if (!(entityName in ENTITY_NAME_TO_ENTITY)) {
throw new Error(`Entity for table ${entityName} not found!`);
}
return ENTITY_NAME_TO_ENTITY[entityName];
}
function createEntityFromRow(row) {
let entity;
if (row.labelId) {
entity = new Label(row);
if (row.attributeId) {
entity = new Attribute(row);
}
else if (row.noteRevisionId) {
entity = new NoteRevision(row);
@@ -46,8 +66,9 @@ function createEntityFromRow(row) {
return entity;
}
repository.setEntityConstructor(createEntityFromRow);
module.exports = {
createEntityFromRow
};
createEntityFromRow,
getEntityFromEntityName
};
repository.setEntityConstructor(module.exports);

View File

@@ -3,14 +3,26 @@
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
/**
* This class represents image data.
*
* @param {string} imageId
* @param {string} format
* @param {string} checksum
* @param {string} name
* @param {blob} data
* @param {boolean} isDeleted
* @param {string} dateModified
* @param {string} dateCreated
*
* @extends Entity
*/
class Image extends Entity {
static get tableName() { return "images"; }
static get entityName() { return "images"; }
static get primaryKeyName() { return "imageId"; }
static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateModified", "dateCreated"]; }
static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateCreated"]; }
beforeSaving() {
super.beforeSaving();
if (!this.isDeleted) {
this.isDeleted = false;
}
@@ -19,7 +31,11 @@ class Image extends Entity {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}

View File

@@ -1,41 +0,0 @@
"use strict";
const Entity = require('./entity');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
const sql = require('../services/sql');
class Label extends Entity {
static get tableName() { return "labels"; }
static get primaryKeyName() { return "labelId"; }
static get hashedProperties() { return ["labelId", "noteId", "name", "value", "dateModified", "dateCreated"]; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
}
async beforeSaving() {
super.beforeSaving();
if (!this.value) {
// null value isn't allowed
this.value = "";
}
if (this.position === undefined) {
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM labels WHERE noteId = ?`, [this.noteId]);
}
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
}
}
module.exports = Label;

View File

@@ -1,18 +1,42 @@
"use strict";
const Entity = require('./entity');
const Attribute = require('./attribute');
const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
class Note extends Entity {
static get tableName() { return "notes"; }
static get primaryKeyName() { return "noteId"; }
static get hashedProperties() { return ["noteId", "title", "content", "type", "dateModified", "isProtected", "isDeleted"]; }
const LABEL = 'label';
const RELATION = 'relation';
/**
* This represents a Note which is a central object in the Trilium Notes project.
*
* @property {string} noteId - primary key
* @property {string} type - one of "text", "code", "file" or "render"
* @property {string} mime - MIME type, e.g. "text/html"
* @property {string} title - note title
* @property {string} content - note content - e.g. HTML text for text notes, file payload for files
* @property {boolean} isProtected - true if note is protected
* @property {boolean} isDeleted - true if note is deleted
* @property {string} dateCreated
* @property {string} dateModified
*
* @extends Entity
*/
class Note extends Entity {
static get entityName() { return "notes"; }
static get primaryKeyName() { return "noteId"; }
static get hashedProperties() { return ["noteId", "title", "content", "type", "isProtected", "isDeleted"]; }
/**
* @param row - object containing database row from "notes" table
*/
constructor(row) {
super(row);
this.isProtected = !!this.isProtected;
// check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
if (this.isProtected && this.noteId) {
protectedSessionService.decryptNote(this);
@@ -30,19 +54,28 @@ class Note extends Entity {
catch(e) {}
}
/** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
isRoot() {
return this.noteId === 'root';
}
/** @returns {boolean} true if this note is of application/json content type */
isJson() {
return this.mime === "application/json";
}
/** @returns {boolean} true if this note is JavaScript (code or attachment) */
isJavaScript() {
return (this.type === "code" || this.type === "file")
&& (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript");
}
/** @returns {boolean} true if this note is HTML */
isHtml() {
return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
}
/** @returns {string} JS script environment - either "frontend" or "backend" */
getScriptEnv() {
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
return "frontend";
@@ -59,55 +92,385 @@ class Note extends Entity {
return null;
}
async getLabels() {
return await repository.getEntities("SELECT * FROM labels WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
/**
* @returns {Promise<Attribute[]>} attributes belonging to this specific note (excludes inherited attributes)
*/
async getOwnedAttributes() {
return await repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
}
// WARNING: this doesn't take into account the possibility to have multi-valued labels!
async getLabelMap() {
const map = {};
for (const label of await this.getLabels()) {
map[label.name] = label.value;
/** @returns {Promise<Attribute[]>} all note's attributes, including inherited ones */
async getAttributes() {
if (!this.__attributeCache) {
await this.loadAttributesToCache();
}
return map;
return this.__attributeCache;
}
async hasLabel(name) {
const map = await this.getLabelMap();
return map.hasOwnProperty(name);
/** @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones */
async getLabels() {
return (await this.getAttributes()).filter(attr => attr.type === LABEL);
}
// WARNING: this doesn't take into account the possibility to have multi-valued labels!
async getLabel(name) {
return await repository.getEntity("SELECT * FROM labels WHERE noteId = ? AND name = ?", [this.noteId, name]);
/** @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones */
async getRelations() {
return (await this.getAttributes()).filter(attr => attr.type === RELATION);
}
/**
* Clear note's attributes cache to force fresh reload for next attribute request.
* Cache is note instance scoped.
*/
invalidateAttributeCache() {
this.__attributeCache = null;
}
/** @returns {Promise<void>} */
async loadAttributesToCache() {
const attributes = await repository.getEntities(`
WITH RECURSIVE
tree(noteId, level) AS (
SELECT ?, 0
UNION
SELECT branches.parentNoteId, tree.level + 1 FROM branches
JOIN tree ON branches.noteId = tree.noteId
JOIN notes ON notes.noteId = branches.parentNoteId
WHERE notes.isDeleted = 0
AND branches.isDeleted = 0
),
treeWithAttrs(noteId, level) AS (
SELECT * FROM tree
UNION
SELECT attributes.value, treeWithAttrs.level + 1 FROM attributes
JOIN treeWithAttrs ON treeWithAttrs.noteId = attributes.noteId
WHERE attributes.isDeleted = 0
AND attributes.type = 'relation'
AND attributes.name = 'template'
AND (attributes.noteId = ? OR attributes.isInheritable = 1)
)
SELECT attributes.* FROM attributes JOIN treeWithAttrs ON attributes.noteId = treeWithAttrs.noteId
WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR attributes.noteId = ?)
ORDER BY level, noteId, position`, [this.noteId, this.noteId, this.noteId]);
// attributes are ordered so that "closest" attributes are first
// we order by noteId so that attributes from same note stay together. Actual noteId ordering doesn't matter.
const filteredAttributes = attributes.filter((attr, index) => {
if (attr.isDefinition()) {
const firstDefinitionIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name);
// keep only if this element is the first definition for this type & name
return firstDefinitionIndex === index;
}
else {
const definitionAttr = attributes.find(el => el.type === attr.type + '-definition' && el.name === attr.name);
if (!definitionAttr) {
return true;
}
const definition = definitionAttr.value;
if (definition.multiplicityType === 'multivalue') {
return true;
}
else {
const firstAttrIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name);
// in case of single-valued attribute we'll keep it only if it's first (closest)
return firstAttrIndex === index;
}
}
});
for (const attr of filteredAttributes) {
attr.isOwned = attr.noteId === this.noteId;
}
this.__attributeCache = filteredAttributes;
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<boolean>} true if note has an attribute with given type and name (including inherited)
*/
async hasAttribute(type, name) {
return !!await this.getAttribute(type, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<Attribute>} attribute of given type and name. If there's more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
*/
async getAttribute(type, name) {
const attributes = await this.getAttributes();
return attributes.find(attr => attr.type === type && attr.name === name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<string>} attribute value of given type and name or null if no such attribute exists.
*/
async getAttributeValue(type, name) {
const attr = await this.getAttribute(type, name);
return attr ? attr.value : null;
}
/**
* Based on enabled, attribute is either set or removed.
*
* @param {string} type - attribute type ('relation', 'label' etc.)
* @param {boolean} enabled - toggle On or Off
* @param {string} name - attribute name
* @param {string} [value] - attribute value (optional)
* @returns {Promise<void>}
*/
async toggleAttribute(type, enabled, name, value) {
if (enabled) {
await this.setAttribute(type, name, value);
}
else {
await this.removeAttribute(type, name, value);
}
}
/**
* Creates given attribute name-value pair if it doesn't exist.
*
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @param {string} [value] - attribute value (optional)
* @returns {Promise<void>}
*/
async setAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
let attr = attributes.find(attr => attr.type === type && (value === undefined || attr.value === value));
if (!attr) {
attr = new Attribute({
noteId: this.noteId,
type: type,
name: name,
value: value !== undefined ? value : ""
});
await attr.save();
this.invalidateAttributeCache();
}
}
/**
* Removes given attribute name-value pair if it exists.
*
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @param {string} [value] - attribute value (optional)
* @returns {Promise<void>}
*/
async removeAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
for (const attribute of attributes) {
if (attribute.type === type && (value === undefined || value === attribute.value)) {
attribute.isDeleted = true;
await attribute.save();
this.invalidateAttributeCache();
}
}
}
/**
* @param {string} name - label name
* @returns {Promise<boolean>} true if label exists (including inherited)
*/
async hasLabel(name) { return await this.hasAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<boolean>} true if relation exists (including inherited)
*/
async hasRelation(name) { return await this.hasAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise<Attribute>} label if it exists, null otherwise
*/
async getLabel(name) { return await this.getAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<Attribute>} relation if it exists, null otherwise
*/
async getRelation(name) { return await this.getAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise<string>} label value if label exists, null otherwise
*/
async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<string>} relation value if relation exists, null otherwise
*/
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
/**
* Based on enabled, label is either set or removed.
*
* @param {boolean} enabled - toggle On or Off
* @param {string} name - label name
* @param {string} [value] - label value (optional)
* @returns {Promise<void>}
*/
async toggleLabel(enabled, name, value) { return await this.toggleAttribute(LABEL, enabled, name, value); }
/**
* Based on enabled, relation is either set or removed.
*
* @param {boolean} enabled - toggle On or Off
* @param {string} name - relation name
* @param {string} [value] - relation value (noteId)
* @returns {Promise<void>}
*/
async toggleRelation(enabled, name, value) { return await this.toggleAttribute(RELATION, enabled, name, value); }
/**
* Create label name-value pair if it doesn't exist yet.
*
* @param {string} name - label name
* @param {string} [value] - label value
* @returns {Promise<void>}
*/
async setLabel(name, value) { return await this.setAttribute(LABEL, name, value); }
/**
* Create relation name-value pair if it doesn't exist yet.
*
* @param {string} name - relation name
* @param {string} [value] - relation value (noteId)
* @returns {Promise<void>}
*/
async setRelation(name, value) { return await this.setAttribute(RELATION, name, value); }
/**
* Remove label name-value pair, if it exists.
*
* @param {string} name - label name
* @param {string} [value] - label value
* @returns {Promise<void>}
*/
async removeLabel(name, value) { return await this.removeAttribute(LABEL, name, value); }
/**
* Remove relation name-value pair, if it exists.
*
* @param {string} name - relation name
* @param {string} [value] - relation value (noteId)
* @returns {Promise<void>}
*/
async removeRelation(name, value) { return await this.removeAttribute(RELATION, name, value); }
/**
* @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
*/
async getRelationTarget(name) {
const relation = await this.getRelation(name);
return relation ? await repository.getNote(relation.value) : null;
}
/**
* Finds notes with given attribute name and value. Only own attributes are considered, not inherited ones
*
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @param {string} [value] - attribute value
* @returns {Promise<Note[]>}
*/
async findNotesWithAttribute(type, name, value) {
const params = [this.noteId, name];
let valueCondition = "";
if (value !== undefined) {
params.push(value);
valueCondition = " AND attributes.value = ?";
}
const notes = await repository.getEntities(`
WITH RECURSIVE
tree(noteId) AS (
SELECT ?
UNION
SELECT branches.noteId FROM branches
JOIN tree ON branches.parentNoteId = tree.noteId
JOIN notes ON notes.noteId = branches.noteId
WHERE notes.isDeleted = 0
AND branches.isDeleted = 0
)
SELECT notes.* FROM notes
JOIN tree ON tree.noteId = notes.noteId
JOIN attributes ON attributes.noteId = notes.noteId
WHERE attributes.isDeleted = 0
AND attributes.name = ?
${valueCondition}
ORDER BY noteId, position`, params);
return notes;
}
/**
* Finds notes with given label name and value. Only own labels are considered, not inherited ones
*
* @param {string} name - label name
* @param {string} [value] - label value
* @returns {Promise<Note[]>}
*/
async findNotesWithLabel(name, value) { return await this.findNotesWithAttribute(LABEL, name, value); }
/**
* Finds notes with given relation name and value. Only own relations are considered, not inherited ones
*
* @param {string} name - relation name
* @param {string} [value] - relation value
* @returns {Promise<Note[]>}
*/
async findNotesWithRelation(name, value) { return await this.findNotesWithAttribute(RELATION, name, value); }
/**
* Returns note revisions of this note.
*
* @returns {Promise<NoteRevision[]>}
*/
async getRevisions() {
return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
}
/**
* @returns {Promise<NoteImage[]>}
*/
async getNoteImages() {
return await repository.getEntities("SELECT * FROM note_images WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
}
/**
* @returns {Promise<Branch[]>}
*/
async getBranches() {
return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
}
async getChildNote(name) {
return await repository.getEntity(`
SELECT notes.*
FROM branches
JOIN notes USING(noteId)
WHERE notes.isDeleted = 0
AND branches.isDeleted = 0
AND branches.parentNoteId = ?
AND notes.title = ?`, [this.noteId, name]);
}
/**
* @returns {Promise<Note[]>} child notes of this note
*/
async getChildNotes() {
return await repository.getEntities(`
SELECT notes.*
@@ -119,6 +482,9 @@ class Note extends Entity {
ORDER BY branches.notePosition`, [this.noteId]);
}
/**
* @returns {Promise<Branch[]>} child branches of this note
*/
async getChildBranches() {
return await repository.getEntities(`
SELECT branches.*
@@ -128,6 +494,9 @@ class Note extends Entity {
ORDER BY branches.notePosition`, [this.noteId]);
}
/**
* @returns {Promise<Note[]>} parent notes of this note (note can have multiple parents because of cloning)
*/
async getParentNotes() {
return await repository.getEntities(`
SELECT parent_notes.*
@@ -140,8 +509,6 @@ class Note extends Entity {
}
beforeSaving() {
super.beforeSaving();
if (this.isJson() && this.jsonContent) {
this.content = JSON.stringify(this.jsonContent, null, '\t');
}
@@ -158,7 +525,11 @@ class Note extends Entity {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}

View File

@@ -4,10 +4,22 @@ const Entity = require('./entity');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
/**
* This class represents image's placement in the note(s). One image may be placed into several notes.
*
* @param {string} noteImageId
* @param {string} noteId
* @param {string} imageId
* @param {boolean} isDeleted
* @param {string} dateModified
* @param {string} dateCreated
*
* @extends Entity
*/
class NoteImage extends Entity {
static get tableName() { return "note_images"; }
static get entityName() { return "note_images"; }
static get primaryKeyName() { return "noteImageId"; }
static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateModified", "dateCreated"]; }
static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateCreated"]; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
@@ -18,8 +30,6 @@ class NoteImage extends Entity {
}
beforeSaving() {
super.beforeSaving();
if (!this.isDeleted) {
this.isDeleted = false;
}
@@ -28,7 +38,11 @@ class NoteImage extends Entity {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}

View File

@@ -4,14 +4,31 @@ const Entity = require('./entity');
const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
/**
* NoteRevision represents snapshot of note's title and content at some point in the past. It's used for seamless note versioning.
*
* @param {string} noteRevisionId
* @param {string} noteId
* @param {string} type
* @param {string} mime
* @param {string} title
* @param {string} content
* @param {string} isProtected
* @param {string} dateModifiedFrom
* @param {string} dateModifiedTo
*
* @extends Entity
*/
class NoteRevision extends Entity {
static get tableName() { return "note_revisions"; }
static get entityName() { return "note_revisions"; }
static get primaryKeyName() { return "noteRevisionId"; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "dateModifiedFrom", "dateModifiedTo"]; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "isProtected", "dateModifiedFrom", "dateModifiedTo"]; }
constructor(row) {
super(row);
this.isProtected = !!this.isProtected;
if (this.isProtected) {
protectedSessionService.decryptNoteRevision(this);
}
@@ -22,11 +39,11 @@ class NoteRevision extends Entity {
}
beforeSaving() {
super.beforeSaving();
if (this.isProtected) {
protectedSessionService.encryptNoteRevision(this);
}
super.beforeSaving();
}
}

View File

@@ -3,15 +3,34 @@
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
/**
* Option represents name-value pair, either directly configurable by the user or some system property.
*
* @param {string} name
* @param {string} value
* @param {boolean} isSynced
* @param {string} dateModified
* @param {string} dateCreated
*
* @extends Entity
*/
class Option extends Entity {
static get tableName() { return "options"; }
static get entityName() { return "options"; }
static get primaryKeyName() { return "name"; }
static get hashedProperties() { return ["name", "value"]; }
constructor(row) {
super(row);
this.isSynced = !!this.isSynced;
}
beforeSaving() {
super.beforeSaving();
this.dateModified = dateUtils.nowDate();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}

View File

@@ -3,14 +3,22 @@
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
/**
* RecentNote represents recently visited note.
*
* @param {string} branchId
* @param {string} notePath
* @param {boolean} isDeleted
* @param {string} dateModified
*
* @extends Entity
*/
class RecentNote extends Entity {
static get tableName() { return "recent_notes"; }
static get entityName() { return "recent_notes"; }
static get primaryKeyName() { return "branchId"; }
static get hashedProperties() { return ["branchId", "notePath", "dateCreated", "isDeleted"]; }
beforeSaving() {
super.beforeSaving();
if (!this.isDeleted) {
this.isDeleted = false;
}
@@ -18,6 +26,8 @@ class RecentNote extends Entity {
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
super.beforeSaving();
}
}

View File

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

View File

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 245 B

View File

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 339 B

View File

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

View File

Before

Width:  |  Height:  |  Size: 288 B

After

Width:  |  Height:  |  Size: 288 B

View File

Before

Width:  |  Height:  |  Size: 284 B

After

Width:  |  Height:  |  Size: 284 B

View File

Before

Width:  |  Height:  |  Size: 292 B

After

Width:  |  Height:  |  Size: 292 B

View File

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 511 B

View File

Before

Width:  |  Height:  |  Size: 155 B

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

View File

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 358 B

View File

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

View File

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 419 B

View File

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

View File

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fafafa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shield"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>

After

Width:  |  Height:  |  Size: 274 B

View File

@@ -2,8 +2,8 @@ import cloningService from '../services/cloning.js';
import linkService from '../services/link.js';
import noteDetailService from '../services/note_detail.js';
import treeUtils from '../services/tree_utils.js';
import server from "../services/server.js";
import noteDetailText from "../services/note_detail_text.js";
import noteAutocompleteService from "../services/note_autocomplete.js";
const $dialog = $("#add-link-dialog");
const $form = $("#add-link-form");
@@ -15,6 +15,7 @@ const $prefixFormGroup = $("#add-link-prefix-form-group");
const $linkTypeDiv = $("#add-link-type-div");
const $linkTypes = $("input[name='add-link-type']");
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
const $showRecentNotesButton = $dialog.find(".show-recent-notes-button");
function setLinkType(linkType) {
$linkTypes.each(function () {
@@ -53,16 +54,16 @@ async function showDialog() {
$linkTitle.val(noteTitle);
}
$autoComplete.autocomplete({
source: async function(request, response) {
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
await $autoComplete.autocomplete({
source: noteAutocompleteService.autocompleteSource,
minLength: 0,
change: async (event, ui) => {
if (!ui.item) {
return;
}
const notePath = linkService.getNotePathFromLabel(ui.item.value);
response(result);
},
minLength: 2,
change: async () => {
const val = $autoComplete.val();
const notePath = linkService.getNodePathFromLabel(val);
if (!notePath) {
return;
}
@@ -73,21 +74,30 @@ async function showDialog() {
await setDefaultLinkTitle(noteId);
}
},
select: function (event, ui) {
if (ui.item.value === 'No results') {
return false;
}
},
// this is called when user goes through autocomplete list with keyboard
// at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
focus: async (event, ui) => {
const notePath = linkService.getNodePathFromLabel(ui.item.value);
const notePath = linkService.getNotePathFromLabel(ui.item.value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
await setDefaultLinkTitle(noteId);
event.preventDefault();
}
});
showRecentNotes();
}
$form.submit(() => {
const value = $autoComplete.val();
const notePath = linkService.getNodePathFromLabel(value);
const notePath = linkService.getNotePathFromLabel(value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (notePath) {
@@ -145,8 +155,14 @@ function linkTypeChanged() {
$linkTypeDiv.toggle(!hasSelection());
}
function showRecentNotes() {
$autoComplete.autocomplete("search", "");
}
$linkTypes.change(linkTypeChanged);
$showRecentNotesButton.click(showRecentNotes);
export default {
showDialog
};

View File

@@ -0,0 +1,302 @@
import noteDetailService from '../services/note_detail.js';
import server from '../services/server.js';
import infoService from "../services/info.js";
import treeUtils from "../services/tree_utils.js";
import linkService from "../services/link.js";
const $dialog = $("#attributes-dialog");
const $saveAttributesButton = $("#save-attributes-button");
const $ownedAttributesBody = $('#owned-attributes-table tbody');
const attributesModel = new AttributesModel();
function AttributesModel() {
const self = this;
this.ownedAttributes = ko.observableArray();
this.inheritedAttributes = ko.observableArray();
this.availableTypes = [
{ text: "Label", value: "label" },
{ text: "Label definition", value: "label-definition" },
{ text: "Relation", value: "relation" },
{ text: "Relation definition", value: "relation-definition" }
];
this.availableLabelTypes = [
{ text: "Text", value: "text" },
{ text: "Number", value: "number" },
{ text: "Boolean", value: "boolean" },
{ text: "Date", value: "date" },
{ text: "URL", value: "url"}
];
this.multiplicityTypes = [
{ text: "Single value", value: "singlevalue" },
{ text: "Multi value", value: "multivalue" }
];
this.typeChanged = function(data, event) {
self.getTargetAttribute(event.target).valueHasMutated();
};
this.labelTypeChanged = function(data, event) {
self.getTargetAttribute(event.target).valueHasMutated();
};
this.updateAttributePositions = function() {
let position = 0;
// we need to update positions by searching in the DOM, because order of the
// attributes in the viewmodel (self.ownedAttributes()) stays the same
$ownedAttributesBody.find('input[name="position"]').each(function() {
const attribute = self.getTargetAttribute(this);
attribute().position = position++;
});
};
async function showAttributes(attributes) {
const ownedAttributes = attributes.filter(attr => attr.isOwned);
for (const attr of ownedAttributes) {
attr.labelValue = attr.type === 'label' ? attr.value : '';
attr.relationValue = attr.type === 'relation' ? (await treeUtils.getNoteTitle(attr.value) + " (" + attr.value + ")") : '';
attr.labelDefinition = (attr.type === 'label-definition' && attr.value) ? attr.value : {
labelType: "text",
multiplicityType: "singlevalue",
isPromoted: true
};
attr.relationDefinition = (attr.type === 'relation-definition' && attr.value) ? attr.value : {
multiplicityType: "singlevalue",
isPromoted: true
};
delete attr.value;
}
self.ownedAttributes(ownedAttributes.map(ko.observable));
addLastEmptyRow();
const inheritedAttributes = attributes.filter(attr => !attr.isOwned);
self.inheritedAttributes(inheritedAttributes);
}
this.loadAttributes = async function() {
const noteId = noteDetailService.getCurrentNoteId();
const attributes = await server.get('notes/' + noteId + '/attributes');
await showAttributes(attributes);
// attribute might not be rendered immediatelly so could not focus
setTimeout(() => $(".attribute-type-select:last").focus(), 100);
$ownedAttributesBody.sortable({
handle: '.handle',
containment: $ownedAttributesBody,
update: this.updateAttributePositions
});
};
this.deleteAttribute = function(data, event) {
const attribute = self.getTargetAttribute(event.target);
const attributeData = attribute();
if (attributeData) {
attributeData.isDeleted = true;
attribute(attributeData);
addLastEmptyRow();
}
};
function isValid() {
for (let attributes = self.ownedAttributes(), i = 0; i < attributes.length; i++) {
if (self.isEmptyName(i)) {
return false;
}
}
return true;
}
this.save = async function() {
// we need to defocus from input (in case of enter-triggered save) because value is updated
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
$saveAttributesButton.focus();
if (!isValid()) {
alert("Please fix all validation errors and try saving again.");
return;
}
self.updateAttributePositions();
const noteId = noteDetailService.getCurrentNoteId();
const attributesToSave = self.ownedAttributes()
.map(attribute => attribute())
.filter(attribute => attribute.attributeId !== "" || attribute.name !== "");
for (const attr of attributesToSave) {
if (attr.type === 'label') {
attr.value = attr.labelValue;
}
else if (attr.type === 'relation') {
attr.value = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel(attr.relationValue));
}
else if (attr.type === 'label-definition') {
attr.value = attr.labelDefinition;
}
else if (attr.type === 'relation-definition') {
attr.value = attr.relationDefinition;
}
delete attr.labelValue;
delete attr.relationValue;
delete attr.labelDefinition;
delete attr.relationDefinition;
}
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
await showAttributes(attributes);
infoService.showMessage("Attributes have been saved.");
noteDetailService.loadAttributes();
};
function addLastEmptyRow() {
const attributes = self.ownedAttributes().filter(attr => !attr().isDeleted);
const last = attributes.length === 0 ? null : attributes[attributes.length - 1]();
if (!last || last.name.trim() !== "") {
self.ownedAttributes.push(ko.observable({
attributeId: '',
type: 'label',
name: '',
labelValue: '',
relationValue: '',
isInheritable: false,
isDeleted: false,
position: 0,
labelDefinition: {
labelType: "text",
multiplicityType: "singlevalue",
isPromoted: true
},
relationDefinition: {
multiplicityType: "singlevalue",
isPromoted: true
}
}));
}
}
this.attributeChanged = function (data, event) {
addLastEmptyRow();
const attribute = self.getTargetAttribute(event.target);
attribute.valueHasMutated();
};
this.isEmptyName = function(index) {
const cur = self.ownedAttributes()[index]();
return cur.name.trim() === "" && !cur.isDeleted && (cur.attributeId !== "" || cur.labelValue !== "" || cur.relationValue);
};
this.getTargetAttribute = function(target) {
const context = ko.contextFor(target);
const index = context.$index();
return self.ownedAttributes()[index];
}
}
async function showDialog() {
glob.activeDialog = $dialog;
await attributesModel.loadAttributes();
$dialog.dialog({
modal: true,
width: 950,
height: 700
});
}
ko.applyBindings(attributesModel, $dialog[0]);
$dialog.on('focus', '.attribute-name', function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
$(this).autocomplete({
source: async (request, response) => {
const attribute = attributesModel.getTargetAttribute(this);
const type = (attribute().type === 'relation' || attribute().type === 'relation-definition') ? 'relation' : 'label';
const names = await server.get('attributes/names/?type=' + type + '&query=' + encodeURIComponent(request.term));
const result = names.map(name => {
return {
label: name,
value: name
}
});
if (result.length > 0) {
response(result);
}
else {
response([{
label: "No results",
value: "No results"
}]);
}
},
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
$dialog.on('focus', '.label-value', async function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
const attributeName = $(this).parent().parent().find('.attribute-name').val();
if (attributeName.trim() === "") {
return;
}
const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName));
if (attributeValues.length === 0) {
return;
}
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in autocomplete.js
source: attributeValues.map(attribute => {
return {
attribute: attribute,
value: attribute
}
}),
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
export default {
showDialog
};

View File

@@ -15,7 +15,7 @@ async function showDialog() {
await $dialog.dialog({
modal: true,
width: 500
width: 600
});
const currentNode = treeService.getCurrentNode();

View File

@@ -1,10 +1,12 @@
import treeService from '../services/tree.js';
import linkService from '../services/link.js';
import server from '../services/server.js';
import searchNotesService from '../services/search_notes.js';
import noteautocompleteService from '../services/note_autocomplete.js';
import linkService from "../services/link.js";
const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
const $form = $("#jump-to-note-form");
const $showInFullTextButton = $("#show-in-full-text-button");
const $showRecentNotesButton = $dialog.find(".show-recent-notes-button");
async function showDialog() {
glob.activeDialog = $dialog;
@@ -13,39 +15,54 @@ async function showDialog() {
$dialog.dialog({
modal: true,
width: 800
width: 800,
position: { my: "center top+100", at: "top", of: window }
});
await $autoComplete.autocomplete({
source: async function(request, response) {
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
source: noteautocompleteService.autocompleteSource,
focus: event => event.preventDefault(),
minLength: 0,
autoFocus: true,
select: function (event, ui) {
if (ui.item.value === 'No results') {
return false;
}
response(result);
},
minLength: 2
const notePath = linkService.getNotePathFromLabel(ui.item.value);
treeService.activateNote(notePath);
$dialog.dialog('close');
}
});
showRecentNotes();
}
function getSelectedNotePath() {
const val = $autoComplete.val();
return linkService.getNodePathFromLabel(val);
function showInFullText(e) {
// stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
e.preventDefault();
e.stopPropagation();
const searchText = $autoComplete.val();
searchNotesService.resetSearch();
searchNotesService.showSearch();
searchNotesService.doSearch(searchText);
$dialog.dialog('close');
}
function goToNote() {
const notePath = getSelectedNotePath();
if (notePath) {
treeService.activateNode(notePath);
$dialog.dialog('close');
}
function showRecentNotes() {
$autoComplete.autocomplete("search", "");
}
$form.submit(() => {
goToNote();
$showInFullTextButton.click(showInFullText);
return false;
});
$showRecentNotesButton.click(showRecentNotes);
$dialog.bind('keydown', 'ctrl+return', showInFullText);
export default {
showDialog

View File

@@ -1,223 +0,0 @@
import noteDetailService from '../services/note_detail.js';
import utils from '../services/utils.js';
import server from '../services/server.js';
import infoService from "../services/info.js";
const $dialog = $("#labels-dialog");
const $saveLabelsButton = $("#save-labels-button");
const $labelsBody = $('#labels-table tbody');
const labelsModel = new LabelsModel();
let labelNames = [];
function LabelsModel() {
const self = this;
this.labels = ko.observableArray();
this.updateLabelPositions = function() {
let position = 0;
// we need to update positions by searching in the DOM, because order of the
// labels in the viewmodel (self.labels()) stays the same
$labelsBody.find('input[name="position"]').each(function() {
const label = self.getTargetLabel(this);
label().position = position++;
});
};
this.loadLabels = async function() {
const noteId = noteDetailService.getCurrentNoteId();
const labels = await server.get('notes/' + noteId + '/labels');
self.labels(labels.map(ko.observable));
addLastEmptyRow();
labelNames = await server.get('labels/names');
// label might not be rendered immediatelly so could not focus
setTimeout(() => $(".label-name:last").focus(), 100);
$labelsBody.sortable({
handle: '.handle',
containment: $labelsBody,
update: this.updateLabelPositions
});
};
this.deleteLabel = function(data, event) {
const label = self.getTargetLabel(event.target);
const labelData = label();
if (labelData) {
labelData.isDeleted = 1;
label(labelData);
addLastEmptyRow();
}
};
function isValid() {
for (let labels = self.labels(), i = 0; i < labels.length; i++) {
if (self.isEmptyName(i)) {
return false;
}
}
return true;
}
this.save = async function() {
// we need to defocus from input (in case of enter-triggered save) because value is updated
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
$saveLabelsButton.focus();
if (!isValid()) {
alert("Please fix all validation errors and try saving again.");
return;
}
self.updateLabelPositions();
const noteId = noteDetailService.getCurrentNoteId();
const labelsToSave = self.labels()
.map(label => label())
.filter(label => label.labelId !== "" || label.name !== "");
const labels = await server.put('notes/' + noteId + '/labels', labelsToSave);
self.labels(labels.map(ko.observable));
addLastEmptyRow();
infoService.showMessage("Labels have been saved.");
noteDetailService.loadLabelList();
};
function addLastEmptyRow() {
const labels = self.labels().filter(attr => attr().isDeleted === 0);
const last = labels.length === 0 ? null : labels[labels.length - 1]();
if (!last || last.name.trim() !== "" || last.value !== "") {
self.labels.push(ko.observable({
labelId: '',
name: '',
value: '',
isDeleted: 0,
position: 0
}));
}
}
this.labelChanged = function (data, event) {
addLastEmptyRow();
const label = self.getTargetLabel(event.target);
label.valueHasMutated();
};
this.isNotUnique = function(index) {
const cur = self.labels()[index]();
if (cur.name.trim() === "") {
return false;
}
for (let labels = self.labels(), i = 0; i < labels.length; i++) {
const label = labels[i]();
if (index !== i && cur.name === label.name) {
return true;
}
}
return false;
};
this.isEmptyName = function(index) {
const cur = self.labels()[index]();
return cur.name.trim() === "" && (cur.labelId !== "" || cur.value !== "");
};
this.getTargetLabel = function(target) {
const context = ko.contextFor(target);
const index = context.$index();
return self.labels()[index];
}
}
async function showDialog() {
glob.activeDialog = $dialog;
await labelsModel.loadLabels();
$dialog.dialog({
modal: true,
width: 800,
height: 500
});
}
ko.applyBindings(labelsModel, document.getElementById('labels-dialog'));
$(document).on('focus', '.label-name', function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in autocomplete.js
source: labelNames.map(label => {
return {
label: label,
value: label
}
}),
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
$(document).on('focus', '.label-value', async function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
const labelName = $(this).parent().parent().find('.label-name').val();
if (labelName.trim() === "") {
return;
}
const labelValues = await server.get('labels/values/' + encodeURIComponent(labelName));
if (labelValues.length === 0) {
return;
}
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in autocomplete.js
source: labelValues.map(label => {
return {
label: label,
value: label
}
}),
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
export default {
showDialog
};

View File

@@ -63,10 +63,10 @@ $list.on('change', () => {
}
});
$(document).on('click', "a[action='note-revision']", event => {
$(document).on('click', "a[data-action='note-revision']", event => {
const linkEl = $(event.target);
const noteId = linkEl.attr('note-path');
const noteRevisionId = linkEl.attr('note-revision-id');
const noteId = linkEl.attr('data-note-path');
const noteRevisionId = linkEl.attr('data-note-revision-id');
showNoteRevisionsDialog(noteId, noteRevisionId);

View File

@@ -29,7 +29,7 @@ function formatNode(node, level) {
const indentAfter = new Array(level - 1).join(' ');
let textNode;
for (const i = 0; i < node.children.length; i++) {
for (let i = 0; i < node.children.length; i++) {
textNode = document.createTextNode('\n' + indentBefore);
node.insertBefore(textNode, node.children[i]);

View File

@@ -1,9 +1,10 @@
"use strict";
import protectedSessionHolder from '../services/protected_session_holder.js';
import utils from '../services/utils.js';
import server from '../services/server.js';
import infoService from "../services/info.js";
import zoomService from "../services/zoom.js";
import utils from "../services/utils.js";
const $dialog = $("#options-dialog");
const $tabs = $("#options-tabs");
@@ -33,8 +34,8 @@ async function showDialog() {
}
}
async function saveOptions(optionName, optionValue) {
await server.put('options/' + encodeURIComponent(optionName) + '/' + encodeURIComponent(optionValue));
async function saveOptions(options) {
await server.put('options', options);
infoService.showMessage("Options change have been saved.");
}
@@ -44,6 +45,41 @@ export default {
saveOptions
};
addTabHandler((function() {
const $themeSelect = $("#theme-select");
const $zoomFactorSelect = $("#zoom-factor-select");
const $html = $("html");
function optionsLoaded(options) {
$themeSelect.val(options.theme);
if (utils.isElectron()) {
$zoomFactorSelect.val(options.zoomFactor);
}
else {
$zoomFactorSelect.prop('disabled', true);
}
}
$themeSelect.change(function() {
const newTheme = $(this).val();
$html.attr("class", "theme-" + newTheme);
server.put('options/theme/' + newTheme);
});
$zoomFactorSelect.change(function() {
const newZoomFactor = $(this).val();
zoomService.setZoomFactorAndSave(newZoomFactor);
});
return {
optionsLoaded
};
})());
addTabHandler((function() {
const $form = $("#change-password-form");
const $oldPassword = $("#old-password");
@@ -93,16 +129,15 @@ addTabHandler((function() {
addTabHandler((function() {
const $form = $("#protected-session-timeout-form");
const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
const optionName = 'protectedSessionTimeout';
function optionsLoaded(options) {
$protectedSessionTimeout.val(options[optionName]);
$protectedSessionTimeout.val(options['protectedSessionTimeout']);
}
$form.submit(() => {
const protectedSessionTimeout = $protectedSessionTimeout.val();
saveOptions(optionName, protectedSessionTimeout).then(() => {
saveOptions({ 'protectedSessionTimeout': protectedSessionTimeout }).then(() => {
protectedSessionHolder.setProtectedSessionTimeout(protectedSessionTimeout);
});
@@ -117,14 +152,13 @@ addTabHandler((function() {
addTabHandler((function () {
const $form = $("#note-revision-snapshot-time-interval-form");
const $timeInterval = $("#note-revision-snapshot-time-interval-in-seconds");
const optionName = 'noteRevisionSnapshotTimeInterval';
function optionsLoaded(options) {
$timeInterval.val(options[optionName]);
$timeInterval.val(options['noteRevisionSnapshotTimeInterval']);
}
$form.submit(() => {
saveOptions(optionName, $timeInterval.val());
saveOptions({ 'noteRevisionSnapshotTimeInterval': $timeInterval.val() });
return false;
});
@@ -137,6 +171,7 @@ addTabHandler((function () {
addTabHandler((async function () {
const $appVersion = $("#app-version");
const $dbVersion = $("#db-version");
const $syncVersion = $("#sync-version");
const $buildDate = $("#build-date");
const $buildRevision = $("#build-revision");
@@ -144,6 +179,7 @@ addTabHandler((async function () {
$appVersion.html(appInfo.appVersion);
$dbVersion.html(appInfo.dbVersion);
$syncVersion.html(appInfo.syncVersion);
$buildDate.html(appInfo.buildDate);
$buildRevision.html(appInfo.buildRevision);
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
@@ -151,6 +187,57 @@ addTabHandler((async function () {
return {};
})());
addTabHandler((function() {
const $form = $("#sync-setup-form");
const $syncServerHost = $("#sync-server-host");
const $syncServerTimeout = $("#sync-server-timeout");
const $syncProxy = $("#sync-proxy");
const $testSyncButton = $("#test-sync-button");
const $syncToServerButton = $("#sync-to-server-button");
function optionsLoaded(options) {
$syncServerHost.val(options['syncServerHost']);
$syncServerTimeout.val(options['syncServerTimeout']);
$syncProxy.val(options['syncProxy']);
}
$form.submit(() => {
saveOptions({
'syncServerHost': $syncServerHost.val(),
'syncServerTimeout': $syncServerTimeout.val(),
'syncProxy': $syncProxy.val()
});
return false;
});
$testSyncButton.click(async () => {
const result = await server.post('sync/test');
if (result.connection === "Success") {
infoService.showMessage("Sync server handshake has been successful");
}
else {
infoService.showError("Sync server handshake failed, error: " + result.error);
}
});
$syncToServerButton.click(async () => {
const resp = await server.post("setup/sync-to-server");
if (resp.success) {
infoService.showMessage("Sync has been established to the server instance. It will take some time to finish.");
}
else {
infoService.showError('Sync setup failed: ' + resp.error);
}
});
return {
optionsLoaded
};
})());
addTabHandler((async function () {
const $forceFullSyncButton = $("#force-full-sync-button");
const $fillSyncRowsButton = $("#fill-sync-rows-button");

View File

@@ -15,7 +15,11 @@ async function showDialog() {
const result = await server.get('recent-changes/');
$dialog.html('');
$dialog.empty();
if (result.length === 0) {
$dialog.append("No changes yet ...");
}
const groupedByDate = groupByDate(result);
@@ -30,9 +34,9 @@ async function showDialog() {
const revLink = $("<a>", {
href: 'javascript:',
text: 'rev'
}).attr('action', 'note-revision')
.attr('note-path', change.noteId)
.attr('note-revision-id', change.noteRevisionId);
}).attr('data-action', 'note-revision')
.attr('data-note-path', change.noteId)
.attr('data-note-revision-id', change.noteRevisionId);
let noteLink;

View File

@@ -1,113 +0,0 @@
import treeService from '../services/tree.js';
import messagingService from '../services/messaging.js';
import server from '../services/server.js';
import utils from "../services/utils.js";
import treeUtils from "../services/tree_utils.js";
const $dialog = $("#recent-notes-dialog");
const $searchInput = $('#recent-notes-search-input');
// list of recent note paths
let list = [];
async function reload() {
const result = await server.get('recent-notes');
list = result.map(r => r.notePath);
}
function addRecentNote(branchId, notePath) {
setTimeout(async () => {
// we include the note into recent list only if the user stayed on the note at least 5 seconds
if (notePath && notePath === treeService.getCurrentNotePath()) {
const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
list = result.map(r => r.notePath);
}
}, 1500);
}
async function getNoteTitle(notePath) {
let noteTitle;
try {
noteTitle = await treeUtils.getNotePathTitle(notePath);
}
catch (e) {
noteTitle = "[error - can't find note title]";
messagingService.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
}
return noteTitle;
}
async function showDialog() {
glob.activeDialog = $dialog;
$dialog.dialog({
modal: true,
width: 800,
height: 100,
position: { my: "center top+100", at: "top", of: window }
});
$searchInput.val('');
// remove the current note
const recNotes = list.filter(note => note !== treeService.getCurrentNotePath());
const items = [];
for (const notePath of recNotes) {
items.push({
label: await getNoteTitle(notePath),
value: notePath
});
}
$searchInput.autocomplete({
source: items,
minLength: 0,
autoFocus: true,
select: function (event, ui) {
treeService.activateNode(ui.item.value);
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
},
focus: function (event, ui) {
event.preventDefault();
},
close: function (event, ui) {
if (event.keyCode === 27) { // escape closes dialog
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
}
else {
// keep autocomplete open
// we're kind of abusing autocomplete to work in a way which it's not designed for
$searchInput.autocomplete("search", "");
}
},
create: () => $searchInput.autocomplete("search", ""),
classes: {
"ui-autocomplete": "recent-notes-autocomplete"
}
});
}
setTimeout(reload, 100);
messagingService.subscribeToMessages(syncData => {
if (syncData.some(sync => sync.entityName === 'recent_notes')) {
console.log(utils.now(), "Reloading recent notes because of background changes");
reload();
}
});
export default {
showDialog,
addRecentNote,
reload
};

View File

@@ -1,19 +1,28 @@
/** Represents mapping between note and parent note */
class Branch {
constructor(treeCache, row) {
this.treeCache = treeCache;
/** @param {string} primary key */
this.branchId = row.branchId;
/** @param {string} */
this.noteId = row.noteId;
this.note = null;
/** @param {string} */
this.parentNoteId = row.parentNoteId;
/** @param {int} */
this.notePosition = row.notePosition;
/** @param {string} */
this.prefix = row.prefix;
/** @param {boolean} */
this.isExpanded = row.isExpanded;
}
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
}
/** @returns {boolean} true if it's top level, meaning its parent is root note */
isTopLevel() {
return this.parentNoteId === 'root';
}

View File

@@ -1,13 +1,18 @@
import NoteShort from './note_short.js';
/**
* Represents full note, specifically including note's content.
*/
class NoteFull extends NoteShort {
constructor(treeCache, row) {
super(treeCache, row);
/** @param {string} */
this.content = row.content;
if (this.content !== "" && this.isJson()) {
try {
/** @param {object} */
this.jsonContent = JSON.parse(this.content);
}
catch(e) {}

View File

@@ -1,18 +1,31 @@
/**
* This note's representation is used in note tree and is kept in TreeCache.
* Its notable omission is the note content.
*/
class NoteShort {
constructor(treeCache, row) {
this.treeCache = treeCache;
/** @param {string} */
this.noteId = row.noteId;
/** @param {string} */
this.title = row.title;
/** @param {boolean} */
this.isProtected = row.isProtected;
/** @param {string} one of 'text', 'code', 'file' or 'render' */
this.type = row.type;
/** @param {string} content-type, e.g. "application/json" */
this.mime = row.mime;
this.hideInAutocomplete = row.hideInAutocomplete;
/** @param {boolean} */
this.archived = row.archived;
this.cssClass = row.cssClass;
}
/** @returns {boolean} */
isJson() {
return this.mime === "application/json";
}
/** @returns {Promise<Branch[]>} */
async getBranches() {
const branchIds = this.treeCache.parents[this.noteId].map(
parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId));
@@ -20,11 +33,13 @@ class NoteShort {
return this.treeCache.getBranches(branchIds);
}
/** @returns {boolean} */
hasChildren() {
return this.treeCache.children[this.noteId]
&& this.treeCache.children[this.noteId].length > 0;
}
/** @returns {Promise<Branch[]>} */
async getChildBranches() {
if (!this.treeCache.children[this.noteId]) {
return [];
@@ -36,18 +51,22 @@ class NoteShort {
return await this.treeCache.getBranches(branchIds);
}
/** @returns {string[]} */
getParentNoteIds() {
return this.treeCache.parents[this.noteId] || [];
}
/** @returns {Promise<NoteShort[]>} */
async getParentNotes() {
return await this.treeCache.getNotes(this.getParentNoteIds());
}
/** @returns {string[]} */
getChildNoteIds() {
return this.treeCache.children[this.noteId] || [];
}
/** @returns {Promise<NoteShort[]>} */
async getChildNotes() {
return await this.treeCache.getNotes(this.getChildNoteIds());
}
@@ -59,7 +78,7 @@ class NoteShort {
get dto() {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.hideInAutocomplete;
delete dto.archived;
return dto;
}

View File

@@ -1,46 +0,0 @@
import server from './services/server.js';
$(document).ready(async () => {
const {appDbVersion, dbVersion} = await server.get('migration');
console.log("HI", {appDbVersion, dbVersion});
if (appDbVersion === dbVersion) {
$("#up-to-date").show();
}
else {
$("#need-to-migrate").show();
$("#app-db-version").html(appDbVersion);
$("#db-version").html(dbVersion);
}
});
$("#run-migration").click(async () => {
$("#run-migration").prop("disabled", true);
$("#migration-result").show();
const result = await server.post('migration');
for (const migration of result.migrations) {
const row = $('<tr>')
.append($('<td>').html(migration.dbVersion))
.append($('<td>').html(migration.name))
.append($('<td>').html(migration.success ? 'Yes' : 'No'))
.append($('<td>').html(migration.success ? 'N/A' : migration.error));
if (!migration.success) {
row.addClass("danger");
}
$("#migration-table").append(row);
}
});
// copy of this shortcut to be able to debug migration problems
$(document).bind('keydown', 'ctrl+shift+i', () => {
require('electron').remote.getCurrentWindow().toggleDevTools();
return false;
});

View File

@@ -1,10 +1,9 @@
import addLinkDialog from '../dialogs/add_link.js';
import jumpToNoteDialog from '../dialogs/jump_to_note.js';
import labelsDialog from '../dialogs/labels.js';
import attributesDialog from '../dialogs/attributes.js';
import noteRevisionsDialog from '../dialogs/note_revisions.js';
import noteSourceDialog from '../dialogs/note_source.js';
import recentChangesDialog from '../dialogs/recent_changes.js';
import recentNotesDialog from '../dialogs/recent_notes.js';
import optionsDialog from '../dialogs/options.js';
import sqlConsoleDialog from '../dialogs/sql_console.js';
@@ -17,8 +16,8 @@ import messagingService from './messaging.js';
import noteDetailService from './note_detail.js';
import noteType from './note_type.js';
import protected_session from './protected_session.js';
import searchTreeService from './search_tree.js';
import ScriptApi from './script_api.js';
import searchNotesService from './search_notes.js';
import FrontendScriptApi from './frontend_script_api.js';
import ScriptContext from './script_context.js';
import sync from './sync.js';
import treeService from './tree.js';
@@ -36,6 +35,8 @@ import libraryLoader from "./library_loader.js";
window.glob.getCurrentNode = treeService.getCurrentNode;
window.glob.getHeaders = server.getHeaders;
window.glob.showAddLinkDialog = addLinkDialog.showDialog;
// this is required by CKEditor when uploading images
window.glob.noteChanged = noteDetailService.noteChanged;
// required for ESLint plugin
window.glob.getCurrentNote = noteDetailService.getCurrentNote;
@@ -47,7 +48,12 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
let message = "Uncaught error: ";
if (string.indexOf("script error") > -1){
if (string.includes("Cannot read property 'defaultView' of undefined")) {
// ignore this specific error which is very common but we don't know where it comes from
// and it seems to be harmless
return true;
}
else if (string.includes("script error")) {
message += 'No details available';
}
else {
@@ -65,6 +71,14 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
return false;
};
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/";
$(document).on("click", "button[data-help-page]", e => {
const $button = $(e.target);
window.open(wikiBaseUrl + $button.attr("data-help-page"), '_blank');
});
$("#logout-button").toggle(!utils.isElectron());
if (utils.isElectron()) {
@@ -74,7 +88,7 @@ if (utils.isElectron()) {
await treeService.reload();
}
await treeService.activateNode(parentNoteId);
await treeService.activateNote(parentNoteId);
setTimeout(() => {
const node = treeService.getCurrentNode();

View File

@@ -1,12 +1,24 @@
import ScriptContext from "./script_context.js";
import server from "./server.js";
import infoService from "./info.js";
async function executeBundle(bundle) {
const apiContext = ScriptContext(bundle.note, bundle.allNotes);
async function getAndExecuteBundle(noteId, originEntity = null) {
const bundle = await server.get('script/bundle/' + noteId);
return await (function () {
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
}.call(apiContext));
await executeBundle(bundle, originEntity);
}
async function executeBundle(bundle, originEntity) {
const apiContext = ScriptContext(bundle.note, bundle.allNotes, originEntity);
try {
return await (function () {
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
}.call(apiContext));
}
catch (e) {
infoService.showAndLogError(`Execution of script "${bundle.note.title}" (${bundle.note.noteId}) failed with error: ${e.message}`);
}
}
async function executeStartupBundles() {
@@ -17,7 +29,17 @@ async function executeStartupBundles() {
}
}
async function executeRelationBundles(note, relationName) {
const bundlesToRun = await server.get("script/relation/" + note.noteId + "/" + relationName);
for (const bundle of bundlesToRun) {
await executeBundle(bundle, note);
}
}
export default {
executeBundle,
executeStartupBundles
getAndExecuteBundle,
executeStartupBundles,
executeRelationBundles
}

View File

@@ -114,13 +114,14 @@ const contextMenuOptions = {
// Modify menu entries depending on node status
$tree.contextmenu("enableEntry", "insertNoteHere", isNotRoot && parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search');
$tree.contextmenu("enableEntry", "delete", isNotRoot);
$tree.contextmenu("enableEntry", "delete", isNotRoot && parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "copy", isNotRoot);
$tree.contextmenu("enableEntry", "cut", isNotRoot);
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
$tree.contextmenu("enableEntry", "importBranch", note.type !== 'search');
$tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search');
$tree.contextmenu("enableEntry", "editBranchPrefix", isNotRoot && parentNote.type !== 'search');
// Activate node on right-click
node.setActive();

View File

@@ -2,16 +2,16 @@ import utils from "./utils.js";
import treeService from "./tree.js";
import linkService from "./link.js";
import fileService from "./file.js";
import zoomService from "./zoom.js";
import noteRevisionsDialog from "../dialogs/note_revisions.js";
import optionsDialog from "../dialogs/options.js";
import addLinkDialog from "../dialogs/add_link.js";
import recentNotesDialog from "../dialogs/recent_notes.js";
import jumpToNoteDialog from "../dialogs/jump_to_note.js";
import noteSourceDialog from "../dialogs/note_source.js";
import recentChangesDialog from "../dialogs/recent_changes.js";
import sqlConsoleDialog from "../dialogs/sql_console.js";
import searchTreeService from "./search_tree.js";
import labelsDialog from "../dialogs/labels.js";
import searchNotesService from "./search_notes.js";
import attributesDialog from "../dialogs/attributes.js";
import protectedSessionService from "./protected_session.js";
function registerEntrypoints() {
@@ -22,27 +22,23 @@ function registerEntrypoints() {
utils.bindShortcut('ctrl+l', addLinkDialog.showDialog);
$("#jump-to-note-button").click(jumpToNoteDialog.showDialog);
$("#jump-to-note-dialog-button").click(jumpToNoteDialog.showDialog);
utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog);
$("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions);
$("#show-source-button").click(noteSourceDialog.showDialog);
utils.bindShortcut('ctrl+u', noteSourceDialog.showDialog);
$("#recent-changes-button").click(recentChangesDialog.showDialog);
$("#protected-session-on").click(protectedSessionService.enterProtectedSession);
$("#protected-session-off").click(protectedSessionService.leaveProtectedSession);
$("#recent-notes-button").click(recentNotesDialog.showDialog);
utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog);
$("#toggle-search-button").click(searchNotesService.toggleSearch);
utils.bindShortcut('ctrl+s', searchNotesService.toggleSearch);
$("#toggle-search-button").click(searchTreeService.toggleSearch);
utils.bindShortcut('ctrl+s', searchTreeService.toggleSearch);
$(".show-labels-button").click(labelsDialog.showDialog);
utils.bindShortcut('alt+l', labelsDialog.showDialog);
$(".show-attributes-button").click(attributesDialog.showDialog);
utils.bindShortcut('alt+a', attributesDialog.showDialog);
$("#options-button").click(optionsDialog.showDialog);
@@ -57,7 +53,13 @@ function registerEntrypoints() {
utils.bindShortcut('alt+right', window.history.forward);
}
utils.bindShortcut('alt+m', e => $(".hide-toggle").toggleClass("suppressed"));
utils.bindShortcut('alt+m', e => {
$(".hide-toggle").toggle();
// when hiding switch display to block, otherwise grid still tries to display columns which shows
// left empty column
$("#container").css("display", $("#container").css("display") === "grid" ? "block" : "grid");
});
// hide (toggle) everything except for the note content for distraction free writing
utils.bindShortcut('alt+t', e => {
@@ -109,27 +111,10 @@ function registerEntrypoints() {
$("#note-detail-text").focus();
});
$(document).bind('keydown', 'ctrl+-', () => {
if (utils.isElectron()) {
const webFrame = require('electron').webFrame;
if (webFrame.getZoomFactor() > 0.2) {
webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
}
return false;
}
});
$(document).bind('keydown', 'ctrl+=', () => {
if (utils.isElectron()) {
const webFrame = require('electron').webFrame;
webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
return false;
}
});
if (utils.isElectron()) {
$(document).bind('keydown', 'ctrl+-', zoomService.decreaseZoomFactor);
$(document).bind('keydown', 'ctrl+=', zoomService.increaseZoomFactor);
}
$("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus());

View File

@@ -21,7 +21,7 @@ $("#file-upload").change(async function() {
await treeService.reload();
await treeService.activateNode(resp.noteId);
await treeService.activateNote(resp.noteId);
});
export default {

View File

@@ -0,0 +1,196 @@
import treeService from './tree.js';
import server from './server.js';
import utils from './utils.js';
import infoService from './info.js';
import linkService from './link.js';
import treeCache from './tree_cache.js';
/**
* This is the main frontend API interface for scripts. It's published in the local "api" object.
*
* @constructor
* @hideconstructor
*/
function FrontendScriptApi(startNote, currentNote, originEntity = null) {
const $pluginButtons = $("#plugin-buttons");
/** @property {object} note where script started executing */
this.startNote = startNote;
/** @property {object} note where script is currently executing */
this.currentNote = currentNote;
/** @property {object|null} entity whose event triggered this execution */
this.originEntity = originEntity;
/**
* Activates note in the tree and in the note detail.
*
* @method
* @param {string} notePath (or noteId)
* @returns {Promise<void>}
*/
this.activateNote = treeService.activateNote;
/**
* Activates newly created note. Compared to this.activateNote() also refreshes tree.
*
* @param {string} notePath (or noteId)
* @return {Promise<void>}
*/
this.activateNewNote = async notePath => {
await treeService.reload();
await treeService.activateNote(notePath, true);
};
/**
* @typedef {Object} ToolbarButtonOptions
* @property {string} title
* @property {string} [icon] - name of the jQuery UI icon to be used (e.g. "clock" for "ui-icon-clock" icon)
* @property {function} action - callback handling the click on the button
* @property {string} [shortcut] - keyboard shortcut for the button, e.g. "alt+t"
*/
/**
* Adds new button the the plugin area.
*
* @param {ToolbarButtonOptions} opts
*/
this.addButtonToToolbar = opts => {
const buttonId = "toolbar-button-" + opts.title.replace(/[^a-zA-Z0-9]/g, "-");
$("#" + buttonId).remove();
const icon = $("<span>")
.addClass("ui-icon ui-icon-" + opts.icon);
const button = $('<button>')
.addClass("btn btn-xs")
.click(opts.action)
.append(icon)
.append($("<span>").text(opts.title));
button.attr('id', buttonId);
$pluginButtons.append(button);
if (opts.shortcut) {
$(document).bind('keydown', opts.shortcut, opts.action);
button.attr("title", "Shortcut " + opts.shortcut);
}
};
function prepareParams(params) {
if (!params) {
return params;
}
return params.map(p => {
if (typeof p === "function") {
return "!@#Function: " + p.toString();
}
else {
return p;
}
});
}
/**
* Executes given anonymous function on the server.
* Internally this serializes the anonymous function into string and sends it to backend via AJAX.
*
* @param {string} script - script to be executed on the backend
* @param {Array.<?>} params - list of parameters to the anonymous function to be send to backend
* @return {Promise<*>} return value of the executed function on the backend
*/
this.runOnServer = async (script, params = []) => {
if (typeof script === "function") {
script = script.toString();
}
const ret = await server.post('script/exec', {
script: script,
params: prepareParams(params),
startNoteId: startNote.noteId,
currentNoteId: currentNote.noteId,
originEntityName: "notes", // currently there's no other entity on frontend which can trigger event
originEntityId: originEntity ? originEntity.noteId : null
});
if (ret.success) {
return ret.executionResult;
}
else {
throw new Error("server error: " + ret.error);
}
};
/**
* Returns list of notes. If note is missing from cache, it's loaded.
*
* This is often used to bulk-fill the cache with notes which would have to be picked one by one
* otherwise (by e.g. createNoteLink())
*
* @param {string[]} noteIds
* @param {boolean} [silentNotFoundError] - don't report error if the note is not found
* @return {Promise<NoteShort[]>}
*/
this.getNotes = async (noteIds, silentNotFoundError = false) => await treeCache.getNotes(noteIds, silentNotFoundError);
/**
* Instance name identifies particular Trilium instance. It can be useful for scripts
* if some action needs to happen on only one specific instance.
*
* @return {string}
*/
this.getInstanceName = () => window.glob.instanceName;
/**
* @method
* @param {Date} date
* @returns {string} date in YYYY-MM-DD format
*/
this.formatDateISO = utils.formatDateISO;
/**
* @method
* @param {string} str
* @returns {Date} parsed object
*/
this.parseDate = utils.parseDate;
/**
* Show info message to the user.
*
* @method
* @param {string} message
*/
this.showMessage = infoService.showMessage;
/**
* Show error message to the user.
*
* @method
* @param {string} message
*/
this.showError = infoService.showError;
/**
* Refresh tree
*
* @method
* @returns {Promise<void>}
*/
this.refreshTree = treeService.reload;
/**
* Create note link (jQuery object) for given note.
*
* @method
* @param {string} notePath (or noteId)
* @param {string} [noteTitle] - if not present we'll use note title
*/
this.createNoteLink = linkService.createNoteLink;
}
export default FrontendScriptApi;

View File

@@ -14,6 +14,12 @@ function showMessage(message) {
});
}
function showAndLogError(message, delay = 10000) {
showError(message, delay);
messagingService.logError(message);
}
function showError(message, delay = 10000) {
console.log(utils.now(), "error: ", message);
@@ -36,5 +42,6 @@ function throwError(message) {
export default {
showMessage,
showError,
showAndLogError,
throwError
}

View File

@@ -3,7 +3,7 @@ import noteDetailText from './note_detail_text.js';
import treeUtils from './tree_utils.js';
function getNotePathFromLink(url) {
const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url);
const notePathMatch = /#root([A-Za-z0-9/]*)$/.exec(url);
if (notePathMatch === null) {
return null;
@@ -13,8 +13,8 @@ function getNotePathFromLink(url) {
}
}
function getNodePathFromLabel(label) {
const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
function getNotePathFromLabel(label) {
const notePathMatch = / \(([#A-Za-z0-9/]+)\)/.exec(label);
if (notePathMatch !== null) {
return notePathMatch[1];
@@ -23,7 +23,7 @@ function getNodePathFromLabel(label) {
return null;
}
async function createNoteLink(notePath, noteTitle) {
async function createNoteLink(notePath, noteTitle = null) {
if (!noteTitle) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
@@ -33,8 +33,8 @@ async function createNoteLink(notePath, noteTitle) {
const noteLink = $("<a>", {
href: 'javascript:',
text: noteTitle
}).attr('action', 'note')
.attr('note-path', notePath);
}).attr('data-action', 'note')
.attr('data-note-path', notePath);
return noteLink;
}
@@ -43,10 +43,10 @@ function goToLink(e) {
e.preventDefault();
const $link = $(e.target);
let notePath = $link.attr("note-path");
let notePath = $link.attr("data-note-path");
if (!notePath) {
const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
const address = $link.attr("data-note-path") ? $link.attr("data-note-path") : $link.attr('href');
if (!address) {
return;
@@ -61,7 +61,7 @@ function goToLink(e) {
notePath = getNotePathFromLink(address);
}
treeService.activateNode(notePath);
treeService.activateNote(notePath);
// this is quite ugly hack, but it seems like we can't close the tooltip otherwise
$("[role='tooltip']").remove();
@@ -90,14 +90,26 @@ function addTextToEditor(text) {
doc.enqueueChanges(() => editor.data.insertText(text), doc.selection);
}
ko.bindingHandlers.noteLink = {
init: async function(element, valueAccessor, allBindings, viewModel, bindingContext) {
const noteId = ko.unwrap(valueAccessor());
if (noteId) {
const link = await createNoteLink(noteId);
$(element).append(link);
}
}
};
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
// of opening the link in new window/tab
$(document).on('click', "a[action='note']", goToLink);
$(document).on('click', "a[data-action='note']", goToLink);
$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
$(document).on('dblclick', '#note-detail-text a', goToLink);
export default {
getNodePathFromLabel,
getNotePathFromLabel,
getNotePathFromLink,
createNoteLink,
addLinkToEditor,

View File

@@ -1,8 +1,9 @@
import utils from './utils.js';
import infoService from "./info.js";
const $changesToPushCount = $("#changes-to-push-count");
const $outstandingSyncsCount = $("#outstanding-syncs-count");
const syncMessageHandlers = [];
const messageHandlers = [];
let ws;
@@ -25,9 +26,17 @@ function subscribeToMessages(messageHandler) {
messageHandlers.push(messageHandler);
}
function subscribeToSyncMessages(messageHandler) {
syncMessageHandlers.push(messageHandler);
}
function handleMessage(event) {
const message = JSON.parse(event.data);
for (const messageHandler of messageHandlers) {
messageHandler(message);
}
if (message.type === 'sync') {
lastPingTs = new Date().getTime();
@@ -39,11 +48,11 @@ function handleMessage(event) {
const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
for (const messageHandler of messageHandlers) {
messageHandler(syncData);
for (const syncMessageHandler of syncMessageHandlers) {
syncMessageHandler(syncData);
}
$changesToPushCount.html(message.changesToPushCount);
$outstandingSyncsCount.html(message.outstandingSyncs);
}
else if (message.type === 'sync-hash-check-failed') {
infoService.showError("Sync check failed!", 60000);
@@ -73,26 +82,10 @@ setTimeout(() => {
lastSyncId = glob.maxSyncIdAtLoad;
lastPingTs = new Date().getTime();
let connectionBrokenNotification = null;
setInterval(async () => {
if (new Date().getTime() - lastPingTs > 30000) {
if (!connectionBrokenNotification) {
connectionBrokenNotification = $.notify({
// options
message: "Lost connection to server"
},{
// options
type: 'danger',
delay: 100000000 // keep it until we explicitly close it
});
}
}
else if (connectionBrokenNotification) {
await connectionBrokenNotification.close();
connectionBrokenNotification = null;
infoService.showMessage("Re-connected to server");
console.log("Lost connection to server");
}
ws.send(JSON.stringify({
@@ -104,5 +97,6 @@ setTimeout(() => {
export default {
logError,
subscribeToMessages
subscribeToMessages,
subscribeToSyncMessages
};

View File

@@ -0,0 +1,60 @@
import server from "./server.js";
import noteDetailService from "./note_detail.js";
async function autocompleteSource(request, response) {
const result = await server.get('autocomplete'
+ '?query=' + encodeURIComponent(request.term)
+ '&currentNoteId=' + noteDetailService.getCurrentNoteId());
if (result.length > 0) {
response(result.map(row => {
return {
label: row.label,
value: row.label + ' (' + row.value + ')'
}
}));
}
else {
response([{
label: "No results",
value: "No results"
}]);
}
}
async function initNoteAutocomplete($el) {
if (!$el.hasClass("ui-autocomplete-input")) {
const $showRecentNotesButton = $("<span>")
.addClass("input-group-addon show-recent-notes-button")
.prop("title", "Show recent notes");
$el.after($showRecentNotesButton);
$showRecentNotesButton.click(() => $el.autocomplete("search", ""));
await $el.autocomplete({
appendTo: $el.parent().parent(),
source: autocompleteSource,
minLength: 0,
change: function (event, ui) {
$el.trigger("change");
},
select: function (event, ui) {
if (ui.item.value === 'No results') {
return false;
}
}
});
}
}
ko.bindingHandlers.noteAutocomplete = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
initNoteAutocomplete($(element));
}
};
export default {
initNoteAutocomplete,
autocompleteSource
}

View File

@@ -7,6 +7,7 @@ import utils from './utils.js';
import server from './server.js';
import messagingService from "./messaging.js";
import infoService from "./info.js";
import linkService from "./link.js";
import treeCache from "./tree_cache.js";
import NoteFull from "../entities/note_full.js";
import noteDetailCode from './note_detail_code.js';
@@ -14,6 +15,8 @@ import noteDetailText from './note_detail_text.js';
import noteDetailFile from './note_detail_file.js';
import noteDetailSearch from './note_detail_search.js';
import noteDetailRender from './note_detail_render.js';
import bundleService from "./bundle.js";
import noteAutocompleteService from "./note_autocomplete.js";
const $noteTitle = $("#note-title");
@@ -23,9 +26,11 @@ const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display");
const $labelList = $("#label-list");
const $labelListInner = $("#label-list-inner");
const $attributeList = $("#attribute-list");
const $attributeListInner = $("#attribute-list-inner");
const $childrenOverview = $("#children-overview");
const $scriptArea = $("#note-detail-script-area");
const $promotedAttributesContainer = $("#note-detail-promoted-attributes");
let currentNote = null;
@@ -114,11 +119,12 @@ async function saveNoteIfChanged() {
}
function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.isProtected;
const isProtected = note.isProtected;
$noteDetailWrapper.toggleClass("protected", isProtected);
$protectButton.toggle(!isProtected);
$unprotectButton.toggle(isProtected);
$protectButton.toggleClass("active", isProtected);
$unprotectButton.toggleClass("active", !isProtected);
$unprotectButton.prop("disabled", !protectedSessionHolder.isProtectedSessionAvailable());
}
let isNewNoteCreated = false;
@@ -150,7 +156,7 @@ async function loadNoteDetail(noteId) {
$noteIdDisplay.html(noteId);
await handleProtectedSession();
setNoteBackgroundIfProtected(currentNote);
$noteDetailWrapper.show();
@@ -164,21 +170,26 @@ async function loadNoteDetail(noteId) {
$noteDetailComponents.hide();
await handleProtectedSession();
await getComponent(currentNote.type).show();
}
finally {
noteChangeDisabled = false;
}
setNoteBackgroundIfProtected(currentNote);
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
$noteDetailWrapper.scrollTop(0);
const labels = await loadLabelList();
$scriptArea.html('');
const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview');
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
const attributes = await loadAttributes();
const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview');
await showChildrenOverview(hideChildrenOverview);
}
@@ -198,7 +209,7 @@ async function showChildrenOverview(hideChildrenOverview) {
const link = $('<a>', {
href: 'javascript:',
text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
}).attr('action', 'note').attr('note-path', notePath + '/' + childBranch.noteId);
}).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId);
const childEl = $('<div class="child-overview">').html(link);
$childrenOverview.append(childEl);
@@ -207,25 +218,230 @@ async function showChildrenOverview(hideChildrenOverview) {
$childrenOverview.show();
}
async function loadLabelList() {
async function loadAttributes() {
$promotedAttributesContainer.empty();
$attributeList.hide();
const noteId = getCurrentNoteId();
const labels = await server.get('notes/' + noteId + '/labels');
const attributes = await server.get('notes/' + noteId + '/attributes');
$labelListInner.html('');
const promoted = attributes.filter(attr =>
(attr.type === 'label-definition' || attr.type === 'relation-definition')
&& !attr.name.startsWith("child:")
&& attr.value.isPromoted);
if (labels.length > 0) {
for (const label of labels) {
$labelListInner.append(utils.formatLabel(label) + " ");
let idx = 1;
async function createRow(definitionAttr, valueAttr) {
const definition = definitionAttr.value;
const inputId = "promoted-input-" + idx;
const $tr = $("<tr>");
const $labelCell = $("<th>").append(valueAttr.name);
const $input = $("<input>")
.prop("id", inputId)
.prop("tabindex", definitionAttr.position)
.prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
.prop("attribute-type", valueAttr.type)
.prop("attribute-name", valueAttr.name)
.prop("value", valueAttr.value)
.addClass("form-control")
.addClass("promoted-attribute-input");
idx++;
const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input));
const $actionCell = $("<td>");
const $multiplicityCell = $("<td>");
$tr
.append($labelCell)
.append($inputCell)
.append($actionCell)
.append($multiplicityCell);
if (valueAttr.type === 'label') {
if (definition.labelType === 'text') {
$input.prop("type", "text");
// no need to await for this, can be done asynchronously
server.get('attributes/values/' + encodeURIComponent(valueAttr.name)).then(attributeValues => {
if (attributeValues.length === 0) {
return;
}
$input.autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in autocomplete.js
source: attributeValues.map(attribute => {
return {
attribute: attribute,
value: attribute
}
}),
minLength: 0
});
$input.focus(() => $input.autocomplete("search", ""));
});
}
else if (definition.labelType === 'number') {
$input.prop("type", "number");
}
else if (definition.labelType === 'boolean') {
$input.prop("type", "checkbox");
if (valueAttr.value === "true") {
$input.prop("checked", "checked");
}
}
else if (definition.labelType === 'date') {
$input.prop("type", "text");
$input.datepicker({
changeMonth: true,
changeYear: true,
yearRange: "c-200:c+10",
dateFormat: "yy-mm-dd"
});
const $todayButton = $("<button>").addClass("btn btn-small").text("Today").click(() => {
$input.val(utils.formatDateISO(new Date()));
$input.trigger("change");
});
$actionCell.append($todayButton);
}
else if (definition.labelType === 'url') {
$input.prop("placeholder", "http://website...");
const $openButton = $("<button>").addClass("btn btn-small").text("Open").click(() => {
window.open($input.val(), '_blank');
});
$actionCell.append($openButton);
}
else {
messagingService.logError("Unknown labelType=" + definitionAttr.labelType);
}
}
else if (valueAttr.type === 'relation') {
if (valueAttr.value) {
$input.val((await treeUtils.getNoteTitle(valueAttr.value) + " (" + valueAttr.value + ")"));
}
// no need to wait for this
noteAutocompleteService.initNoteAutocomplete($input);
// ideally we'd use link instead of button which would allow tooltip preview, but
// we can't guarantee updating the link in the a element
const $openButton = $("<button>").addClass("btn btn-small").text("Open").click(() => {
const notePath = linkService.getNotePathFromLabel($input.val());
treeService.activateNote(notePath);
});
$actionCell.append($openButton);
}
else {
messagingService.logError("Unknown attribute type=" + valueAttr.type);
return;
}
$labelList.show();
}
else {
$labelList.hide();
if (definition.multiplicityType === "multivalue") {
const addButton = $("<span>")
.addClass("glyphicon glyphicon-plus pointer")
.prop("title", "Add new attribute")
.click(async () => {
const $new = await createRow(definitionAttr, {
attributeId: "",
type: valueAttr.type,
name: definitionAttr.name,
value: ""
});
$tr.after($new);
$new.find('input').focus();
});
const removeButton = $("<span>")
.addClass("glyphicon glyphicon-trash pointer")
.prop("title", "Remove this attribute")
.click(async () => {
if (valueAttr.attributeId) {
await server.remove("notes/" + noteId + "/attributes/" + valueAttr.attributeId);
}
$tr.remove();
});
$multiplicityCell.append(addButton).append(" &nbsp; ").append(removeButton);
}
return $tr;
}
return labels;
if (promoted.length > 0) {
const $tbody = $("<tbody>");
for (const definitionAttr of promoted) {
const definitionType = definitionAttr.type;
const valueType = definitionType.substr(0, definitionType.length - 11);
let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType);
if (valueAttrs.length === 0) {
valueAttrs.push({
attributeId: "",
type: valueType,
name: definitionAttr.name,
value: ""
});
}
if (definitionAttr.value.multiplicityType === 'singlevalue') {
valueAttrs = valueAttrs.slice(0, 1);
}
for (const valueAttr of valueAttrs) {
const $tr = await createRow(definitionAttr, valueAttr);
$tbody.append($tr);
}
}
// we replace the whole content in one step so there can't be any race conditions
// (previously we saw promoted attributes doubling)
$promotedAttributesContainer.empty().append($tbody);
}
else {
$attributeListInner.html('');
if (attributes.length > 0) {
for (const attribute of attributes) {
if (attribute.type === 'label') {
$attributeListInner.append(utils.formatLabel(attribute) + " ");
}
else if (attribute.type === 'relation') {
$attributeListInner.append(attribute.name + "=");
$attributeListInner.append(await linkService.createNoteLink(attribute.value));
$attributeListInner.append(" ");
}
else if (attribute.type === 'label-definition' || attribute.type === 'relation-definition') {
$attributeListInner.append(attribute.name + " definition ");
}
else {
messagingService.logError("Unknown attr type: " + attribute.type);
}
}
$attributeList.show();
}
}
return attributes;
}
async function loadNote(noteId) {
@@ -240,7 +456,7 @@ function focus() {
getComponent(note.type).focus();
}
messagingService.subscribeToMessages(syncData => {
messagingService.subscribeToSyncMessages(syncData => {
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getCurrentNoteId())) {
infoService.showMessage('Reloading note because of background changes');
@@ -248,6 +464,35 @@ messagingService.subscribeToMessages(syncData => {
}
});
$promotedAttributesContainer.on('change', '.promoted-attribute-input', async event => {
const $attr = $(event.target);
let value;
if ($attr.prop("type") === "checkbox") {
value = $attr.is(':checked') ? "true" : "false";
}
else if ($attr.prop("attribute-type") === "relation") {
if ($attr.val()) {
value = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel($attr.val()));
}
}
else {
value = $attr.val();
}
const result = await server.put("notes/" + getCurrentNoteId() + "/attribute", {
attributeId: $attr.prop("attribute-id"),
type: $attr.prop("attribute-type"),
name: $attr.prop("attribute-name"),
value: value
});
$attr.prop("attribute-id", result.attributeId);
infoService.showMessage("Attribute has been saved.");
});
$(document).ready(() => {
$noteTitle.on('input', () => {
noteChanged();
@@ -276,7 +521,7 @@ export default {
getCurrentNoteId,
newNoteCreated,
focus,
loadLabelList,
loadAttributes,
saveNote,
saveNoteIfChanged,
noteChanged

View File

@@ -32,7 +32,7 @@ async function show() {
lint: true,
gutters: ["CodeMirror-lint-markers"],
lineNumbers: true,
tabindex: 2 // so that tab from title will lead to code editor focus
tabindex: 100
});
codeEditor.on('change', noteDetailService.noteChanged);
@@ -75,9 +75,7 @@ async function executeCurrentNote() {
const currentNote = noteDetailService.getCurrentNote();
if (currentNote.mime.endsWith("env=frontend")) {
const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
bundleService.executeBundle(bundle);
await bundleService.getAndExecuteBundle(noteDetailService.getCurrentNoteId());
}
if (currentNote.mime.endsWith("env=backend")) {

View File

@@ -14,13 +14,13 @@ const $fileOpen = $("#file-open");
async function show() {
const currentNote = noteDetailService.getCurrentNote();
const labels = await server.get('notes/' + currentNote.noteId + '/labels');
const labelMap = utils.toObject(labels, l => [l.name, l.value]);
const attributes = await server.get('notes/' + currentNote.noteId + '/attributes');
const attributeMap = utils.toObject(attributes, l => [l.name, l.value]);
$noteDetailFile.show();
$fileFileName.text(labelMap.original_file_name);
$fileFileSize.text(labelMap.file_size + " bytes");
$fileFileName.text(attributeMap.originalFileName);
$fileFileSize.text(attributeMap.fileSize + " bytes");
$fileFileType.text(currentNote.mime);
}

View File

@@ -13,7 +13,7 @@ let codeEditorInitialized;
async function show() {
codeEditorInitialized = false;
$noteDetailRender.show();
$noteDetailRender.empty().show();
await render();
}

View File

@@ -11,7 +11,13 @@ async function show() {
textEditor = await BalloonEditor.create($noteDetailText[0], {});
textEditor.model.document.on('change', noteDetailService.noteChanged);
textEditor.model.document.on('change', () => {
// change is triggered on just marker/selection changes which is not interesting for us
if (textEditor.model.document.differ.getChanges().length > 0) {
noteDetailService.noteChanged();
}
}
);
}
textEditor.setData(noteDetailService.getCurrentNote().content);

View File

@@ -0,0 +1,9 @@
import server from "./server.js";
const optionsReady = new Promise((resolve, reject) => {
$(document).ready(() => server.get('options').then(resolve));
});
export default {
optionsReady
}

View File

@@ -32,6 +32,7 @@ function ensureProtectedSession(requireProtectedSession, modal) {
const dfd = $.Deferred();
if (requireProtectedSession && !protectedSessionHolder.isProtectedSessionAvailable()) {
// using deferred instead of promise because it allows resolving from outside
protectedSessionDeferred = dfd;
if (treeService.getCurrentNode().data.isProtected) {
@@ -39,7 +40,9 @@ function ensureProtectedSession(requireProtectedSession, modal) {
}
$dialog.dialog({
modal: modal,
// everything is now non-modal, because modal dialog caused weird high CPU usage on opening
// and tearing of text input
modal: false,
width: 400,
open: () => {
if (!modal) {
@@ -80,11 +83,10 @@ async function setupProtectedSession() {
$noteDetailWrapper.show();
protectedSessionDeferred.resolve();
protectedSessionDeferred = null;
$protectedSessionOnButton.addClass('active');
$protectedSessionOffButton.removeClass('active');
protectedSessionDeferred = null;
}
}
@@ -105,6 +107,10 @@ async function enterProtectedSessionOnServer(password) {
}
async function protectNoteAndSendToServer() {
if (noteDetailService.getCurrentNote().isProtected) {
return;
}
await ensureProtectedSession(true, true);
const note = noteDetailService.getCurrentNote();
@@ -118,7 +124,18 @@ async function protectNoteAndSendToServer() {
}
async function unprotectNoteAndSendToServer() {
await ensureProtectedSession(true, true);
if (!noteDetailService.getCurrentNote().isProtected) {
return;
}
if (!protectedSessionHolder.isProtectedSessionAvailable()) {
console.log("Unprotecting notes outside of protected session is not allowed.");
// the reason is that it's not easy to handle even with ensureProtectedSession,
// because we would first have to make sure the note is loaded and only then unprotect
// we used to have a bug where we would overwrite the previous note with unprotected content.
return;
}
const note = noteDetailService.getCurrentNote();
note.isProtected = false;

View File

@@ -1,13 +1,11 @@
import utils from "./utils.js";
import server from "./server.js";
import optionsInitService from './options_init.js';
let lastProtectedSessionOperationDate = null;
let protectedSessionTimeout = null;
let protectedSessionId = null;
$(document).ready(() => {
server.get('options').then(options => protectedSessionTimeout = options.protectedSessionTimeout);
});
optionsInitService.optionsReady.then(options => protectedSessionTimeout = options.protectedSessionTimeout);
setInterval(() => {
if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {

Some files were not shown because too many files have changed in this diff Show More