Compare commits

...

204 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
azivner
c07785be67 release 0.14.0 2018-05-31 23:23:44 -04:00
azivner
80d2457b23 moved parent list next to note title 2018-05-31 23:21:47 -04:00
azivner
5dde2752d2 add switch to manually enter/leave protected session, fixes #107 2018-05-31 20:00:39 -04:00
azivner
8bf4633cd0 fixes 2018-05-30 23:18:56 -04:00
azivner
bd66b8a1c8 fix issue with limitation of number of SQLite parameters (999) which caused problems when loading tree which was too expanded 2018-05-30 20:28:10 -04:00
azivner
be51e533fc OPML import support (issue #78) 2018-05-29 20:32:13 -04:00
azivner
f47ae12019 OPML export support (issue #78), import missing for now 2018-05-27 12:26:34 -04:00
azivner
cab54a458f unifying surrogate keys for event log and options, fixes #103 2018-05-26 23:25:09 -04:00
azivner
a30734f1bc Add history backwards/forwards buttons, fixes #94 2018-05-26 22:54:06 -04:00
azivner
7ad9f7b129 fixed layouting issues 2018-05-26 19:58:08 -04:00
azivner
40a32e6826 render notes can be edited and can contain HTML markup 2018-05-26 19:27:47 -04:00
azivner
ab0486aaf1 expose root node, fixes #101 2018-05-26 16:16:34 -04:00
azivner
874593a167 fix code editor growing 2018-05-26 15:28:36 -04:00
azivner
03bf33630e unify audit fields, fixes #102 2018-05-26 12:38:25 -04:00
azivner
933cce1b94 fix hideInAutocomplete bug 2018-05-26 10:50:13 -04:00
azivner
4a6ff573f8 fixed autocomplete issues with capitalization 2018-05-26 10:24:33 -04:00
azivner
1a737f7d19 expose add link on UI, fixes #95 2018-05-26 10:04:40 -04:00
226 changed files with 10898 additions and 6824 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 *.db
config.ini config.ini
cert.key cert.key
cert.crt cert.crt
docs/

View File

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

View File

@@ -3,15 +3,10 @@
instanceName= instanceName=
[Network] [Network]
# port setting is relevant only for web deployments, desktop builds run on random free port
port=8080 port=8080
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure). # true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
https=false 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= certPath=
keyPath= keyPath=
[Sync]
syncServerHost=
syncServerTimeout=10000
syncProxy=
syncServerCertificate=

View File

@@ -0,0 +1,30 @@
ALTER TABLE branches ADD dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
CREATE TABLE `event_log_mig` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`noteId` TEXT,
`comment` TEXT,
`dateCreated` TEXT NOT NULL
);
INSERT INTO event_log_mig (id, noteId, comment, dateCreated)
SELECT id, noteId, comment, dateAdded FROM event_log;
DROP TABLE event_log;
ALTER TABLE event_log_mig RENAME TO event_log;
ALTER TABLE options ADD dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
CREATE TABLE `recent_notes_mig` (
`branchId` TEXT NOT NULL PRIMARY KEY,
`notePath` TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
`dateCreated` TEXT NOT NULL,
isDeleted INT
);
INSERT INTO recent_notes_mig (branchId, notePath, hash, dateCreated, isDeleted)
SELECT branchId, notePath, hash, dateAccessed, isDeleted FROM recent_notes;
DROP TABLE recent_notes;
ALTER TABLE recent_notes_mig RENAME TO recent_notes;

View File

@@ -0,0 +1 @@
UPDATE notes SET mime = 'text/html' WHERE type = 'render';

View File

@@ -0,0 +1,29 @@
CREATE TABLE `event_log_mig` (
`eventId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT,
`comment` TEXT,
`dateCreated` TEXT NOT NULL
);
INSERT INTO event_log_mig (eventId, noteId, comment, dateCreated)
SELECT id, noteId, comment, dateCreated FROM event_log;
DROP TABLE event_log;
ALTER TABLE event_log_mig RENAME TO event_log;
create table options_mig
(
optionId TEXT NOT NULL PRIMARY KEY,
name TEXT not null,
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 (optionId, name, value, dateModified, isSynced, hash, dateCreated)
SELECT name || "_key", name, value, dateModified, isSynced, hash, dateCreated FROM options;
DROP TABLE options;
ALTER TABLE options_mig RENAME TO options;

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" ( CREATE TABLE IF NOT EXISTS "sync" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL, `entityName` TEXT NOT NULL,
@@ -29,7 +24,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (
`isProtected` INT NOT NULL DEFAULT 0, `isProtected` INT NOT NULL DEFAULT 0,
`dateModifiedFrom` TEXT NOT NULL, `dateModifiedFrom` TEXT NOT NULL,
`dateModifiedTo` 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` ( CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
`noteId` `noteId`
); );
@@ -49,7 +44,7 @@ CREATE TABLE IF NOT EXISTS "images"
isDeleted INT NOT NULL DEFAULT 0, isDeleted INT NOT NULL DEFAULT 0,
dateModified TEXT NOT NULL, dateModified TEXT NOT NULL,
dateCreated TEXT NOT NULL dateCreated TEXT NOT NULL
); , hash TEXT DEFAULT "" NOT NULL);
CREATE TABLE note_images CREATE TABLE note_images
( (
noteImageId TEXT PRIMARY KEY NOT NULL, noteImageId TEXT PRIMARY KEY NOT NULL,
@@ -58,7 +53,7 @@ CREATE TABLE note_images
isDeleted INT NOT NULL DEFAULT 0, isDeleted INT NOT NULL DEFAULT 0,
dateModified TEXT NOT NULL, dateModified TEXT NOT NULL,
dateCreated 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_noteId ON note_images (noteId);
CREATE INDEX IDX_note_images_imageId ON note_images (imageId); CREATE INDEX IDX_note_images_imageId ON note_images (imageId);
CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, 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, token TEXT NOT NULL,
dateCreated TEXT NOT NULL, dateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0 isDeleted INT NOT NULL DEFAULT 0
); , hash TEXT DEFAULT "" NOT NULL);
CREATE TABLE IF NOT EXISTS "branches" ( CREATE TABLE IF NOT EXISTS "branches" (
`branchId` TEXT NOT NULL, `branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL, `noteId` TEXT NOT NULL,
@@ -77,7 +72,7 @@ CREATE TABLE IF NOT EXISTS "branches" (
`prefix` TEXT, `prefix` TEXT,
`isExpanded` BOOLEAN, `isExpanded` BOOLEAN,
`isDeleted` INTEGER NOT NULL DEFAULT 0, `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`) PRIMARY KEY(`branchId`)
); );
CREATE INDEX `IDX_branches_noteId` ON `branches` ( CREATE INDEX `IDX_branches_noteId` ON `branches` (
@@ -87,34 +82,6 @@ CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
`noteId`, `noteId`,
`parentNoteId` `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" ( CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL, `noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "unnamed", `title` TEXT NOT NULL DEFAULT "unnamed",
@@ -124,9 +91,43 @@ CREATE TABLE IF NOT EXISTS "notes" (
`dateCreated` TEXT NOT NULL, `dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL, `dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text', 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`) PRIMARY KEY(`noteId`)
); );
CREATE INDEX `IDX_notes_isDeleted` ON `notes` ( CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
`isDeleted` 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 electron = require('electron');
const path = require('path'); const path = require('path');
const config = require('./src/services/config');
const log = require('./src/services/log'); const log = require('./src/services/log');
const url = require("url"); const url = require("url");
const port = require('./src/services/port');
const app = electron.app; const app = electron.app;
const globalShortcut = electron.globalShortcut; const globalShortcut = electron.globalShortcut;
@@ -23,7 +23,7 @@ function onClosed() {
mainWindow = null; mainWindow = null;
} }
function createMainWindow() { async function createMainWindow() {
const win = new electron.BrowserWindow({ const win = new electron.BrowserWindow({
width: 1200, width: 1200,
height: 900, height: 900,
@@ -31,14 +31,12 @@ function createMainWindow() {
icon: path.join(__dirname, 'src/public/images/app-icons/png/256x256.png') icon: path.join(__dirname, 'src/public/images/app-icons/png/256x256.png')
}); });
const port = config['Network']['port'] || '3000';
win.setMenu(null); win.setMenu(null);
win.loadURL('http://localhost:' + port); win.loadURL('http://localhost:' + await port);
win.on('closed', onClosed); win.on('closed', onClosed);
win.webContents.on('new-window', (e, url) => { win.webContents.on('new-window', (e, url) => {
if (url !== mainWindow.webContents.getURL()) { if (url !== win.webContents.getURL()) {
e.preventDefault(); e.preventDefault();
require('electron').shell.openExternal(url); require('electron').shell.openExternal(url);
} }

8428
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,12 @@
{ {
"name": "trilium", "name": "trilium",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.13.0-beta", "version": "0.20.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": {
"trilium": "./src/www"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/zadam/trilium.git" "url": "https://github.com/zadam/trilium.git"
@@ -17,59 +20,65 @@
"start-forge": "electron-forge start", "start-forge": "electron-forge start",
"package-forge": "electron-forge package", "package-forge": "electron-forge package",
"make-forge": "electron-forge make", "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": { "dependencies": {
"async-mutex": "^0.1.3", "async-mutex": "0.1.3",
"axios": "^0.18", "axios": "0.18",
"body-parser": "^1.18.3", "body-parser": "1.18.3",
"cls-hooked": "^4.2.2", "cls-hooked": "4.2.2",
"cookie-parser": "~1.4.3", "cookie-parser": "1.4.3",
"debug": "~3.1.0", "debug": "3.1.0",
"devtron": "^1.4.0", "devtron": "1.4.0",
"ejs": "~2.6.1", "ejs": "2.6.1",
"electron-debug": "^1.5.0", "electron-debug": "2.0.0",
"electron-dl": "^1.12.0", "electron-dl": "1.12.0",
"electron-in-page-search": "^1.3.2", "electron-in-page-search": "1.3.2",
"express": "~4.16.3", "express": "4.16.3",
"express-session": "^1.15.6", "express-session": "1.15.6",
"fs-extra": "^6.0.1", "fs-extra": "7.0.0",
"helmet": "^3.12.1", "get-port": "4.0.0",
"html": "^1.0.0", "helmet": "3.13.0",
"image-type": "^3.0.0", "html": "1.0.0",
"imagemin": "^5.3.1", "image-type": "3.0.0",
"imagemin-giflossy": "^5.1.10", "imagemin": "6.0.0",
"imagemin-mozjpeg": "^7.0.0", "imagemin-giflossy": "5.1.10",
"imagemin-pngquant": "^5.1.0", "imagemin-mozjpeg": "7.0.0",
"ini": "^1.3.5", "imagemin-pngquant": "6.0.0",
"jimp": "^0.2.28", "ini": "1.3.5",
"moment": "^2.22.1", "jimp": "0.3.5",
"multer": "^1.3.0", "moment": "2.22.2",
"multer": "1.3.1",
"open": "0.0.5", "open": "0.0.5",
"rand-token": "^0.4.0", "rand-token": "0.4.0",
"rcedit": "^1.1.0", "rcedit": "1.1.0",
"request": "^2.87.0", "request": "2.88.0",
"request-promise": "^4.2.2", "request-promise": "4.2.2",
"rimraf": "^2.6.2", "rimraf": "2.6.2",
"sanitize-filename": "^1.6.1", "sanitize-filename": "1.6.1",
"scrypt": "^6.0.3", "scrypt": "6.0.3",
"serve-favicon": "~2.5.0", "serve-favicon": "2.5.0",
"session-file-store": "^1.2.0", "session-file-store": "1.2.0",
"simple-node-logger": "^0.93.37", "simple-node-logger": "0.93.37",
"sqlite": "^2.9.2", "sqlite": "3.0.0",
"tar-stream": "^1.6.1", "tar-stream": "1.6.1",
"unescape": "^1.0.1", "unescape": "1.0.1",
"ws": "^5.2.0" "ws": "6.0.0",
"xml2js": "0.4.19"
}, },
"devDependencies": { "devDependencies": {
"electron": "^2.0.1", "electron": "2.0.7",
"electron-compile": "^6.4.2", "electron-compile": "6.4.3",
"electron-packager": "^12.1.0", "electron-packager": "12.1.1",
"electron-prebuilt-compile": "2.0.0", "electron-prebuilt-compile": "2.0.7",
"electron-rebuild": "^1.7.3", "electron-rebuild": "1.8.2",
"lorem-ipsum": "^1.0.4", "lorem-ipsum": "1.0.5",
"tape": "^4.9.0", "tape": "4.9.1",
"xo": "^0.21.1" "xo": "0.22.0",
"pkg": "4.3.4"
}, },
"config": { "config": {
"forge": { "forge": {
@@ -108,5 +117,15 @@
"node", "node",
"browser" "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 sessionSecret = require('./services/session_secret');
const cls = require('./services/cls'); const cls = require('./services/cls');
require('./entities/entity_constructor'); require('./entities/entity_constructor');
require('./services/handlers');
const app = express(); const app = express();
@@ -47,7 +48,7 @@ const sessionParser = session({
cookie: { cookie: {
// path: "/", // path: "/",
httpOnly: true, httpOnly: true,
maxAge: 1800000 maxAge: 24 * 60 * 60 * 1000 // in milliseconds
}, },
store: new FileStore({ store: new FileStore({
ttl: 30 * 24 * 3600, ttl: 30 * 24 * 3600,

View File

@@ -3,14 +3,22 @@
const Entity = require('./entity'); const Entity = require('./entity');
const dateUtils = require('../services/date_utils'); 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 { class ApiToken extends Entity {
static get tableName() { return "api_tokens"; } static get entityName() { return "api_tokens"; }
static get primaryKeyName() { return "apiTokenId"; } static get primaryKeyName() { return "apiTokenId"; }
static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; } static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
beforeSaving() { beforeSaving() {
super.beforeSaving();
if (!this.isDeleted) { if (!this.isDeleted) {
this.isDeleted = false; this.isDeleted = false;
} }
@@ -18,6 +26,8 @@ class ApiToken extends Entity {
if (!this.dateCreated) { if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate(); 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 repository = require('../services/repository');
const sql = require('../services/sql'); 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 { class Branch extends Entity {
static get tableName() { return "branches"; } static get entityName() { return "branches"; }
static get primaryKeyName() { return "branchId"; } static get primaryKeyName() { return "branchId"; }
// notePosition is not part of hash because it would produce a lot of updates in case of reordering // 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() { async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
} }
async beforeSaving() { async beforeSaving() {
super.beforeSaving();
if (this.notePosition === undefined) { if (this.notePosition === undefined) {
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]); const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 1; this.notePosition = maxNotePos === null ? 0 : maxNotePos + 1;
@@ -27,7 +41,15 @@ class Branch extends Entity {
this.isDeleted = false; this.isDeleted = false;
} }
this.dateModified = dateUtils.nowDate() if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
} }
} }

View File

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

View File

@@ -3,17 +3,37 @@ const NoteRevision = require('../entities/note_revision');
const Image = require('../entities/image'); const Image = require('../entities/image');
const NoteImage = require('../entities/note_image'); const NoteImage = require('../entities/note_image');
const Branch = require('../entities/branch'); const Branch = require('../entities/branch');
const Label = require('../entities/label'); const Attribute = require('../entities/attribute');
const RecentNote = require('../entities/recent_note'); const RecentNote = require('../entities/recent_note');
const ApiToken = require('../entities/api_token'); const ApiToken = require('../entities/api_token');
const Option = require('../entities/option'); const Option = require('../entities/option');
const repository = require('../services/repository'); 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) { function createEntityFromRow(row) {
let entity; let entity;
if (row.labelId) { if (row.attributeId) {
entity = new Label(row); entity = new Attribute(row);
} }
else if (row.noteRevisionId) { else if (row.noteRevisionId) {
entity = new NoteRevision(row); entity = new NoteRevision(row);
@@ -46,8 +66,9 @@ function createEntityFromRow(row) {
return entity; return entity;
} }
repository.setEntityConstructor(createEntityFromRow);
module.exports = { module.exports = {
createEntityFromRow createEntityFromRow,
}; getEntityFromEntityName
};
repository.setEntityConstructor(module.exports);

View File

@@ -3,14 +3,26 @@
const Entity = require('./entity'); const Entity = require('./entity');
const dateUtils = require('../services/date_utils'); 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 { class Image extends Entity {
static get tableName() { return "images"; } static get entityName() { return "images"; }
static get primaryKeyName() { return "imageId"; } 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() { beforeSaving() {
super.beforeSaving();
if (!this.isDeleted) { if (!this.isDeleted) {
this.isDeleted = false; this.isDeleted = false;
} }
@@ -19,7 +31,11 @@ class Image extends Entity {
this.dateCreated = dateUtils.nowDate(); 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"; "use strict";
const Entity = require('./entity'); const Entity = require('./entity');
const Attribute = require('./attribute');
const protectedSessionService = require('../services/protected_session'); const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository'); const repository = require('../services/repository');
const dateUtils = require('../services/date_utils'); const dateUtils = require('../services/date_utils');
class Note extends Entity { const LABEL = 'label';
static get tableName() { return "notes"; } const RELATION = 'relation';
static get primaryKeyName() { return "noteId"; }
static get hashedProperties() { return ["noteId", "title", "content", "type", "dateModified", "isProtected", "isDeleted"]; }
/**
* 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) { constructor(row) {
super(row); super(row);
this.isProtected = !!this.isProtected;
// check if there's noteId, otherwise this is a new entity which wasn't encrypted yet // check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
if (this.isProtected && this.noteId) { if (this.isProtected && this.noteId) {
protectedSessionService.decryptNote(this); protectedSessionService.decryptNote(this);
@@ -30,19 +54,28 @@ class Note extends Entity {
catch(e) {} 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() { isJson() {
return this.mime === "application/json"; return this.mime === "application/json";
} }
/** @returns {boolean} true if this note is JavaScript (code or attachment) */
isJavaScript() { isJavaScript() {
return (this.type === "code" || this.type === "file") return (this.type === "code" || this.type === "file")
&& (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript"); && (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript");
} }
/** @returns {boolean} true if this note is HTML */
isHtml() { isHtml() {
return (this.type === "code" || this.type === "file") && this.mime === "text/html"; return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
} }
/** @returns {string} JS script environment - either "frontend" or "backend" */
getScriptEnv() { getScriptEnv() {
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) { if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
return "frontend"; return "frontend";
@@ -59,55 +92,385 @@ class Note extends Entity {
return null; 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! /** @returns {Promise<Attribute[]>} all note's attributes, including inherited ones */
async getLabelMap() { async getAttributes() {
const map = {}; if (!this.__attributeCache) {
await this.loadAttributesToCache();
for (const label of await this.getLabels()) {
map[label.name] = label.value;
} }
return map; return this.__attributeCache;
} }
async hasLabel(name) { /** @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones */
const map = await this.getLabelMap(); async getLabels() {
return (await this.getAttributes()).filter(attr => attr.type === LABEL);
return map.hasOwnProperty(name);
} }
// WARNING: this doesn't take into account the possibility to have multi-valued labels! /** @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones */
async getLabel(name) { async getRelations() {
return await repository.getEntity("SELECT * FROM labels WHERE noteId = ? AND name = ?", [this.noteId, name]); 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() { async getRevisions() {
return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]); return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
} }
/**
* @returns {Promise<NoteImage[]>}
*/
async getNoteImages() { async getNoteImages() {
return await repository.getEntities("SELECT * FROM note_images WHERE noteId = ? AND isDeleted = 0", [this.noteId]); return await repository.getEntities("SELECT * FROM note_images WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
} }
/**
* @returns {Promise<Branch[]>}
*/
async getBranches() { async getBranches() {
return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]); return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
} }
async getChildNote(name) { /**
return await repository.getEntity(` * @returns {Promise<Note[]>} child notes of this note
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]);
}
async getChildNotes() { async getChildNotes() {
return await repository.getEntities(` return await repository.getEntities(`
SELECT notes.* SELECT notes.*
@@ -119,6 +482,9 @@ class Note extends Entity {
ORDER BY branches.notePosition`, [this.noteId]); ORDER BY branches.notePosition`, [this.noteId]);
} }
/**
* @returns {Promise<Branch[]>} child branches of this note
*/
async getChildBranches() { async getChildBranches() {
return await repository.getEntities(` return await repository.getEntities(`
SELECT branches.* SELECT branches.*
@@ -128,6 +494,9 @@ class Note extends Entity {
ORDER BY branches.notePosition`, [this.noteId]); ORDER BY branches.notePosition`, [this.noteId]);
} }
/**
* @returns {Promise<Note[]>} parent notes of this note (note can have multiple parents because of cloning)
*/
async getParentNotes() { async getParentNotes() {
return await repository.getEntities(` return await repository.getEntities(`
SELECT parent_notes.* SELECT parent_notes.*
@@ -140,8 +509,6 @@ class Note extends Entity {
} }
beforeSaving() { beforeSaving() {
super.beforeSaving();
if (this.isJson() && this.jsonContent) { if (this.isJson() && this.jsonContent) {
this.content = JSON.stringify(this.jsonContent, null, '\t'); this.content = JSON.stringify(this.jsonContent, null, '\t');
} }
@@ -158,7 +525,11 @@ class Note extends Entity {
this.dateCreated = dateUtils.nowDate(); 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 repository = require('../services/repository');
const dateUtils = require('../services/date_utils'); 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 { class NoteImage extends Entity {
static get tableName() { return "note_images"; } static get entityName() { return "note_images"; }
static get primaryKeyName() { return "noteImageId"; } 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() { async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
@@ -18,8 +30,6 @@ class NoteImage extends Entity {
} }
beforeSaving() { beforeSaving() {
super.beforeSaving();
if (!this.isDeleted) { if (!this.isDeleted) {
this.isDeleted = false; this.isDeleted = false;
} }
@@ -28,7 +38,11 @@ class NoteImage extends Entity {
this.dateCreated = dateUtils.nowDate(); 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 protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository'); 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 { class NoteRevision extends Entity {
static get tableName() { return "note_revisions"; } static get entityName() { return "note_revisions"; }
static get primaryKeyName() { return "noteRevisionId"; } 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) { constructor(row) {
super(row); super(row);
this.isProtected = !!this.isProtected;
if (this.isProtected) { if (this.isProtected) {
protectedSessionService.decryptNoteRevision(this); protectedSessionService.decryptNoteRevision(this);
} }
@@ -22,11 +39,11 @@ class NoteRevision extends Entity {
} }
beforeSaving() { beforeSaving() {
super.beforeSaving();
if (this.isProtected) { if (this.isProtected) {
protectedSessionService.encryptNoteRevision(this); protectedSessionService.encryptNoteRevision(this);
} }
super.beforeSaving();
} }
} }

View File

@@ -3,15 +3,34 @@
const Entity = require('./entity'); const Entity = require('./entity');
const dateUtils = require('../services/date_utils'); 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 { class Option extends Entity {
static get tableName() { return "options"; } static get entityName() { return "options"; }
static get primaryKeyName() { return "name"; } static get primaryKeyName() { return "name"; }
static get hashedProperties() { return ["name", "value"]; } static get hashedProperties() { return ["name", "value"]; }
constructor(row) {
super(row);
this.isSynced = !!this.isSynced;
}
beforeSaving() { beforeSaving() {
super.beforeSaving(); super.beforeSaving();
this.dateModified = dateUtils.nowDate(); if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
} }
} }

View File

@@ -1,11 +1,34 @@
"use strict"; "use strict";
const Entity = require('./entity'); 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 { class RecentNote extends Entity {
static get tableName() { return "recent_notes"; } static get entityName() { return "recent_notes"; }
static get primaryKeyName() { return "branchId"; } static get primaryKeyName() { return "branchId"; }
static get hashedProperties() { return ["branchId", "notePath", "dateAccessed", "isDeleted"]; } static get hashedProperties() { return ["branchId", "notePath", "dateCreated", "isDeleted"]; }
beforeSaving() {
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
super.beforeSaving();
}
} }
module.exports = RecentNote; module.exports = RecentNote;

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M63.966,45.043c0.008-0.009,0.021-0.021,0.027-0.029c0.938-1.156-0.823-13.453-5.063-20.125 c-1.389-2.186-2.239-3.423-3.219-4.719c-3.907-5.166-6-6.125-6-6.125S35.732,24.78,36.149,44.315 c-1.754,0.065-11.218,7.528-14.826,14.388c-1.206,2.291-1.856,3.645-2.493,5.141c-2.539,5.957-2.33,8.25-2.33,8.25 s16.271,6.79,33.014-3.294c0.007,0.021,0.013,0.046,0.02,0.063c0.537,1.389,12.08,5.979,19.976,5.621 c2.587-0.116,4.084-0.238,5.696-0.444c6.424-0.818,8.298-2.157,8.298-2.157S81.144,54.396,63.966,45.043z M50.787,65.343 c1.059-1.183,4.648-5.853,0.995-11.315c-0.253-0.377-0.496-0.236-0.496-0.236s0.063,10.822-5.162,12.359 c-5.225,1.537-13.886,4.4-20.427,0.455C25,66.186,26.924,53.606,38.544,47.229c0.546,1.599,2.836,6.854,9.292,6.409 c0.453-0.031,0.453-0.313,0.453-0.313s-9.422-5.328-8.156-10.625s3.089-14.236,9.766-17.948c0.714-0.397,10.746,7.593,10.417,20.94 c-1.606-0.319-7.377-1.004-10.226,4.864c-0.198,0.409,0.046,0.549,0.046,0.549s9.31-5.521,13.275-1.789 c3.965,3.733,10.813,9.763,10.71,17.4C74.111,67.533,62.197,72.258,50.787,65.343z M35.613,35.145c0,0-0.991,3.241-0.603,7.524 l-13.393-7.524C21.618,35.145,27.838,30.931,35.613,35.145z M21.193,36.03l13.344,7.612c-3.872,1.872-6.142,4.388-6.142,4.388 C20.78,43.531,21.193,36.03,21.193,36.03z M72.287,49.064c0,0-2.321-2.471-6.23-4.263l13.187-7.881 C79.243,36.92,79.808,44.413,72.287,49.064z M78.687,36.113l-13.237,7.794c0.3-4.291-0.754-7.511-0.754-7.511 C72.383,32.025,78.687,36.113,78.687,36.113z M42.076,73.778c0,0,3.309-0.737,6.845-3.185l0.056,15.361 C48.977,85.955,42.244,82.621,42.076,73.778z M49.956,85.888L50,70.526c3.539,2.445,6.846,3.181,6.846,3.181 C56.686,82.551,49.956,85.888,49.956,85.888z"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -2,7 +2,8 @@ import cloningService from '../services/cloning.js';
import linkService from '../services/link.js'; import linkService from '../services/link.js';
import noteDetailService from '../services/note_detail.js'; import noteDetailService from '../services/note_detail.js';
import treeUtils from '../services/tree_utils.js'; import treeUtils from '../services/tree_utils.js';
import autocompleteService from '../services/autocomplete.js'; import noteDetailText from "../services/note_detail_text.js";
import noteAutocompleteService from "../services/note_autocomplete.js";
const $dialog = $("#add-link-dialog"); const $dialog = $("#add-link-dialog");
const $form = $("#add-link-form"); const $form = $("#add-link-form");
@@ -11,8 +12,10 @@ const $linkTitle = $("#link-title");
const $clonePrefix = $("#clone-prefix"); const $clonePrefix = $("#clone-prefix");
const $linkTitleFormGroup = $("#add-link-title-form-group"); const $linkTitleFormGroup = $("#add-link-title-form-group");
const $prefixFormGroup = $("#add-link-prefix-form-group"); const $prefixFormGroup = $("#add-link-prefix-form-group");
const $linkTypeDiv = $("#add-link-type-div");
const $linkTypes = $("input[name='add-link-type']"); const $linkTypes = $("input[name='add-link-type']");
const $linkTypeHtml = $linkTypes.filter('input[value="html"]'); const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
const $showRecentNotesButton = $dialog.find(".show-recent-notes-button");
function setLinkType(linkType) { function setLinkType(linkType) {
$linkTypes.each(function () { $linkTypes.each(function () {
@@ -51,12 +54,16 @@ async function showDialog() {
$linkTitle.val(noteTitle); $linkTitle.val(noteTitle);
} }
$autoComplete.autocomplete({ await $autoComplete.autocomplete({
source: await autocompleteService.getAutocompleteItems(), source: noteAutocompleteService.autocompleteSource,
minLength: 0, minLength: 0,
change: async () => { change: async (event, ui) => {
const val = $autoComplete.val(); if (!ui.item) {
const notePath = linkService.getNodePathFromLabel(val); return;
}
const notePath = linkService.getNotePathFromLabel(ui.item.value);
if (!notePath) { if (!notePath) {
return; return;
} }
@@ -67,21 +74,30 @@ async function showDialog() {
await setDefaultLinkTitle(noteId); 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 // 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 // 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) => { focus: async (event, ui) => {
const notePath = linkService.getNodePathFromLabel(ui.item.value); const notePath = linkService.getNotePathFromLabel(ui.item.value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath); const noteId = treeUtils.getNoteIdFromNotePath(notePath);
await setDefaultLinkTitle(noteId); await setDefaultLinkTitle(noteId);
event.preventDefault();
} }
}); });
showRecentNotes();
} }
$form.submit(() => { $form.submit(() => {
const value = $autoComplete.val(); const value = $autoComplete.val();
const notePath = linkService.getNodePathFromLabel(value); const notePath = linkService.getNotePathFromLabel(value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath); const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (notePath) { if (notePath) {
@@ -92,7 +108,16 @@ $form.submit(() => {
$dialog.dialog("close"); $dialog.dialog("close");
linkService.addLinkToEditor(linkTitle, '#' + notePath); const linkHref = '#' + notePath;
if (hasSelection()) {
const editor = noteDetailText.getEditor();
editor.execute('link', linkHref);
}
else {
linkService.addLinkToEditor(linkTitle, linkHref);
}
} }
else if (linkType === 'selected-to-current') { else if (linkType === 'selected-to-current') {
const prefix = $clonePrefix.val(); const prefix = $clonePrefix.val();
@@ -113,21 +138,31 @@ $form.submit(() => {
return false; return false;
}); });
// returns true if user selected some text, false if there's no selection
function hasSelection() {
const model = noteDetailText.getEditor().model;
const selection = model.document.selection;
return !selection.isCollapsed;
}
function linkTypeChanged() { function linkTypeChanged() {
const value = $linkTypes.filter(":checked").val(); const value = $linkTypes.filter(":checked").val();
if (value === 'html') { $linkTitleFormGroup.toggle(!hasSelection() && value === 'html');
$linkTitleFormGroup.show(); $prefixFormGroup.toggle(!hasSelection() && value !== 'html');
$prefixFormGroup.hide();
} $linkTypeDiv.toggle(!hasSelection());
else { }
$linkTitleFormGroup.hide();
$prefixFormGroup.show(); function showRecentNotes() {
} $autoComplete.autocomplete("search", "");
} }
$linkTypes.change(linkTypeChanged); $linkTypes.change(linkTypeChanged);
$showRecentNotesButton.click(showRecentNotes);
export default { export default {
showDialog 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({ await $dialog.dialog({
modal: true, modal: true,
width: 500 width: 600
}); });
const currentNode = treeService.getCurrentNode(); const currentNode = treeService.getCurrentNode();

View File

@@ -19,7 +19,7 @@ async function showDialog() {
$list.html(''); $list.html('');
for (const event of result) { for (const event of result) {
const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded)); const dateTime = utils.formatDateTime(utils.parseDate(event.dateCreated));
if (event.noteId) { if (event.noteId) {
const noteLink = await linkService.createNoteLink(event.noteId).prop('outerHTML'); const noteLink = await linkService.createNoteLink(event.noteId).prop('outerHTML');

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
"use strict"; "use strict";
import protectedSessionHolder from '../services/protected_session_holder.js'; import protectedSessionHolder from '../services/protected_session_holder.js';
import utils from '../services/utils.js';
import server from '../services/server.js'; import server from '../services/server.js';
import infoService from "../services/info.js"; import infoService from "../services/info.js";
import zoomService from "../services/zoom.js";
import utils from "../services/utils.js";
const $dialog = $("#options-dialog"); const $dialog = $("#options-dialog");
const $tabs = $("#options-tabs"); const $tabs = $("#options-tabs");
@@ -33,8 +34,8 @@ async function showDialog() {
} }
} }
async function saveOptions(optionName, optionValue) { async function saveOptions(options) {
await server.put('options/' + encodeURIComponent(optionName) + '/' + encodeURIComponent(optionValue)); await server.put('options', options);
infoService.showMessage("Options change have been saved."); infoService.showMessage("Options change have been saved.");
} }
@@ -44,6 +45,41 @@ export default {
saveOptions 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() { addTabHandler((function() {
const $form = $("#change-password-form"); const $form = $("#change-password-form");
const $oldPassword = $("#old-password"); const $oldPassword = $("#old-password");
@@ -93,16 +129,15 @@ addTabHandler((function() {
addTabHandler((function() { addTabHandler((function() {
const $form = $("#protected-session-timeout-form"); const $form = $("#protected-session-timeout-form");
const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds"); const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
const optionName = 'protectedSessionTimeout';
function optionsLoaded(options) { function optionsLoaded(options) {
$protectedSessionTimeout.val(options[optionName]); $protectedSessionTimeout.val(options['protectedSessionTimeout']);
} }
$form.submit(() => { $form.submit(() => {
const protectedSessionTimeout = $protectedSessionTimeout.val(); const protectedSessionTimeout = $protectedSessionTimeout.val();
saveOptions(optionName, protectedSessionTimeout).then(() => { saveOptions({ 'protectedSessionTimeout': protectedSessionTimeout }).then(() => {
protectedSessionHolder.setProtectedSessionTimeout(protectedSessionTimeout); protectedSessionHolder.setProtectedSessionTimeout(protectedSessionTimeout);
}); });
@@ -117,14 +152,13 @@ addTabHandler((function() {
addTabHandler((function () { addTabHandler((function () {
const $form = $("#note-revision-snapshot-time-interval-form"); const $form = $("#note-revision-snapshot-time-interval-form");
const $timeInterval = $("#note-revision-snapshot-time-interval-in-seconds"); const $timeInterval = $("#note-revision-snapshot-time-interval-in-seconds");
const optionName = 'noteRevisionSnapshotTimeInterval';
function optionsLoaded(options) { function optionsLoaded(options) {
$timeInterval.val(options[optionName]); $timeInterval.val(options['noteRevisionSnapshotTimeInterval']);
} }
$form.submit(() => { $form.submit(() => {
saveOptions(optionName, $timeInterval.val()); saveOptions({ 'noteRevisionSnapshotTimeInterval': $timeInterval.val() });
return false; return false;
}); });
@@ -137,6 +171,7 @@ addTabHandler((function () {
addTabHandler((async function () { addTabHandler((async function () {
const $appVersion = $("#app-version"); const $appVersion = $("#app-version");
const $dbVersion = $("#db-version"); const $dbVersion = $("#db-version");
const $syncVersion = $("#sync-version");
const $buildDate = $("#build-date"); const $buildDate = $("#build-date");
const $buildRevision = $("#build-revision"); const $buildRevision = $("#build-revision");
@@ -144,6 +179,7 @@ addTabHandler((async function () {
$appVersion.html(appInfo.appVersion); $appVersion.html(appInfo.appVersion);
$dbVersion.html(appInfo.dbVersion); $dbVersion.html(appInfo.dbVersion);
$syncVersion.html(appInfo.syncVersion);
$buildDate.html(appInfo.buildDate); $buildDate.html(appInfo.buildDate);
$buildRevision.html(appInfo.buildRevision); $buildRevision.html(appInfo.buildRevision);
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision); $buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
@@ -151,6 +187,57 @@ addTabHandler((async function () {
return {}; 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 () { addTabHandler((async function () {
const $forceFullSyncButton = $("#force-full-sync-button"); const $forceFullSyncButton = $("#force-full-sync-button");
const $fillSyncRowsButton = $("#fill-sync-rows-button"); const $fillSyncRowsButton = $("#fill-sync-rows-button");

View File

@@ -15,7 +15,11 @@ async function showDialog() {
const result = await server.get('recent-changes/'); const result = await server.get('recent-changes/');
$dialog.html(''); $dialog.empty();
if (result.length === 0) {
$dialog.append("No changes yet ...");
}
const groupedByDate = groupByDate(result); const groupedByDate = groupByDate(result);
@@ -30,9 +34,9 @@ async function showDialog() {
const revLink = $("<a>", { const revLink = $("<a>", {
href: 'javascript:', href: 'javascript:',
text: 'rev' text: 'rev'
}).attr('action', 'note-revision') }).attr('data-action', 'note-revision')
.attr('note-path', change.noteId) .attr('data-note-path', change.noteId)
.attr('note-revision-id', change.noteRevisionId); .attr('data-note-revision-id', change.noteRevisionId);
let noteLink; 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 { class Branch {
constructor(treeCache, row) { constructor(treeCache, row) {
this.treeCache = treeCache; this.treeCache = treeCache;
/** @param {string} primary key */
this.branchId = row.branchId; this.branchId = row.branchId;
/** @param {string} */
this.noteId = row.noteId; this.noteId = row.noteId;
this.note = null; this.note = null;
/** @param {string} */
this.parentNoteId = row.parentNoteId; this.parentNoteId = row.parentNoteId;
/** @param {int} */
this.notePosition = row.notePosition; this.notePosition = row.notePosition;
/** @param {string} */
this.prefix = row.prefix; this.prefix = row.prefix;
/** @param {boolean} */
this.isExpanded = row.isExpanded; this.isExpanded = row.isExpanded;
} }
/** @returns {NoteShort} */
async getNote() { async getNote() {
return await this.treeCache.getNote(this.noteId); return await this.treeCache.getNote(this.noteId);
} }
/** @returns {boolean} true if it's top level, meaning its parent is root note */
isTopLevel() { isTopLevel() {
return this.parentNoteId === 'root'; return this.parentNoteId === 'root';
} }

View File

@@ -1,13 +1,18 @@
import NoteShort from './note_short.js'; import NoteShort from './note_short.js';
/**
* Represents full note, specifically including note's content.
*/
class NoteFull extends NoteShort { class NoteFull extends NoteShort {
constructor(treeCache, row) { constructor(treeCache, row) {
super(treeCache, row); super(treeCache, row);
/** @param {string} */
this.content = row.content; this.content = row.content;
if (this.content !== "" && this.isJson()) { if (this.content !== "" && this.isJson()) {
try { try {
/** @param {object} */
this.jsonContent = JSON.parse(this.content); this.jsonContent = JSON.parse(this.content);
} }
catch(e) {} 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 { class NoteShort {
constructor(treeCache, row) { constructor(treeCache, row) {
this.treeCache = treeCache; this.treeCache = treeCache;
/** @param {string} */
this.noteId = row.noteId; this.noteId = row.noteId;
/** @param {string} */
this.title = row.title; this.title = row.title;
/** @param {boolean} */
this.isProtected = row.isProtected; this.isProtected = row.isProtected;
/** @param {string} one of 'text', 'code', 'file' or 'render' */
this.type = row.type; this.type = row.type;
/** @param {string} content-type, e.g. "application/json" */
this.mime = row.mime; this.mime = row.mime;
this.hideInAutocomplete = row.hideInAutocomplete; /** @param {boolean} */
this.archived = row.archived;
this.cssClass = row.cssClass;
} }
/** @returns {boolean} */
isJson() { isJson() {
return this.mime === "application/json"; return this.mime === "application/json";
} }
/** @returns {Promise<Branch[]>} */
async getBranches() { async getBranches() {
const branchIds = this.treeCache.parents[this.noteId].map( const branchIds = this.treeCache.parents[this.noteId].map(
parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId)); parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId));
@@ -20,11 +33,13 @@ class NoteShort {
return this.treeCache.getBranches(branchIds); return this.treeCache.getBranches(branchIds);
} }
/** @returns {boolean} */
hasChildren() { hasChildren() {
return this.treeCache.children[this.noteId] return this.treeCache.children[this.noteId]
&& this.treeCache.children[this.noteId].length > 0; && this.treeCache.children[this.noteId].length > 0;
} }
/** @returns {Promise<Branch[]>} */
async getChildBranches() { async getChildBranches() {
if (!this.treeCache.children[this.noteId]) { if (!this.treeCache.children[this.noteId]) {
return []; return [];
@@ -36,18 +51,22 @@ class NoteShort {
return await this.treeCache.getBranches(branchIds); return await this.treeCache.getBranches(branchIds);
} }
/** @returns {string[]} */
getParentNoteIds() { getParentNoteIds() {
return this.treeCache.parents[this.noteId] || []; return this.treeCache.parents[this.noteId] || [];
} }
/** @returns {Promise<NoteShort[]>} */
async getParentNotes() { async getParentNotes() {
return await this.treeCache.getNotes(this.getParentNoteIds()); return await this.treeCache.getNotes(this.getParentNoteIds());
} }
/** @returns {string[]} */
getChildNoteIds() { getChildNoteIds() {
return this.treeCache.children[this.noteId] || []; return this.treeCache.children[this.noteId] || [];
} }
/** @returns {Promise<NoteShort[]>} */
async getChildNotes() { async getChildNotes() {
return await this.treeCache.getNotes(this.getChildNoteIds()); return await this.treeCache.getNotes(this.getChildNoteIds());
} }
@@ -59,7 +78,7 @@ class NoteShort {
get dto() { get dto() {
const dto = Object.assign({}, this); const dto = Object.assign({}, this);
delete dto.treeCache; delete dto.treeCache;
delete dto.hideInAutocomplete; delete dto.archived;
return dto; 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,104 +0,0 @@
import treeCache from "./tree_cache.js";
import treeUtils from "./tree_utils.js";
import protectedSessionHolder from './protected_session_holder.js';
async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
if (!parentNoteId) {
parentNoteId = 'root';
}
const parentNote = await treeCache.getNote(parentNoteId);
const childNotes = await parentNote.getChildNotes();
if (!childNotes.length) {
return [];
}
if (!notePath) {
notePath = '';
}
if (!titlePath) {
titlePath = '';
}
const autocompleteItems = [];
for (const childNote of childNotes) {
if (childNote.hideInAutocomplete) {
continue;
}
const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId;
const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId);
if (!childNote.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
autocompleteItems.push({
value: childTitlePath + ' (' + childNotePath + ')',
label: childTitlePath
});
}
const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath);
for (const childItem of childItems) {
autocompleteItems.push(childItem);
}
}
if (parentNoteId === 'root') {
console.log(`Generated ${autocompleteItems.length} autocomplete items`);
}
return autocompleteItems;
}
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
$.ui.autocomplete.filter = (array, terms) => {
if (!terms) {
return array;
}
const startDate = new Date();
const results = [];
const tokens = terms.toLowerCase().split(" ");
for (const item of array) {
const lcLabel = item.label.toLowerCase();
const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
if (!found) {
continue;
}
// this is not completely correct and might cause minor problems with note with names containing this " / "
const lastSegmentIndex = lcLabel.lastIndexOf(" / ");
if (lastSegmentIndex !== -1) {
const lastSegment = lcLabel.substr(lastSegmentIndex + 3);
// at least some token needs to be in the last segment (leaf note), otherwise this
// particular note is not that interesting (query is satisfied by parent note)
const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1);
if (!foundInLastSegment) {
continue;
}
}
results.push(item);
if (results.length > 100) {
break;
}
}
console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
return results;
};
export default {
getAutocompleteItems
};

View File

@@ -1,10 +1,9 @@
import addLinkDialog from '../dialogs/add_link.js'; import addLinkDialog from '../dialogs/add_link.js';
import jumpToNoteDialog from '../dialogs/jump_to_note.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 noteRevisionsDialog from '../dialogs/note_revisions.js';
import noteSourceDialog from '../dialogs/note_source.js'; import noteSourceDialog from '../dialogs/note_source.js';
import recentChangesDialog from '../dialogs/recent_changes.js'; import recentChangesDialog from '../dialogs/recent_changes.js';
import recentNotesDialog from '../dialogs/recent_notes.js';
import optionsDialog from '../dialogs/options.js'; import optionsDialog from '../dialogs/options.js';
import sqlConsoleDialog from '../dialogs/sql_console.js'; import sqlConsoleDialog from '../dialogs/sql_console.js';
@@ -17,8 +16,8 @@ import messagingService from './messaging.js';
import noteDetailService from './note_detail.js'; import noteDetailService from './note_detail.js';
import noteType from './note_type.js'; import noteType from './note_type.js';
import protected_session from './protected_session.js'; import protected_session from './protected_session.js';
import searchTreeService from './search_tree.js'; import searchNotesService from './search_notes.js';
import ScriptApi from './script_api.js'; import FrontendScriptApi from './frontend_script_api.js';
import ScriptContext from './script_context.js'; import ScriptContext from './script_context.js';
import sync from './sync.js'; import sync from './sync.js';
import treeService from './tree.js'; import treeService from './tree.js';
@@ -35,6 +34,9 @@ import libraryLoader from "./library_loader.js";
// required for CKEditor image upload plugin // required for CKEditor image upload plugin
window.glob.getCurrentNode = treeService.getCurrentNode; window.glob.getCurrentNode = treeService.getCurrentNode;
window.glob.getHeaders = server.getHeaders; 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 // required for ESLint plugin
window.glob.getCurrentNote = noteDetailService.getCurrentNote; window.glob.getCurrentNote = noteDetailService.getCurrentNote;
@@ -46,7 +48,12 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
let message = "Uncaught 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'; message += 'No details available';
} }
else { else {
@@ -64,6 +71,14 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
return false; 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()); $("#logout-button").toggle(!utils.isElectron());
if (utils.isElectron()) { if (utils.isElectron()) {
@@ -73,7 +88,7 @@ if (utils.isElectron()) {
await treeService.reload(); await treeService.reload();
} }
await treeService.activateNode(parentNoteId); await treeService.activateNote(parentNoteId);
setTimeout(() => { setTimeout(() => {
const node = treeService.getCurrentNode(); const node = treeService.getCurrentNode();

View File

@@ -1,12 +1,24 @@
import ScriptContext from "./script_context.js"; import ScriptContext from "./script_context.js";
import server from "./server.js"; import server from "./server.js";
import infoService from "./info.js";
async function executeBundle(bundle) { async function getAndExecuteBundle(noteId, originEntity = null) {
const apiContext = ScriptContext(bundle.note, bundle.allNotes); const bundle = await server.get('script/bundle/' + noteId);
return await (function () { await executeBundle(bundle, originEntity);
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); }
}.call(apiContext));
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() { 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 { export default {
executeBundle, executeBundle,
executeStartupBundles getAndExecuteBundle,
executeStartupBundles,
executeRelationBundles
} }

View File

@@ -94,27 +94,34 @@ const contextMenuOptions = {
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
{title: "----"}, {title: "----"},
{title: "Export branch", cmd: "exportBranch", uiIcon: " ui-icon-arrowthick-1-ne"}, {title: "Export branch", cmd: "exportBranch", uiIcon: " ui-icon-arrowthick-1-ne", children: [
{title: "Import into branch", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"}, {title: "Native&nbsp;Tar", cmd: "exportBranchToTar"},
{title: "OPML", cmd: "exportBranchToOpml"}
]},
{title: "Import into branch (tar, opml)", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"},
{title: "----"}, {title: "----"},
{title: "Collapse branch <kbd>Alt+-</kbd>", cmd: "collapseBranch", uiIcon: "ui-icon-minus"}, {title: "Collapse branch <kbd>Alt+-</kbd>", cmd: "collapseBranch", uiIcon: "ui-icon-minus"},
{title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"}, {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"},
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} {title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
], ],
beforeOpen: async (event, ui) => { beforeOpen: async (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target); const node = $.ui.fancytree.getNode(ui.target);
const branch = await treeCache.getBranch(node.data.branchId); const branch = await treeCache.getBranch(node.data.branchId);
const note = await treeCache.getNote(node.data.noteId); const note = await treeCache.getNote(node.data.noteId);
const parentNote = await treeCache.getNote(branch.parentNoteId); const parentNote = await treeCache.getNote(branch.parentNoteId);
const isNotRoot = note.noteId !== 'root';
// Modify menu entries depending on node status // Modify menu entries depending on node status
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && (!parentNote || parentNote.type !== 'search')); $tree.contextmenu("enableEntry", "insertNoteHere", isNotRoot && parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
$tree.contextmenu("enableEntry", "insertNoteHere", !parentNote || parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search'); $tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search');
$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", "importBranch", note.type !== 'search');
$tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search'); $tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search');
$tree.contextmenu("enableEntry", "editBranchPrefix", isNotRoot && parentNote.type !== 'search');
// Activate node on right-click // Activate node on right-click
node.setActive(); node.setActive();
@@ -159,8 +166,11 @@ const contextMenuOptions = {
else if (ui.cmd === "delete") { else if (ui.cmd === "delete") {
treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); treeChangesService.deleteNodes(treeService.getSelectedNodes(true));
} }
else if (ui.cmd === "exportBranch") { else if (ui.cmd === "exportBranchToTar") {
exportService.exportBranch(node.data.noteId); exportService.exportBranch(node.data.noteId, 'tar');
}
else if (ui.cmd === "exportBranchToOpml") {
exportService.exportBranch(node.data.noteId, 'opml');
} }
else if (ui.cmd === "importBranch") { else if (ui.cmd === "importBranch") {
exportService.importBranch(node.data.noteId); exportService.importBranch(node.data.noteId);

View File

@@ -2,16 +2,17 @@ import utils from "./utils.js";
import treeService from "./tree.js"; import treeService from "./tree.js";
import linkService from "./link.js"; import linkService from "./link.js";
import fileService from "./file.js"; import fileService from "./file.js";
import zoomService from "./zoom.js";
import noteRevisionsDialog from "../dialogs/note_revisions.js"; import noteRevisionsDialog from "../dialogs/note_revisions.js";
import optionsDialog from "../dialogs/options.js"; import optionsDialog from "../dialogs/options.js";
import addLinkDialog from "../dialogs/add_link.js"; import addLinkDialog from "../dialogs/add_link.js";
import recentNotesDialog from "../dialogs/recent_notes.js";
import jumpToNoteDialog from "../dialogs/jump_to_note.js"; import jumpToNoteDialog from "../dialogs/jump_to_note.js";
import noteSourceDialog from "../dialogs/note_source.js"; import noteSourceDialog from "../dialogs/note_source.js";
import recentChangesDialog from "../dialogs/recent_changes.js"; import recentChangesDialog from "../dialogs/recent_changes.js";
import sqlConsoleDialog from "../dialogs/sql_console.js"; import sqlConsoleDialog from "../dialogs/sql_console.js";
import searchTreeService from "./search_tree.js"; import searchNotesService from "./search_notes.js";
import labelsDialog from "../dialogs/labels.js"; import attributesDialog from "../dialogs/attributes.js";
import protectedSessionService from "./protected_session.js";
function registerEntrypoints() { function registerEntrypoints() {
// hot keys are active also inside inputs and content editables // hot keys are active also inside inputs and content editables
@@ -21,35 +22,44 @@ function registerEntrypoints() {
utils.bindShortcut('ctrl+l', addLinkDialog.showDialog); 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); utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog);
$("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions); $("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions);
$("#show-source-button").click(noteSourceDialog.showDialog); $("#show-source-button").click(noteSourceDialog.showDialog);
utils.bindShortcut('ctrl+u', noteSourceDialog.showDialog);
$("#recent-changes-button").click(recentChangesDialog.showDialog); $("#recent-changes-button").click(recentChangesDialog.showDialog);
$("#recent-notes-button").click(recentNotesDialog.showDialog); $("#protected-session-on").click(protectedSessionService.enterProtectedSession);
utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog); $("#protected-session-off").click(protectedSessionService.leaveProtectedSession);
$("#toggle-search-button").click(searchTreeService.toggleSearch); $("#toggle-search-button").click(searchNotesService.toggleSearch);
utils.bindShortcut('ctrl+s', searchTreeService.toggleSearch); utils.bindShortcut('ctrl+s', searchNotesService.toggleSearch);
$(".show-labels-button").click(labelsDialog.showDialog); $(".show-attributes-button").click(attributesDialog.showDialog);
utils.bindShortcut('alt+l', labelsDialog.showDialog); utils.bindShortcut('alt+a', attributesDialog.showDialog);
$("#options-button").click(optionsDialog.showDialog); $("#options-button").click(optionsDialog.showDialog);
utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog); utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog);
if (utils.isElectron()) { if (utils.isElectron()) {
$("#history-navigation").show();
$("#history-back-button").click(window.history.back);
$("#history-forward-button").click(window.history.forward);
utils.bindShortcut('alt+left', window.history.back); utils.bindShortcut('alt+left', window.history.back);
utils.bindShortcut('alt+right', window.history.forward); 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 // hide (toggle) everything except for the note content for distraction free writing
utils.bindShortcut('alt+t', e => { utils.bindShortcut('alt+t', e => {
@@ -101,27 +111,10 @@ function registerEntrypoints() {
$("#note-detail-text").focus(); $("#note-detail-text").focus();
}); });
$(document).bind('keydown', 'ctrl+-', () => { if (utils.isElectron()) {
if (utils.isElectron()) { $(document).bind('keydown', 'ctrl+-', zoomService.decreaseZoomFactor);
const webFrame = require('electron').webFrame; $(document).bind('keydown', 'ctrl+=', zoomService.increaseZoomFactor);
}
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;
}
});
$("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus()); $("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus());

View File

@@ -3,9 +3,9 @@ import protectedSessionHolder from './protected_session_holder.js';
import utils from './utils.js'; import utils from './utils.js';
import server from './server.js'; import server from './server.js';
function exportBranch(noteId) { function exportBranch(noteId, format) {
const url = utils.getHost() + "/api/notes/" + noteId + "/export?protectedSessionId=" const url = utils.getHost() + "/api/notes/" + noteId + "/export/" + format +
+ encodeURIComponent(protectedSessionHolder.getProtectedSessionId()); "?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId());
utils.download(url); utils.download(url);
} }
@@ -29,7 +29,7 @@ $("#import-upload").change(async function() {
type: 'POST', type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS processData: false, // NEEDED, DON'T OMIT THIS
}); }).fail((xhr, status, error) => alert('Import error: ' + xhr.responseText));
await treeService.reload(); await treeService.reload();
}); });

View File

@@ -21,7 +21,7 @@ $("#file-upload").change(async function() {
await treeService.reload(); await treeService.reload();
await treeService.activateNode(resp.noteId); await treeService.activateNote(resp.noteId);
}); });
export default { 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) { function showError(message, delay = 10000) {
console.log(utils.now(), "error: ", message); console.log(utils.now(), "error: ", message);
@@ -36,5 +42,6 @@ function throwError(message) {
export default { export default {
showMessage, showMessage,
showError, showError,
showAndLogError,
throwError throwError
} }

View File

@@ -3,7 +3,7 @@ import noteDetailText from './note_detail_text.js';
import treeUtils from './tree_utils.js'; import treeUtils from './tree_utils.js';
function getNotePathFromLink(url) { function getNotePathFromLink(url) {
const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url); const notePathMatch = /#root([A-Za-z0-9/]*)$/.exec(url);
if (notePathMatch === null) { if (notePathMatch === null) {
return null; return null;
@@ -13,8 +13,8 @@ function getNotePathFromLink(url) {
} }
} }
function getNodePathFromLabel(label) { function getNotePathFromLabel(label) {
const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label); const notePathMatch = / \(([#A-Za-z0-9/]+)\)/.exec(label);
if (notePathMatch !== null) { if (notePathMatch !== null) {
return notePathMatch[1]; return notePathMatch[1];
@@ -23,7 +23,7 @@ function getNodePathFromLabel(label) {
return null; return null;
} }
async function createNoteLink(notePath, noteTitle) { async function createNoteLink(notePath, noteTitle = null) {
if (!noteTitle) { if (!noteTitle) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath); const noteId = treeUtils.getNoteIdFromNotePath(notePath);
@@ -33,8 +33,8 @@ async function createNoteLink(notePath, noteTitle) {
const noteLink = $("<a>", { const noteLink = $("<a>", {
href: 'javascript:', href: 'javascript:',
text: noteTitle text: noteTitle
}).attr('action', 'note') }).attr('data-action', 'note')
.attr('note-path', notePath); .attr('data-note-path', notePath);
return noteLink; return noteLink;
} }
@@ -43,10 +43,10 @@ function goToLink(e) {
e.preventDefault(); e.preventDefault();
const $link = $(e.target); const $link = $(e.target);
let notePath = $link.attr("note-path"); let notePath = $link.attr("data-note-path");
if (!notePath) { 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) { if (!address) {
return; return;
@@ -61,7 +61,7 @@ function goToLink(e) {
notePath = getNotePathFromLink(address); 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 // this is quite ugly hack, but it seems like we can't close the tooltip otherwise
$("[role='tooltip']").remove(); $("[role='tooltip']").remove();
@@ -90,14 +90,26 @@ function addTextToEditor(text) {
doc.enqueueChanges(() => editor.data.insertText(text), doc.selection); 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 // 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 // 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('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
$(document).on('dblclick', '#note-detail-text a', goToLink); $(document).on('dblclick', '#note-detail-text a', goToLink);
export default { export default {
getNodePathFromLabel, getNotePathFromLabel,
getNotePathFromLink, getNotePathFromLink,
createNoteLink, createNoteLink,
addLinkToEditor, addLinkToEditor,

View File

@@ -1,8 +1,9 @@
import utils from './utils.js'; import utils from './utils.js';
import infoService from "./info.js"; import infoService from "./info.js";
const $changesToPushCount = $("#changes-to-push-count"); const $outstandingSyncsCount = $("#outstanding-syncs-count");
const syncMessageHandlers = [];
const messageHandlers = []; const messageHandlers = [];
let ws; let ws;
@@ -25,9 +26,17 @@ function subscribeToMessages(messageHandler) {
messageHandlers.push(messageHandler); messageHandlers.push(messageHandler);
} }
function subscribeToSyncMessages(messageHandler) {
syncMessageHandlers.push(messageHandler);
}
function handleMessage(event) { function handleMessage(event) {
const message = JSON.parse(event.data); const message = JSON.parse(event.data);
for (const messageHandler of messageHandlers) {
messageHandler(message);
}
if (message.type === 'sync') { if (message.type === 'sync') {
lastPingTs = new Date().getTime(); lastPingTs = new Date().getTime();
@@ -39,11 +48,11 @@ function handleMessage(event) {
const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId); const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
for (const messageHandler of messageHandlers) { for (const syncMessageHandler of syncMessageHandlers) {
messageHandler(syncData); syncMessageHandler(syncData);
} }
$changesToPushCount.html(message.changesToPushCount); $outstandingSyncsCount.html(message.outstandingSyncs);
} }
else if (message.type === 'sync-hash-check-failed') { else if (message.type === 'sync-hash-check-failed') {
infoService.showError("Sync check failed!", 60000); infoService.showError("Sync check failed!", 60000);
@@ -73,26 +82,10 @@ setTimeout(() => {
lastSyncId = glob.maxSyncIdAtLoad; lastSyncId = glob.maxSyncIdAtLoad;
lastPingTs = new Date().getTime(); lastPingTs = new Date().getTime();
let connectionBrokenNotification = null;
setInterval(async () => { setInterval(async () => {
if (new Date().getTime() - lastPingTs > 30000) { if (new Date().getTime() - lastPingTs > 30000) {
if (!connectionBrokenNotification) { console.log("Lost connection to server");
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");
} }
ws.send(JSON.stringify({ ws.send(JSON.stringify({
@@ -104,5 +97,6 @@ setTimeout(() => {
export default { export default {
logError, 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 server from './server.js';
import messagingService from "./messaging.js"; import messagingService from "./messaging.js";
import infoService from "./info.js"; import infoService from "./info.js";
import linkService from "./link.js";
import treeCache from "./tree_cache.js"; import treeCache from "./tree_cache.js";
import NoteFull from "../entities/note_full.js"; import NoteFull from "../entities/note_full.js";
import noteDetailCode from './note_detail_code.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 noteDetailFile from './note_detail_file.js';
import noteDetailSearch from './note_detail_search.js'; import noteDetailSearch from './note_detail_search.js';
import noteDetailRender from './note_detail_render.js'; import noteDetailRender from './note_detail_render.js';
import bundleService from "./bundle.js";
import noteAutocompleteService from "./note_autocomplete.js";
const $noteTitle = $("#note-title"); const $noteTitle = $("#note-title");
@@ -23,9 +26,11 @@ const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button"); const $unprotectButton = $("#unprotect-button");
const $noteDetailWrapper = $("#note-detail-wrapper"); const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display"); const $noteIdDisplay = $("#note-id-display");
const $labelList = $("#label-list"); const $attributeList = $("#attribute-list");
const $labelListInner = $("#label-list-inner"); const $attributeListInner = $("#attribute-list-inner");
const $childrenOverview = $("#children-overview"); const $childrenOverview = $("#children-overview");
const $scriptArea = $("#note-detail-script-area");
const $promotedAttributesContainer = $("#note-detail-promoted-attributes");
let currentNote = null; let currentNote = null;
@@ -114,11 +119,12 @@ async function saveNoteIfChanged() {
} }
function setNoteBackgroundIfProtected(note) { function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.isProtected; const isProtected = note.isProtected;
$noteDetailWrapper.toggleClass("protected", isProtected); $noteDetailWrapper.toggleClass("protected", isProtected);
$protectButton.toggle(!isProtected); $protectButton.toggleClass("active", isProtected);
$unprotectButton.toggle(isProtected); $unprotectButton.toggleClass("active", !isProtected);
$unprotectButton.prop("disabled", !protectedSessionHolder.isProtectedSessionAvailable());
} }
let isNewNoteCreated = false; let isNewNoteCreated = false;
@@ -150,7 +156,7 @@ async function loadNoteDetail(noteId) {
$noteIdDisplay.html(noteId); $noteIdDisplay.html(noteId);
await handleProtectedSession(); setNoteBackgroundIfProtected(currentNote);
$noteDetailWrapper.show(); $noteDetailWrapper.show();
@@ -164,21 +170,26 @@ async function loadNoteDetail(noteId) {
$noteDetailComponents.hide(); $noteDetailComponents.hide();
await handleProtectedSession();
await getComponent(currentNote.type).show(); await getComponent(currentNote.type).show();
} }
finally { finally {
noteChangeDisabled = false; noteChangeDisabled = false;
} }
setNoteBackgroundIfProtected(currentNote);
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId); treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top // after loading new note make sure editor is scrolled to the top
$noteDetailWrapper.scrollTop(0); $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); await showChildrenOverview(hideChildrenOverview);
} }
@@ -198,7 +209,7 @@ async function showChildrenOverview(hideChildrenOverview) {
const link = $('<a>', { const link = $('<a>', {
href: 'javascript:', href: 'javascript:',
text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId) 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); const childEl = $('<div class="child-overview">').html(link);
$childrenOverview.append(childEl); $childrenOverview.append(childEl);
@@ -207,25 +218,230 @@ async function showChildrenOverview(hideChildrenOverview) {
$childrenOverview.show(); $childrenOverview.show();
} }
async function loadLabelList() { async function loadAttributes() {
$promotedAttributesContainer.empty();
$attributeList.hide();
const noteId = getCurrentNoteId(); 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) { let idx = 1;
for (const label of labels) {
$labelListInner.append(utils.formatLabel(label) + " "); 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(); if (definition.multiplicityType === "multivalue") {
} const addButton = $("<span>")
else { .addClass("glyphicon glyphicon-plus pointer")
$labelList.hide(); .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) { async function loadNote(noteId) {
@@ -240,7 +456,7 @@ function focus() {
getComponent(note.type).focus(); getComponent(note.type).focus();
} }
messagingService.subscribeToMessages(syncData => { messagingService.subscribeToSyncMessages(syncData => {
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getCurrentNoteId())) { if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getCurrentNoteId())) {
infoService.showMessage('Reloading note because of background changes'); 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(() => { $(document).ready(() => {
$noteTitle.on('input', () => { $noteTitle.on('input', () => {
noteChanged(); noteChanged();
@@ -276,7 +521,7 @@ export default {
getCurrentNoteId, getCurrentNoteId,
newNoteCreated, newNoteCreated,
focus, focus,
loadLabelList, loadAttributes,
saveNote, saveNote,
saveNoteIfChanged, saveNoteIfChanged,
noteChanged noteChanged

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