Compare commits

..

429 Commits

Author SHA1 Message Date
zadam 7f9bcc162e release 0.44.3-beta 2020-09-19 23:45:46 +02:00
zadam 27052143ab fix placement of "Edit" button on read only notes 2020-09-19 23:44:30 +02:00
zadam 50d3ddbed5 added remove formatting plugin, closes #1239 2020-09-19 23:43:50 +02:00
zadam 7ee50113d7 fix export root note 2020-09-19 22:47:14 +02:00
zadam 07bd5129ca similarity tweaks 2020-09-19 21:59:01 +02:00
zadam ec7021b436 added debugModeEnabled option 2020-09-18 23:22:28 +02:00
zadam c75439a4b5 similarity tweaks 2020-09-18 21:47:59 +02:00
zadam d21cb78baa fix wrong dateLastEdited timestamp, closes #1254 2020-09-18 21:47:15 +02:00
zadam c371873b7e support unicode in button names, closes #1250 2020-09-17 14:41:00 +02:00
zadam 6220b02ef0 similarity tweaks 2020-09-17 14:34:10 +02:00
zadam 4e15bc0bb1 similarity tweaks 2020-09-17 09:33:43 +02:00
zadam 2f720df9d2 migration to convert H1 to H2, #1240 2020-09-16 20:51:57 +02:00
zadam d57a303251 fix in ENEX import 2020-09-16 20:32:20 +02:00
zadam 13b9f5231c similar note tweaks 2020-09-16 17:34:48 +02:00
zadam 71ed24344c performance optimizations in similar notes 2020-09-16 14:23:59 +02:00
zadam 0c8a2a23a7 refactor similarity logic 2020-09-16 11:50:47 +02:00
zadam 5738f2a5b3 similar notes tweaks 2020-09-16 11:24:29 +02:00
zadam d345c4850f custom implementation of similar notes algorithm 2020-09-15 22:46:51 +02:00
zadam eeacd8118f similar notes changes 2020-09-15 16:46:03 +02:00
zadam f5216e4799 close note type dropdown after click 2020-09-15 00:13:25 +02:00
zadam 85d13b1d62 resort relationships in tree cache after note reordering 2020-09-14 22:48:20 +02:00
zadam f5f48ef6c4 copy child: attrs also when defined on a template/parent note 2020-09-14 21:08:11 +02:00
zadam f1ed114167 small blink on attr save as a visual hint 2020-09-14 21:07:00 +02:00
zadam 70995a7691 fix broken saved search, closes #1234 2020-09-14 20:00:36 +02:00
zadam a82b8cdb37 release 0.44.2-beta 2020-09-13 23:03:28 +02:00
zadam 33511c0c10 fix hiding similar notes widget by label 2020-09-13 22:47:23 +02:00
zadam 04c93d6736 fix attr detail related notes 2020-09-13 22:23:03 +02:00
zadam 5e11806110 sidebar widgets can be now disabled with labels 2020-09-13 21:59:31 +02:00
zadam 1e1709ca6a mobile layout fixes + fix mobile delete note 2020-09-13 21:12:22 +02:00
zadam 37602cfcae small tweaks 2020-09-13 00:19:50 +02:00
zadam 0ca0996751 no difference in styling when active tree node is in focus or not because focused class is unreliable 2020-09-12 23:06:03 +02:00
zadam 63ebb46049 fix inverse relation detection in relation maps 2020-09-12 22:21:44 +02:00
zadam 847766b434 fancytree 2.37.0 2020-09-12 21:25:47 +02:00
zadam 056c40c0d0 similar notes algorithm tweaks (WIP) 2020-09-10 21:01:46 +02:00
zadam bff5b015ea wrap note autocomplete for very long note path titles, closes #1221 2020-09-09 20:49:44 +02:00
zadam ee4ce3ffd8 display unhoist button on the left of the note title so that it's always visible, #1223 2020-09-08 23:25:21 +02:00
zadam a73733d0fc renamed ParsingContext to SearchContext 2020-09-08 21:47:37 +02:00
zadam 0c5f842626 improvements to note tooltip - include (some) notePath and attributes 2020-09-08 21:45:07 +02:00
zadam 8b4cf474bd cleanup/fixes of label/relation definition usages in the old style 2020-09-08 20:42:50 +02:00
zadam 80f269d844 moved similar notes from sidebar to the center pane 2020-09-07 23:35:41 +02:00
zadam 75c9db6432 fixed search API and its default settings to include archived notes 2020-09-07 22:11:16 +02:00
zadam 69e36d2677 refactoring for archived notes handling in note cache 2020-09-07 00:05:01 +02:00
zadam eb4d86f41d changed presentation of similar notes to "tag cloud" style 2020-09-06 22:48:52 +02:00
zadam b0bf88c487 fix storing dateLastEdited timestamp in note revisions 2020-09-06 22:04:11 +02:00
zadam 032ffa8367 fix printing port to console #1219 2020-09-06 20:58:26 +02:00
zadam 7400c91a4f missing space in inherited attribute list, fixes #1217 2020-09-06 20:53:59 +02:00
zadam fbdf089d5d release 0.44.1-beta 2020-09-05 23:43:06 +02:00
zadam a7505682ed more compact note revisions widget 2020-09-05 23:35:24 +02:00
zadam b148f3d032 more compact note info widget 2020-09-05 22:49:30 +02:00
zadam 874972a3d3 fix broken what links here widget 2020-09-05 22:45:26 +02:00
zadam 6d095b7250 fix popups/tooltips not disappearing after loading new note, closes #1214 2020-09-05 21:51:00 +02:00
zadam ceb762e56b fix zen mode with attributes, closes #1213 2020-09-04 23:35:10 +02:00
zadam 0c72d29684 fix zen mode with attributes, closes #1213 2020-09-04 22:54:50 +02:00
zadam 29efe3a492 fix showing a search box, closes #1212 2020-09-04 21:10:54 +02:00
zadam 5bdfb6e11f release 0.44.0-beta 2020-09-03 23:26:51 +02:00
zadam 11c63b7778 remove test as build step 2020-09-03 23:26:36 +02:00
zadam befec48506 release 0.44.0-bet 2020-09-03 23:25:23 +02:00
zadam 15d94e91db fix note autocomplete for non-recent notes 2020-09-03 22:37:51 +02:00
zadam 7787fa1758 small promoted attrs style tweaks 2020-09-03 22:22:21 +02:00
zadam 0bd9b849b2 fix problems with updating note in note cache when entities are coming out of order (e.g. sync) 2020-09-03 21:37:06 +02:00
zadam 918e827d91 fix potential null in mime or type in note cache 2020-09-03 17:51:37 +02:00
zadam 42ecc0e15b use just note title in note autocomplete input field 2020-09-03 17:38:11 +02:00
zadam a37fa0cb3f don't use   for attributes since it (duh) prevents line breaking in case of inherited attributes 2020-09-02 23:16:56 +02:00
zadam 647863b489 escape will close the attribute dialog and cancel changes 2020-09-02 16:40:19 +02:00
zadam e8114fcc4f fixes in attr detail positioning 2020-09-02 16:06:35 +02:00
zadam 0745d9ec0b fix attr help tooltip appearing only the second time 2020-09-01 23:47:56 +02:00
zadam 5e75d2380a fix error after confirming mention relation name 2020-09-01 23:43:09 +02:00
zadam 32c88c7481 keyboard shortcuts for focusing the attributes, adding a new label and relation 2020-09-01 23:18:28 +02:00
zadam 0aef99b98e attr detail automatically focuses the name input and allows saving with ctrl+enter. 2020-09-01 22:47:36 +02:00
zadam bf548f9d38 fix Jump To search input restoration 2020-09-01 00:05:19 +02:00
zadam a15f8d7e11 small fixes 2020-08-31 23:13:39 +02:00
zadam 574b71de63 remember last search in Jump To dialog when searching soon after the previous search 2020-08-31 21:00:23 +02:00
zadam 408ed8dbf0 fix "search in fulltext" button on "jump to note" dialog. 2020-08-31 20:46:55 +02:00
zadam 98f1bc51a5 Merge remote-tracking branch 'origin/stable' 2020-08-30 23:53:24 +02:00
zadam f3efb22a50 extra search tests 2020-08-30 23:42:24 +02:00
zadam 5fd58def11 small refactorings 2020-08-30 23:19:55 +02:00
zadam b793f8cb88 search will also look for type and mime by default 2020-08-30 23:12:49 +02:00
zadam 058edcfe15 fixed promoted attribute display 2020-08-30 22:39:15 +02:00
zadam 5d500de527 performance monitoring on frontend in dev environment 2020-08-30 22:29:38 +02:00
zadam 81207f3d27 updated demo document with scripts adjusted to syncified DB access 2020-08-29 23:33:15 +02:00
zadam 21575d862b attempt to fix consistency issue bug which caused synced note_contents to have NULL content when original is empty string 2020-08-29 23:08:53 +02:00
zadam 9359f05335 fix "edited notes on a day" for deleted notes 2020-08-29 22:22:24 +02:00
zadam 7fb22d41a0 allow disabling authentication for server version, closes #1132 2020-08-29 00:11:50 +02:00
zadam 2823bf3488 disable toggling navigation buttons because of rare performance issues 2020-08-28 23:49:24 +02:00
zadam 172f3689fa optimization of search 2020-08-28 23:20:22 +02:00
zadam 5a8c3f6a2b removed forgotten console.logs 2020-08-28 22:52:57 +02:00
zadam 416b7088c5 fix update/delete queries in SQL console 2020-08-28 22:04:35 +02:00
zadam 3f69a3c572 fix parsing attributes with non-breakable space 2020-08-28 16:00:30 +02:00
zadam 268910ed3b fix positioning of attr detail popup for smaller windows 2020-08-28 15:38:04 +02:00
zadam 8259b4a32c close down dropdown menus on click + smaller tweaks 2020-08-28 14:52:38 +02:00
zadam 9873dd1242 fix creating new note 2020-08-28 14:29:20 +02:00
zadam f4266754d8 release 0.43.4 2020-08-27 23:58:58 +02:00
zadam 30e84321f9 fixes to build 2020-08-27 23:54:02 +02:00
zadam 020a8766c8 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/public/app/widgets/note_tree.js
2020-08-27 23:25:10 +02:00
zadam dc288fb18c Merge remote-tracking branch 'origin/stable' into stable 2020-08-27 23:22:58 +02:00
zadam 26dfa1ffdb activate PDF preview also in server build, fixes #1208 2020-08-27 23:22:36 +02:00
zadam d4e1900f89 cleanup of useless files when building release artifacts 2020-08-27 23:04:00 +02:00
zadam 4cd1a0ee7d fix note cache for out of order synced entities 2020-08-27 22:37:57 +02:00
zadam 355780c595 sync fixes 2020-08-27 22:11:17 +02:00
zadam ef996f3919 bug fixes 2020-08-27 22:03:56 +02:00
zadam 445902d081 ckeditor 22 2020-08-27 15:24:23 +02:00
zadam 31d85ed8cc fixes and tweaks - readonly bar is smaller and on the right 2020-08-27 14:54:56 +02:00
zadam c8af250caa Merge remote-tracking branch 'origin/master' 2020-08-27 12:47:29 +02:00
zadam b7b13c1d8b make migration more robust for cases where attribute definitions don't contain valid values, closes #1207 2020-08-27 12:44:49 +02:00
zadam b05b9196a0 fix build with better-sqlite3 binaries 2020-08-26 23:51:30 +02:00
zadam fbe3995398 fixed tree hotkeys 2020-08-26 22:12:01 +02:00
zadam 6aa61ca306 initializing note tree synchronously 2020-08-26 16:50:16 +02:00
zadam 87161cfe0d fix issues with note tree 2020-08-25 23:25:15 +02:00
zadam 6a3e27eb62 smaller refactorings in note_tree 2020-08-24 23:33:27 +02:00
zadam b7947a40ea binaries for better-sqlite3, downgrage to electron 9 since build on windows doesn't for 2020-08-24 20:26:52 +02:00
zadam 1d813c82e4 display attribute help on the attr detail during editing 2020-08-23 23:38:05 +02:00
zadam 46e373e822 "smart" date values can now freely contain whitespaces 2020-08-23 21:53:50 +02:00
zadam a6c79c934c fix global menu button and extra window 2020-08-22 23:07:52 +02:00
zadam 08c612c876 fixed builds for linux desktop 2020-08-22 23:07:31 +02:00
zadam ed6181a85e added in-editor help for editing attributes 2020-08-21 23:08:53 +02:00
zadam 0533b95562 Merge remote-tracking branch 'origin/master' 2020-08-20 23:15:45 +02:00
zadam 5b4826fd08 extra tests for escaping 2020-08-20 23:15:38 +02:00
zadam 60e213433b Merge remote-tracking branch 'origin/stable' 2020-08-20 15:45:57 +02:00
zadam 153de63f4d update note title in link map when note is renamed 2020-08-20 15:45:33 +02:00
zadam 4d4d9f2000 small fixes to saved search 2020-08-20 15:28:28 +02:00
zadam 0b38e24185 fixes to saved search 2020-08-20 15:23:24 +02:00
zadam d037420acb note. prefix also separates fulltext tokens from expression tokens 2020-08-20 13:53:15 +02:00
zadam 20b4b059e3 fix jsdoc 2020-08-20 12:00:57 +02:00
zadam 28ebca1268 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	package-lock.json
#	package.json
#	src/services/image.js
2020-08-20 11:56:03 +02:00
zadam a89629b3de add fallback when resizing image fails, closes #1190 2020-08-20 11:34:14 +02:00
zadam eec850c11f fix toggle sidebar issues, closes #1196 2020-08-20 11:14:27 +02:00
zadam 52e0f838a9 fulltext now ignores archived notes like it used to in the old implementation 2020-08-20 00:15:08 +02:00
zadam d1f70efdb4 Merge remote-tracking branch 'origin/master' 2020-08-19 23:01:00 +02:00
zadam 5b1e229d7d added note.text + tests 2020-08-19 23:00:51 +02:00
zadam 5c55278c59 attr detail small visual tweaks 2020-08-19 18:05:07 +02:00
zadam 3c224e80ee fix opening links in attr detail 2020-08-19 17:59:55 +02:00
zadam 5741b380f0 fixed script support 2020-08-18 23:32:50 +02:00
zadam 7335844ae3 updated DB schmea 2020-08-18 22:37:23 +02:00
zadam 53c361945b fix setting mime after import + cleanup of old search code 2020-08-18 22:20:47 +02:00
zadam 03d7ee9abb compatibility with existing scripts 2020-08-18 21:32:45 +02:00
zadam 3670fbff49 frontend validation of attribute name + other changes and fixes 2020-08-17 23:54:18 +02:00
zadam f24e27dadd autocollapse notes after period of inactivity + some other changes, #1192 2020-08-17 20:58:34 +02:00
zadam 53b39e2e82 many changes related to #1192:
- use CSS contain wherever possible to reduce subtrees of forced reflows
- reduced dependency between note and note_contents updates which will reduce number of updates to components
- optimization of "many rows" querying
2020-08-16 22:57:48 +02:00
zadam c20577909c simplified and more consistent link handling, also fixes #1191 2020-08-15 22:30:40 +02:00
zadam a87b710626 attribute fixes 2020-08-15 21:24:17 +02:00
zadam 625483a8f2 fixes 2020-08-15 00:06:26 +02:00
zadam 121c819b79 fix sizing of toast since bootstrap 4.5.1 2020-08-14 23:53:37 +02:00
zadam 77a3bdce1f related notes get updated as user types in the attr detail dialog 2020-08-14 23:00:15 +02:00
zadam 827f8b42a0 Merge remote-tracking branch 'origin/stable' 2020-08-14 22:34:17 +02:00
zadam b815e10ff3 add running tests into the build process 2020-08-14 22:30:01 +02:00
zadam c2789bf355 use relative paths for fonts in steel blue in demo document, closes #1187 2020-08-14 21:04:18 +02:00
zadam e8d63b5647 add "search in note" to "note actions" menu, #1184 2020-08-14 20:58:19 +02:00
zadam 53c6542af6 Merge remote-tracking branch 'origin/master' 2020-08-14 00:11:35 +02:00
zadam d9c246f8a7 fixes for promoted attribute multiplicity 2020-08-14 00:11:26 +02:00
dependabot[bot] 803694d113 Bump lodash from 4.17.15 to 4.17.20 (#1189)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.20.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.20)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-08-13 23:29:27 +02:00
zadam f6f9f0d2c0 ability to remove attribute with mouse + other tweaks 2020-08-13 23:23:57 +02:00
zadam 47a9516ada Merge remote-tracking branch 'origin/stable' 2020-08-13 00:00:05 +02:00
zadam bd8b83898f Merge remote-tracking branch 'origin/stable' into stable 2020-08-12 23:59:42 +02:00
zadam 97109efb6c fix attribute cache invalidation 2020-08-12 23:59:33 +02:00
zadam b4d9fdaeda fix attribute cache invalidation 2020-08-12 23:58:32 +02:00
zadam 1810116a34 attribute fixes 2020-08-12 23:39:05 +02:00
Jody 3e89855aa3 Add web app manifest (#1174)
* Add web app manifest, link to new manifest on mobile template.

* Remove duplicate manifest entry.
2020-08-12 21:39:12 +02:00
zadam eaac65142f tweaks in attr list sizing 2020-08-12 21:36:43 +02:00
zadam ae909d1079 fix saving & refreshing attributes across compoments 2020-08-12 00:02:19 +02:00
zadam 935e5de493 small fixes related to inherited attributes 2020-08-11 22:52:17 +02:00
zadam 8d0df6ddb9 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	db/demo.zip
#	src/services/notes.js
#	src/services/request.js
2020-08-10 23:45:17 +02:00
zadam 3b148eb6f8 fixed demo document to correctly show included notes 2020-08-10 23:42:06 +02:00
zadam 4e8d1dac67 fixes for rendering PDF previews e.g. in include note 2020-08-10 23:39:17 +02:00
zadam 7779fd1dfe synchronize the removal of the selection in cuttonote to make it more predictable 2020-08-10 22:42:57 +02:00
zadam 960d7dede3 add scrolling margins, #1181 2020-08-09 23:20:57 +02:00
zadam e663d7da1b updated schema for entity_changes table 2020-08-09 22:44:53 +02:00
zadam 02cc52af66 small improvements and fixes 2020-08-06 23:55:17 +02:00
zadam f60af1f05e small improvements and fixes 2020-08-06 00:06:42 +02:00
zadam 224fbdc8cd small improvements to text preview in file notes 2020-08-04 22:06:25 +02:00
zadam 8561201abc invalidate note complement cache quickly after load 2020-08-04 21:57:08 +02:00
zadam 3c1a809276 external links should open only in new window, not in the original window, closes #1171 2020-08-04 20:42:32 +02:00
zadam ddf0a89e1f bootstrap 4.5.1 2020-08-04 20:39:16 +02:00
zadam 4b101baf00 CKEditor 21 content styles 2020-08-03 23:36:33 +02:00
zadam 782127dd91 fix noproxy handling for image downloading 2020-08-03 23:33:44 +02:00
zadam 7900622f38 refactored "sync" table to "entity_changes" - more changes 2020-08-02 23:43:39 +02:00
zadam 864271d5ef refactored "sync" table to "entity_changes" 2020-08-02 23:27:48 +02:00
zadam 5a5ea4f474 fix filling sync table after demo document import 2020-08-02 22:53:57 +02:00
zadam 749791f6d1 fixed CSP after upgrade to helmet 4 2020-08-02 22:26:02 +02:00
zadam 5ca3e9a0bc fixed db migrations after merge from stable 2020-08-02 20:44:46 +02:00
zadam f4a4e746bf Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	libraries/ckeditor/ckeditor.js.map
#	package.json
#	src/routes/api/login.js
#	src/routes/api/sync.js
#	src/routes/index.js
#	src/services/app_info.js
#	src/services/sync.js
2020-08-02 20:41:22 +02:00
zadam 4fc8bace94 support horizontal line, closes #1164 2020-08-01 23:41:30 +02:00
zadam 47a22f6e8d release 0.43.3 2020-07-31 23:34:05 +02:00
zadam 17d7ff3ff1 small improvements to sync table handling 2020-07-31 00:08:01 +02:00
zadam 3582013a33 import of initial demo document sets sync.isSynced incorrectly to always 0, #1163 2020-07-31 00:07:38 +02:00
zadam 95bbdb3b6b CKEditor 21 2020-07-29 23:34:49 +02:00
zadam e1dd933ec0 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/services/options_init.js
2020-07-28 23:50:54 +02:00
zadam 8a57960c6e tweaks in sync timeout handling 2020-07-28 23:29:12 +02:00
zadam a7b62b30cb fix image resizing after save/update/import 2020-07-28 00:26:47 +02:00
zadam c9cbc2db02 import fixes 2020-07-27 23:40:14 +02:00
zadam c92526372c Merge remote-tracking branch 'origin/stable' 2020-07-26 23:47:31 +02:00
zadam 5f4a84d967 fix extracting base64 inline images from HTML, fixes #1159 2020-07-26 23:47:06 +02:00
zadam ef59810f89 Merge remote-tracking branch 'origin/stable' 2020-07-26 22:58:54 +02:00
zadam 099e90ed64 fix extracting base64 inline images from HTML, fixes #1159 2020-07-26 22:58:22 +02:00
zadam 9b15437ff6 attr detail sizing 2020-07-26 00:09:53 +02:00
zadam a3a2f1a2b4 autocompletes for attribute detail 2020-07-26 00:04:34 +02:00
zadam 3261fea67f added promotedAttributesExpanded option 2020-07-25 23:32:46 +02:00
zadam b19589033d individual panes of attribute list are now opened correctly 2020-07-25 23:24:48 +02:00
zadam 2217c5a3e0 promoted attributes are now part of attr list and also responsive 2020-07-25 00:06:49 +02:00
zadam b60efbbf5a Merge remote-tracking branch 'origin/master' 2020-07-24 23:14:38 +02:00
zadam e9bfe4521e bug fixes 2020-07-24 23:14:31 +02:00
MeIchthys 79d9d79055 Minor gramatical changes (#1162)
* Minor gramatical change

change "delete also" to "also delete"

* Minor gramatical change

change "once more" to "confirmation"

* Minor gramatical changes

add "a" before some nouns.

* Minor gramatical/spelling fixes

* Minor gramatical changes

* Convert to singular action descriptions

change "changes" to "change" 
change "switches" to "switch"
change "copies" to "copy" 
etc.

* Minor gramatical changes
2020-07-24 22:03:39 +02:00
zadam a36022d8c1 fixes 2020-07-24 00:02:15 +02:00
zadam 0eb77e43e2 full text search uses in-memory search even for unprotected notes and preprocesses content with tag stripping 2020-07-23 23:38:38 +02:00
zadam e1d4be814f toggle to expand/collapse attr list is saved and propagated across tabs 2020-07-23 22:31:06 +02:00
zadam aa4a645670 adding a button to the detail to explicitly save changes. 2020-07-23 00:19:50 +02:00
zadam 2c9565b4b3 fixes to update from detail to editor 2020-07-22 23:58:01 +02:00
zadam c5cc6b91c1 label and relation definition are now displayed without prefixes in the attr detail view 2020-07-22 23:42:58 +02:00
zadam 3109233d4f better reporting for search parsing errors 2020-07-22 22:52:15 +02:00
zadam 60e8bd98b9 improvements to lexer and its tests 2020-07-21 23:42:59 +02:00
zadam 32ecb43b5c search refactoring 2020-07-21 00:01:07 +02:00
zadam f03e4bc7d7 detect attempts to compare between labels (only with constants is possible) 2020-07-20 23:27:30 +02:00
zadam 4e5ddaf7d1 fix inQuotes + tests 2020-07-19 23:23:48 +02:00
zadam 35469f6f2d lexer now marks "in quotes" tokens 2020-07-19 23:19:45 +02:00
zadam 4c7b1d6543 shortcut negation syntax 2020-07-19 15:25:24 +02:00
zadam df69b1d8dd attr detail handles label/relation definition updates 2020-07-18 23:45:28 +02:00
zadam 9d46c7253b fixes in saving 2020-07-18 00:20:24 +02:00
zadam a94ae81c30 split out attribute_editor widget, WIP 2020-07-17 23:55:59 +02:00
zadam 9f527f0330 split out attribute_editor widget, WIP 2020-07-17 00:08:28 +02:00
zadam 68d8b1c8a9 rename note_attributes widget to attribute_list 2020-07-16 23:12:10 +02:00
zadam 0ab9d02434 fancytree 2.36.0 2020-07-15 22:40:58 +02:00
zadam 3d324b954d fix checking affected notes when modified attribute's owning note is not loaded into cache, #803 2020-07-15 22:36:27 +02:00
zadam f028b88cce Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	package.json
#	src/routes/api/clipper.js
2020-07-15 00:11:26 +02:00
zadam 64dc522b27 infrastructure for editing attribute definitions 2020-07-15 00:09:37 +02:00
zadam 3d02f82c77 add label/relation from menu 2020-07-14 23:29:37 +02:00
zadam 528418aecb parsing and setting isInheritable flag 2020-07-13 23:27:23 +02:00
zadam 5dd73562da add new attr wip 2020-07-13 00:27:21 +02:00
zadam 62cacf85ed setting the relation target note 2020-07-13 00:15:00 +02:00
zadam 443f389d73 release 0.43.2 2020-07-11 23:58:59 +02:00
zadam 08edc521e4 fix visibility of hyper-link in repeatedly opened dialog 2020-07-11 23:50:38 +02:00
zadam f54f6d09b0 fix declaring global variables on the setup page, closes #1071 2020-07-11 23:19:24 +02:00
zadam 3219441fdf fallback for missing parsed title, closes trilium-web-clipper/issues/16 2020-07-11 23:05:28 +02:00
zadam 1c97310e92 correctly parsing the click position 2020-07-10 22:39:16 +02:00
zadam 0ecb2f3662 WIP 2020-07-09 23:59:27 +02:00
zadam 055db32510 removed tar import 2020-07-09 21:46:33 +02:00
zadam 054420225f added experimental "save" button 2020-07-09 00:07:14 +02:00
zadam 448a3254a3 small tweaks 2020-07-08 23:51:47 +02:00
zadam b2c0eaeaf8 Merge remote-tracking branch 'origin/stable' 2020-07-08 23:14:26 +02:00
zadam 1b0a2b41da added tab and shift-tab shortcuts for indent/outdent, closes #978 2020-07-08 21:57:09 +02:00
zadam 50b976990c click on inherited attr will also bring up the attribute detail widget 2020-07-05 23:53:55 +02:00
zadam 9f2545395d better value matching regex 2020-07-04 11:14:11 +02:00
zadam 67bffbf30b consistency checks will now fix incorrectly named attributes 2020-07-04 11:02:05 +02:00
zadam 51dae378a3 more cleanup 2020-07-04 10:29:17 +02:00
zadam a0b3bc858d removed attributes dialog 2020-07-04 10:18:01 +02:00
zadam 2e24111c2b wip 2020-07-04 00:40:41 +02:00
zadam 69d529ada1 wip 2020-07-04 00:20:23 +02:00
zadam bfa69a3dc3 closing of attr detail 2020-07-03 22:27:45 +02:00
zadam 5b862c3d29 Merge remote-tracking branch 'origin/stable' 2020-07-02 23:21:17 +02:00
zadam dee4083bf3 improve request logging 2020-07-02 23:15:37 +02:00
zadam 18749092ff sql_init/migration fixes 2020-07-02 22:57:17 +02:00
zadam c4cc48dfc9 fixed initial demo import 2020-07-02 21:08:18 +02:00
zadam ef847b9904 fix search results 2020-07-02 20:19:09 +02:00
zadam 3e189d5c10 fixes 2020-07-01 23:50:33 +02:00
zadam 46d3160da4 removed detail.css, added to individual type widgets 2020-07-01 23:35:00 +02:00
zadam 78a2c1753e consistency checks fixes 2020-07-01 22:42:59 +02:00
zadam f0b608ddec non-intrusive logging of sync errors 2020-07-01 21:33:52 +02:00
zadam 582429e762 updated content styles 2020-07-01 21:03:51 +02:00
zadam bf073690e0 fixing promoted attributes 2020-07-01 00:02:13 +02:00
zadam c012620338 removed now unused image minifying binaries 2020-06-30 23:41:13 +02:00
zadam 5e18e7dc67 html sanitize imported notes, #1137 2020-06-30 23:37:06 +02:00
zadam 51f094f87f image handling consistency fixes 2020-06-28 23:10:45 +02:00
zadam 262a4fa3c7 removed image minification using imagemin because of constant platform related issues 2020-06-28 22:36:42 +02:00
zadam c778e87683 wip 2020-06-27 00:40:35 +02:00
zadam dd62b306fd refactored attr detail into separate widget 2020-06-26 22:17:39 +02:00
zadam 1f05638609 attribute progress 2020-06-25 23:56:06 +02:00
zadam 92e49214c7 more attribute mockups (inherited attributes) 2020-06-24 23:56:39 +02:00
zadam 9615e25a67 fix merge problems 2020-06-24 22:29:53 +02:00
zadam a574fce13f Merge remote-tracking branch 'origin/stable' into syncification
# Conflicts:
#	package-lock.json
#	package.json
#	src/routes/custom.js
#	src/services/import/single.js
2020-06-24 22:08:31 +02:00
zadam 238df0fb40 fix SVG single file import as image and not as file, closes #1114 2020-06-24 21:47:50 +02:00
zadam 89356918f1 fix unescaped HTML in the tree node title, closes #1127 2020-06-24 21:07:55 +02:00
zadam 263b65997c hide "type around" controls in printed PDF, closes #1129 2020-06-24 20:44:00 +02:00
zadam 2b757bfccd upgrade ckeditor to 20.0.0 2020-06-24 16:17:39 +02:00
zadam 99d66ea89f mockup of hidable attributes 2020-06-24 00:02:54 +02:00
zadam c78ca4c9db fixed triggers to sort children of notes with "sorted" label, closes #1126 2020-06-23 22:03:01 +02:00
zadam b245b249d3 got rid of simple-node-logger in favor of simple custom logger 2020-06-23 13:38:27 +02:00
zadam a0395e9866 release 0.43.1 2020-06-23 10:11:17 +02:00
zadam 89aa4fbc73 electron 9.0.5 2020-06-23 10:10:59 +02:00
zadam 74a7802088 fix custom resource handler, closes #1125 2020-06-22 23:13:53 +02:00
zadam a89b6711d1 refactored code to not depend on external elements, #1120 2020-06-22 22:28:45 +02:00
zadam b2549b2834 Merge remote-tracking branch 'origin/stable' into stable 2020-06-22 22:00:22 +02:00
zadam 959c4cbe64 removed icon tooltip again 2020-06-22 22:00:08 +02:00
Shon Ramamurthy d03d3603d2 Add optional support for note title tooltips under note tree widget (#1120)
* Add support for note title tooltips under note tree widget

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

* Revert DB Upgrade

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

* Simplify boolean option comparison

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

* Display tooltip only on center-pane overlap - Experimental

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

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

* Remove option to set tooltip

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

* Revert further changes related to the options menu

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

* Remove debug logging

Remove debug logging and unnecessary line breaks.

* Move note-title tooltip handler under note_tree.js

With this change, we move the definition for the note-title tooltip
handler inside 'note_tree.js'. Registration is done inside
'side_pane_toggles.js' as we would need the handler to detect the
'center-pane' element first before detecting collisions.
2020-06-22 21:58:58 +02:00
zadam 22302e8200 attrs WIP 2020-06-21 23:41:51 +02:00
zadam f117ae4915 fix sending sync rows via WebSocket after transaction is committed 2020-06-21 13:44:47 +02:00
zadam c34a9b96e4 further simplification - now using better-sqlite3 native transaction API 2020-06-21 13:33:56 +02:00
zadam f0acfaf147 simplification of transaction handling 2020-06-21 13:15:36 +02:00
zadam e1c2573778 add tooltip to fancytree node icon, #1120 2020-06-21 12:47:24 +02:00
zadam 56d6384ae1 added protected note full text search 2020-06-20 23:46:49 +02:00
zadam 5f699cc28c converted most dynamic SQL queries into prepared statement to avoid excessive statement caching 2020-06-20 23:24:34 +02:00
zadam 969f31dde2 fixed backup and anonymization with better-sqlite3 2020-06-20 23:09:34 +02:00
zadam 027afab6b1 fix DB setup 2020-06-20 21:42:41 +02:00
zadam 6207203b35 wip 2020-06-20 13:18:03 +02:00
zadam 88348c560c syncification 2020-06-20 12:31:38 +02:00
zadam 30062d687f Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	libraries/ckeditor/ckeditor.js
#	libraries/ckeditor/ckeditor.js.map
2020-06-20 09:40:11 +02:00
zadam ffc96ad6b2 parser status indication 2020-06-20 09:39:44 +02:00
zadam 2af2b45062 fix keyboard shortcut for print this note, #1118 2020-06-20 00:04:31 +02:00
zadam eabe4775bd fix click handler of links inside included note 2020-06-19 23:20:52 +02:00
zadam da9b321aa0 allow mouse interactions in ckeditor widgets, fixes #1117 2020-06-19 22:24:57 +02:00
zadam 7b0aa7ab0b parser status indication 2020-06-18 23:53:57 +02:00
zadam f700e6e12b use standard search to find notes with same attrs 2020-06-18 22:28:18 +02:00
zadam a87e59cecb WIP of attribute view on click 2020-06-18 12:52:16 +02:00
zadam a04aa6fd36 version updates 2020-06-18 09:12:30 +02:00
zadam a7d9870846 Merge remote-tracking branch 'origin/better-sqlite3'
# Conflicts:
#	libraries/ckeditor/ckeditor.js
#	libraries/ckeditor/ckeditor.js.map
#	package-lock.json
#	package.json
#	src/public/app/services/utils.js
#	src/public/app/widgets/type_widgets/editable_text.js
#	src/services/utils.js
2020-06-18 09:03:09 +02:00
zadam c18d8d2d1b add back lost edited notes widget, closes #1115 2020-06-18 08:56:23 +02:00
zadam f745e21e0d use better-sqlite3 2020-06-17 23:03:46 +02:00
zadam 5f2361ebd5 release 0.43.0-beta 2020-06-15 23:26:12 +02:00
zadam 9791dab97d recent changes sorting fixes, closes #1099 2020-06-15 23:22:11 +02:00
zadam 85d986534d fix enforcing node's http requests for sync 2020-06-15 18:24:43 +02:00
zadam 00faf758e8 fixes and polish 2020-06-15 17:56:53 +02:00
zadam 6ba2e5cf73 add word count widget to the demo document (plus cleanup of external links) 2020-06-14 16:29:29 +02:00
zadam fb3876d28b promoted attr selection using autocomplete will trigger change event to save it, closes #699 2020-06-14 16:04:00 +02:00
zadam fb975849b9 small API additions 2020-06-14 14:30:57 +02:00
zadam 16fef78344 add API method to force refresh of included notes, closes #1106 2020-06-14 10:50:08 +02:00
zadam e0b4b369dc transaction handling fixes 2020-06-14 00:35:53 +02:00
zadam 0df7851214 fix sync 2020-06-13 22:34:15 +02:00
zadam 5d47c2b23e opening transactions only on write operations which enforces exclusive lock only there to improve concurrency, custom handling of sync request timeouts, #1093, #1018 2020-06-13 10:23:36 +02:00
zadam d09b021487 add time limit to frontend update 2020-06-11 00:13:56 +02:00
zadam 910bda860c fix delete note function just work one time, closes #1101 2020-06-10 23:43:59 +02:00
zadam 2d92b4931a fix ctrl+click opening the link twice/thrice, closes #1094 2020-06-10 00:10:27 +02:00
zadam 212b719ee9 better detection of changes in attributes and how they affect notes 2020-06-09 22:59:22 +02:00
zadam 1db892d22f return the ability to hide archived notes, closes #1095 2020-06-08 23:15:49 +02:00
zadam 535dcb6d12 release 0.42.7 2020-06-08 10:43:12 +02:00
zadam 4426362799 cleanup sqlite to make the distributed archives smaller 2020-06-08 10:42:40 +02:00
zadam 2c609e8136 promoted attributes widget is now auto-updating, fixes #700 2020-06-08 00:29:52 +02:00
zadam 11b73b79ed refresh promoted attributes when change detected 2020-06-07 23:57:10 +02:00
zadam 28dc6b93ca lock CPU info for perf context 2020-06-07 23:56:51 +02:00
zadam e70c862e72 fix import 2020-06-07 23:55:55 +02:00
zadam b3e66d5a83 fixed command line anonymization 2020-06-07 10:45:41 +02:00
zadam e8cd821e57 futrther improvements to anonymization 2020-06-07 10:20:48 +02:00
zadam 401a3f0ecb add info on read only notes, #1088 2020-06-06 18:46:45 +02:00
zadam e73dffad0b parse incomplete attrs 2020-06-06 12:56:24 +02:00
zadam ef1d062745 attribute parser preserves indexes from original string 2020-06-06 10:39:27 +02:00
zadam f245d51746 basic saving of attributes in the widget 2020-06-05 17:25:14 +02:00
zadam be7ac74235 better fallback for resolving filenames of binary attachments 2020-06-05 10:40:35 +02:00
zadam ad48b59893 no need to reload whole subtree while moving notes 2020-06-05 00:07:45 +02:00
zadam e817acba4e fix template attribute cycle in note cache 2020-06-04 23:58:31 +02:00
zadam 4ae965c5cb Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/public/app/widgets/collapsible_widgets/note_info.js
2020-06-04 22:37:04 +02:00
zadam 58fa0832f6 fix focusing title after creating a note 2020-06-04 21:44:34 +02:00
zadam 1502b9ce66 prevent attribute inheritance cycle via template, closes #1077 2020-06-04 12:27:41 +02:00
zadam ed9fbae65d attribute parser and tests WIP 2020-06-04 00:04:57 +02:00
zadam 01a7af965c refactoring of es6 test skeleton 2020-06-03 17:28:57 +02:00
zadam 99120be46d skeleton of first tests 2020-06-03 17:11:03 +02:00
zadam 915b1d1a45 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	libraries/ckeditor/ckeditor.js
#	libraries/ckeditor/ckeditor.js.map
#	package-lock.json
#	package.json
#	src/public/app/widgets/type_widgets/editable_text.js
2020-06-03 16:25:45 +02:00
zadam e68aed2057 Merge remote-tracking branch 'origin/master' 2020-06-03 16:24:52 +02:00
zadam eedeaca37d wip 2020-06-03 16:24:41 +02:00
zadam 7307ca385f release 0.42.6 2020-06-03 14:30:07 +02:00
zadam c1fd9825aa fix backup 2020-06-03 12:16:16 +02:00
zadam 9de7d3fc53 fix unloading protected session after clicking on a button, closes #1078 2020-06-03 11:47:30 +02:00
zadam 3c5db844ba fix tree focusing issues 2020-06-03 11:06:45 +02:00
zadam e7330c1104 more anonymization 2020-06-03 09:55:05 +02:00
zadam ec4586b164 fix reference link implementation, closes #1069 2020-06-02 23:54:33 +02:00
zadam 91e5f24798 fix db anonymization 2020-06-02 23:13:55 +02:00
zadam 38723e0189 release 0.42.5 2020-05-31 23:33:30 +02:00
zadam 8c88ce6f65 fix moving/cloning notes broken in 0.42.4, closes #1066 2020-05-31 22:33:02 +02:00
Adam Coyne 0def5f9a63 Add a VSCode debug configuration matching the start-server npm script (#1061) 2020-05-31 12:40:53 +02:00
Adam Coyne ac10701b55 Add import option to replace underscores with spaces in note names (#1064) 2020-05-30 23:15:00 +02:00
Adam Coyne c0cd321118 Fix import error (#1060)
* Fix access to attributes on undefined noteMeta

* Remove .markdown extension from imported page titles
2020-05-30 10:09:10 +02:00
zadam d65624d8d2 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	libraries/ckeditor/ckeditor.js
#	libraries/ckeditor/ckeditor.js.map
2020-05-29 22:29:28 +02:00
Ionuț Staicu 1c388c9805 cross platform compatibility (#1051) 2020-05-29 12:01:20 +02:00
zadam 6a957eff17 added autocompletes for labels, relations 2020-05-28 23:59:08 +02:00
zadam 5aaa429203 WIP of new attribute widget 2020-05-28 00:17:13 +02:00
zadam ae934720bc Merge branch 'm43' 2020-05-27 00:09:51 +02:00
zadam 753c181979 Merge remote-tracking branch 'origin/stable' 2020-05-27 00:09:40 +02:00
zadam 55b210d7c5 added not() expression 2020-05-27 00:09:19 +02:00
zadam dc2d5a0a79 fix fulltext content search 2020-05-26 23:25:13 +02:00
zadam c753f228ac more tests & fixes 2020-05-25 23:39:34 +02:00
zadam a1a744bb00 order & limit implementation WIP 2020-05-25 00:25:47 +02:00
zadam 077d19b44b Create SECURITY.md 2020-05-24 19:34:52 +02:00
zadam b5627b138a integrate new search into the main global search 2020-05-24 09:30:08 +02:00
zadam cb4d0624b5 put session directory into data dir to avoid conflict of multiple instances on a single server, fixes #1033 2020-05-24 00:21:20 +02:00
zadam e2490f9975 faster implementation of ancestors 2020-05-23 23:57:59 +02:00
zadam 9ede77aead added ancestor 2020-05-23 23:44:55 +02:00
zadam 8ce2afff8a more tests 2020-05-23 22:18:06 +02:00
zadam a2e1fb35b8 tests for note properties 2020-05-23 20:52:55 +02:00
zadam bb03a8714a more tests 2020-05-23 18:42:32 +02:00
zadam ae772288e2 support for long syntax of labels and relations 2020-05-23 18:13:35 +02:00
zadam 355ffd3d02 added querying by relation's properties 2020-05-23 12:27:44 +02:00
zadam 3d12341ff1 added querying by children 2020-05-23 10:36:49 +02:00
zadam 4ea934509e implemented property based access + parent 2020-05-23 10:25:22 +02:00
zadam 714881ad99 more search tests + numeric label comparison 2020-05-22 19:08:06 +02:00
zadam ee053b9fdf basic search tests 2020-05-22 09:38:30 +02:00
zadam cd48135394 refactoring to allow unit tests of the whole search subsystem 2020-05-21 14:05:56 +02:00
zadam a06662f4ce fuzzy search for values as well 2020-05-21 13:45:18 +02:00
zadam 2e6395ad88 prefix match for autocomplete attribute search 2020-05-21 12:05:12 +02:00
zadam 75d8627f1c refactoring to ParserContext 2020-05-21 11:46:01 +02:00
zadam a8d12f723f fix highlighting 2020-05-21 11:18:15 +02:00
zadam 08dbf90a8c hide body during startup to reduce flicker 2020-05-21 09:42:25 +02:00
zadam 32dde426fd apply new query parsing to note autocomplete 2020-05-21 00:39:17 +02:00
zadam b26100479d parser tests added 2020-05-20 23:20:39 +02:00
zadam faf4daa577 shadow on hover for title bar buttons 2020-05-20 19:14:07 +02:00
zadam 99aa481ace refactoring for testing parser 2020-05-20 00:03:33 +02:00
zadam ef37bc1a99 Merge remote-tracking branch 'origin/stable' into m43
# Conflicts:
#	.idea/dataSources.xml
2020-05-19 23:20:32 +02:00
zadam b72dc977e6 parens handler + parser in progress 2020-05-19 00:00:35 +02:00
zadam 81bf84f2de lexer fixes + tests 2020-05-17 23:14:24 +02:00
zadam e77e0ce675 lexer impl WIP + test 2020-05-17 19:43:37 +02:00
zadam 32eaafd024 note cache fixes after refactoring 2020-05-17 10:11:19 +02:00
zadam 60c2049729 note cache refactoring WIP 2020-05-17 09:48:24 +02:00
zadam dcd371b5b1 note cache breakup into classes, WIP 2020-05-16 23:12:29 +02:00
zadam e3071e630a note cache refactoring WIP 2020-05-16 22:11:09 +02:00
zadam 78ea0b4ba9 refactoring ... 2020-05-15 23:50:36 +02:00
zadam f07025f741 start of note cache expression implementation 2020-05-15 11:04:55 +02:00
zadam 5f1f65a3c2 fuction reorganization 2020-05-14 23:21:48 +02:00
zadam a3e2369599 add also content fulltext 2020-05-14 23:11:59 +02:00
zadam 1ec446137d fulltext also searches for branch prefixes 2020-05-14 21:30:36 +02:00
zadam a287bb59ea note cache fixes for created notes 2020-05-13 23:06:13 +02:00
zadam 7992f32d34 note cache refactoring 2020-05-13 14:42:16 +02:00
zadam b07accfd9d note cache refactoring + handling entity changes 2020-05-13 10:47:22 +02:00
zadam ccb5f3ee18 searching now works correctly in inherited attributes 2020-05-13 00:01:10 +02:00
zadam 29e6b63f82 Merge remote-tracking branch 'origin/master' into m43 2020-05-12 21:15:54 +02:00
zadam 2818854c4b Merge remote-tracking branch 'origin/stable' 2020-05-12 21:15:32 +02:00
zadam d75b770fb9 update of bootstrap to 4.5.0 2020-05-12 20:17:51 +02:00
zadam 13117d2e68 Merge remote-tracking branch 'origin/stable' 2020-05-12 13:41:06 +02:00
zadam b51f5ac6fd Merge remote-tracking branch 'origin/stable' 2020-05-12 10:59:02 +02:00
zadam 2e0fb8aaf1 fix creating new promoted attributes, closes #1008 2020-05-11 23:57:39 +02:00
zadam ac4e6077bd Merge remote-tracking branch 'origin/stable' 2020-05-11 23:23:59 +02:00
zadam 8e8fa48241 ws update 2020-05-11 23:23:18 +02:00
Naveen M V 9a662f76da Fix regex bug (#1005) 2020-05-11 22:06:22 +02:00
zadam 15bc9dce1c search overhaul WIP 2020-05-10 23:27:53 +02:00
zadam 552fc5261a new note cache WIP 2020-05-09 23:42:26 +02:00
zadam 83c9e6e846 Merge branch 'master' into m43 2020-05-09 20:17:26 +02:00
zadam 2e837642e2 create note directly from mention linking 2020-05-09 14:25:27 +02:00
zadam c70a842bc6 Merge remote-tracking branch 'origin/stable' 2020-05-08 23:44:51 +02:00
zadam d20415c979 bind global menu item "Open SQL console" to the logic to create such a note 2020-05-08 23:39:46 +02:00
zadam c3438e0f3f implemented SQL console as a type of code note 2020-05-08 22:22:43 +02:00
zadam 1bb895bb83 Merge remote-tracking branch 'origin/stable' 2020-05-07 23:36:11 +02:00
zadam f1ce0c7774 Merge branch 'm43-2'
# Conflicts:
#	src/public/app/services/app_context.js
#	src/public/app/widgets/run_script_buttons.js
2020-05-07 20:57:22 +02:00
zadam a0f8caeccd added SQLite (Trilium) mime type 2020-05-05 23:59:26 +02:00
zadam edb9bc9229 fix "Render note" and "Execute script" buttons + refactoring around data-trigger-command handling 2020-05-05 23:58:52 +02:00
zadam 768ac83e14 Merge branch 'master' into m43 2020-05-05 19:38:42 +02:00
zadam 7ac2206e9b start of search overhaul 2020-05-03 09:18:57 +02:00
331 changed files with 15936 additions and 12510 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="document.db" uuid="b0b03187-36c8-4ec1-bdab-fd4273cd692e">
<data-source source="LOCAL" name="document.db" uuid="4e69c96a-8a2b-43f5-9b40-d1608f75f7a4">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
+18
View File
@@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "start-server",
"skipFiles": [
"<node_internals>/**"
],
"env": {
"TRILIUM_ENV": "dev"
},
"outputCapture": "std",
"program": "${workspaceFolder}/src/www"
}
]
}
+13
View File
@@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
In the (still active) 0.X phase of the project only the latest stable minor release is getting bugfixes (including security ones).
So e.g. if the latest stable version is 0.42.3 and the latest beta version is 0.43.0-beta, then 0.42 line will still get security fixes but older versions (like 0.41.X) won't get any fixes.
Description above is a general rule and may be altered on case by case basis.
## Reporting a Vulnerability
You can report low severity vulnerabilities as GitHub issues, more severe vulnerabilities should be reported to the email zadam.apps@gmail.com
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6 -8
View File
@@ -7,15 +7,13 @@ then
./bin/copy-trilium.sh $SRC_DIR
fi
echo "Copying required linux-x64 binaries"
rm -r $SRC_DIR/node_modules/sqlite3/lib/binding/*
rm -r $SRC_DIR/node_modules/pngquant-bin/vendor/*
rm -r $SRC_DIR/src/public/app-dist/*.mobile.*
cp -r bin/deps/linux-x64/sqlite/* $SRC_DIR/node_modules/sqlite3/lib/binding/
cp bin/deps/linux-x64/image/pngquant $SRC_DIR/node_modules/pngquant-bin/vendor/
echo "Copying required linux-x64 binaries"
cp -r bin/better-sqlite3/linux-desktop-better_sqlite3.node $SRC_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node
echo "Packaging linux x64 electron build"
./node_modules/.bin/electron-packager $SRC_DIR --asar --out=dist --executable-name=trilium --platform=linux --arch=x64 --overwrite
@@ -41,4 +39,4 @@ tar cJf trilium-linux-x64-${VERSION}.tar.xz trilium-linux-x64
cd ..
bin/build-debian.sh
bin/build-debian.sh
+3 -9
View File
@@ -9,18 +9,12 @@ fi
echo "Copying required mac binaries"
rm -r $SRC_DIR/node_modules/sqlite3/lib/binding/*
rm -r $SRC_DIR/node_modules/mozjpeg/vendor/*
rm -r $SRC_DIR/node_modules/pngquant-bin/vendor/*
rm -r $SRC_DIR/node_modules/giflossy/vendor/*
cp -r bin/deps/mac-x64/sqlite/* $SRC_DIR/node_modules/sqlite3/lib/binding/
cp bin/deps/mac-x64/image/cjpeg $SRC_DIR/node_modules/mozjpeg/vendor/
cp bin/deps/mac-x64/image/pngquant $SRC_DIR/node_modules/pngquant-bin/vendor/
cp bin/deps/mac-x64/image/gifsicle $SRC_DIR/node_modules/giflossy/vendor/
cp -r bin/better-sqlite3/mac-better_sqlite3.node $SRC_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node
rm -r $SRC_DIR/src/public/app-dist/*.mobile.*
echo "Packaging mac x64 electron build"
./node_modules/.bin/electron-packager $SRC_DIR --asar --out=dist --executable-name=trilium --platform=darwin --arch=x64 --overwrite --icon=images/app-icons/mac/icon.icns
BUILD_DIR=./dist/trilium-mac-x64
+5 -4
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
PKG_DIR=dist/trilium-linux-x64-server
NODE_VERSION=12.16.3
NODE_VERSION=12.18.3
if [ "$1" != "DONTCOPY" ]
then
@@ -16,11 +16,12 @@ cd ..
mv dist/node-v${NODE_VERSION}-linux-x64 $PKG_DIR/node
rm -r $PKG_DIR/node/lib/node_modules/npm
rm -r $PKG_DIR/node/include/node
rm -r $PKG_DIR/node_modules/electron*
rm -r $PKG_DIR/node_modules/sqlite3/lib/binding/*
cp -r ./bin/deps/linux-x64/sqlite/node* $PKG_DIR/node_modules/sqlite3/lib/binding/
cp -r bin/better-sqlite3/linux-server-better_sqlite3.node $PKG_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node
printf "#!/bin/sh\n./node/bin/node src/www" > $PKG_DIR/trilium.sh
chmod 755 $PKG_DIR/trilium.sh
+3 -9
View File
@@ -9,18 +9,12 @@ fi
echo "Copying required windows binaries"
rm -r $SRC_DIR/node_modules/sqlite3/lib/binding/*
rm -r $SRC_DIR/node_modules/mozjpeg/vendor/*
rm -r $SRC_DIR/node_modules/pngquant-bin/vendor/*
rm -r $SRC_DIR/node_modules/giflossy/vendor/*
cp -r bin/deps/win-x64/sqlite/* $SRC_DIR/node_modules/sqlite3/lib/binding/
cp bin/deps/win-x64/image/cjpeg.exe $SRC_DIR/node_modules/mozjpeg/vendor/
cp bin/deps/win-x64/image/pngquant.exe $SRC_DIR/node_modules/pngquant-bin/vendor/
cp bin/deps/win-x64/image/gifsicle.exe $SRC_DIR/node_modules/giflossy/vendor/
cp -r bin/better-sqlite3/win-better_sqlite3.node $SRC_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node
rm -r $SRC_DIR/src/public/app-dist/*.mobile.*
echo "Packaging windows x64 electron build"
./node_modules/.bin/electron-packager $SRC_DIR --asar --out=dist --executable-name=trilium --platform=win32 --arch=x64 --overwrite --icon=images/app-icons/win/icon.ico
BUILD_DIR=./dist/trilium-windows-x64
+14 -1
View File
@@ -29,10 +29,23 @@ cp webpack-* $DIR/
# run in subshell (so we return to original dir)
(cd $DIR && npm install --only=prod)
# cleanup of useless files in dependencies
rm -r $DIR/node_modules/image-q/demo
rm -r $DIR/node_modules/better-sqlite3/Release
rm -r $DIR/node_modules/better-sqlite3/deps/sqlite3.tar.gz
rm -r $DIR/node_modules/@jimp/plugin-print/fonts
rm -r $DIR/node_modules/jimp/browser
rm -r $DIR/node_modules/jimp/fonts
# delete all tests (there are often large images as test file for jimp etc.)
find $DIR/node_modules -name test -exec rm -rf {} \;
find $DIR/node_modules -name docs -exec rm -rf {} \;
find $DIR/node_modules -name demo -exec rm -rf {} \;
find $DIR/libraries -name "*.map" -type f -delete
rm -r $DIR/src/public/app
sed -i -e 's/app\/desktop.js/app-dist\/desktop.js/g' $DIR/src/views/desktop.ejs
sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' $DIR/src/views/mobile.ejs
sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' $DIR/src/views/setup.ejs
sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' $DIR/src/views/setup.ejs
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
View File
@@ -2,6 +2,9 @@
# Instance name can be used to distinguish between different instances
instanceName=
# set to true to allow using Trilium without authentication (makes sense for server build only, desktop build doesn't need password)
noAuthentication=false
# Disable automatically generating desktop icon
# noDesktopIcon=true
BIN
View File
Binary file not shown.
@@ -1,4 +1,4 @@
const syncTableService = require('../../src/services/sync_table');
const syncTableService = require('../../src/services/entity_changes.js');
// options has not been filled so far which caused problems with clean-slate sync.
module.exports = async () => await syncTableService.fillAllSyncRows();
module.exports = async () => await syncTableService.fillAllSyncRows();
+2 -2
View File
@@ -1,5 +1,5 @@
const syncTableService = require('../../src/services/sync_table');
const syncTableService = require('../../src/services/entity_changes.js');
module.exports = async () => {
await syncTableService.fillAllSyncRows();
};
};
@@ -0,0 +1,4 @@
UPDATE sync SET isSynced = 1 WHERE entityName != 'options' OR (
entityName = 'options'
AND 1 = (SELECT isSynced FROM options WHERE name = sync.entityId)
)
@@ -0,0 +1,2 @@
UPDATE attributes SET type = 'label', name = 'label:' || name WHERE type = 'label-definition';
UPDATE attributes SET type = 'label', name = 'relation:' || name WHERE type = 'relation-definition';
+79
View File
@@ -0,0 +1,79 @@
const sql = require('../../src/services/sql');
module.exports = () => {
for (const attr of sql.getRows("SELECT * FROM attributes WHERE name LIKE 'label:%'")) {
let obj;
try {
obj = JSON.parse(attr.value);
}
catch (e) {
console.log(`Parsing attribute definition "${attr.value}" of ${attr.attributeId} failed with error "${e.message}", setting to default value.`);
sql.execute('UPDATE attributes SET value = ? WHERE attributeId = ?',
["multi,text", attr.attributeId]);
continue;
}
const tokens = [];
if (obj.isPromoted) {
tokens.push('promoted');
}
if (obj.multiplicityType === 'singlevalue') {
tokens.push('single');
} else if (obj.multiplicityType === 'multivalue') {
tokens.push('multi');
}
if (obj.labelType) {
tokens.push(obj.labelType);
}
if (obj.numberPrecision) {
tokens.push('precision='+obj.numberPrecision);
}
const newValue = tokens.join(',');
sql.execute('UPDATE attributes SET value = ? WHERE attributeId = ?', [newValue, attr.attributeId]);
}
for (const attr of sql.getRows("SELECT * FROM attributes WHERE name LIKE 'relation:%'")) {
let obj;
try {
obj = JSON.parse(attr.value);
}
catch (e) {
console.log(`Parsing attribute definition "${attr.value}" of ${attr.attributeId} failed with error "${e.message}", setting to default value.`);
sql.execute('UPDATE attributes SET value = ? WHERE attributeId = ?',
["multi", attr.attributeId]);
continue;
}
const tokens = [];
if (obj.isPromoted) {
tokens.push('promoted');
}
if (obj.multiplicityType === 'singlevalue') {
tokens.push('single');
} else if (obj.multiplicityType === 'multivalue') {
tokens.push('multi');
}
if (obj.inverseRelation) {
tokens.push('inverse=' + obj.inverseRelation);
}
const newValue = tokens.join(',');
sql.execute('UPDATE attributes SET value = ? WHERE attributeId = ?', [newValue, attr.attributeId]);
}
};
+1
View File
@@ -0,0 +1 @@
DROP TABLE source_ids;
@@ -0,0 +1 @@
ALTER TABLE sync RENAME TO entity_changes;
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS "mig_entity_changes" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`sourceId` TEXT NOT NULL,
`isSynced` INTEGER default 0 not null,
`utcChangedDate` TEXT NOT NULL);
INSERT INTO mig_entity_changes (id, entityName, entityId, sourceId, isSynced, utcChangedDate)
SELECT id, entityName, entityId, sourceId, isSynced, utcSyncDate FROM entity_changes;
DROP TABLE entity_changes;
ALTER TABLE mig_entity_changes RENAME TO entity_changes;
CREATE UNIQUE INDEX `IDX_entityChanges_entityName_entityId` ON "entity_changes" (
`entityName`,
`entityId`
);
CREATE INDEX `IDX_entityChanges_utcChangedDate` ON "entity_changes" (
`utcChangedDate`
);
@@ -0,0 +1,55 @@
CREATE TABLE IF NOT EXISTS "notes_mig" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "note",
`isProtected` INT NOT NULL DEFAULT 0,
`type` TEXT NOT NULL DEFAULT 'text',
`mime` TEXT NOT NULL DEFAULT 'text/html',
`hash` TEXT DEFAULT "" NOT NULL,
`isDeleted` INT NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`isErased` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`));
INSERT INTO notes_mig (noteId, title, isProtected, type, mime, hash, isDeleted, deleteId, isErased, dateCreated, dateModified, utcDateCreated, utcDateModified)
SELECT noteId, title, isProtected, type, mime, hash, isDeleted, deleteId, isErased, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes;
DROP TABLE notes;
ALTER TABLE notes_mig RENAME TO notes;
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
CREATE INDEX `IDX_notes_type` ON `notes` (`type`);
CREATE INDEX `IDX_notes_dateCreated` ON `notes` (`dateCreated`);
CREATE INDEX `IDX_notes_dateModified` ON `notes` (`dateModified`);
CREATE INDEX `IDX_notes_utcDateModified` ON `notes` (`utcDateModified`);
CREATE INDEX `IDX_notes_utcDateCreated` ON `notes` (`utcDateCreated`);
CREATE TABLE IF NOT EXISTS "note_revisions_mig" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL,
`title` TEXT,
`isErased` INT NOT NULL DEFAULT 0,
`isProtected` INT NOT NULL DEFAULT 0,
`utcDateLastEdited` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
`dateLastEdited` TEXT NOT NULL,
`dateCreated` TEXT NOT NULL,
type TEXT DEFAULT '' NOT NULL,
mime TEXT DEFAULT '' NOT NULL,
hash TEXT DEFAULT '' NOT NULL);
INSERT INTO note_revisions_mig (noteRevisionId, noteId, title, isErased, isProtected, utcDateLastEdited, utcDateCreated, utcDateModified, dateLastEdited, dateCreated, type, mime, hash)
SELECT noteRevisionId, noteId, title, isErased, isProtected, utcDateLastEdited, utcDateCreated, utcDateModified, dateLastEdited, dateCreated, type, mime, hash FROM note_revisions;
DROP TABLE note_revisions;
ALTER TABLE note_revisions_mig RENAME TO note_revisions;
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (`noteId`);
CREATE INDEX `IDX_note_revisions_utcDateCreated` ON `note_revisions` (`utcDateCreated`);
CREATE INDEX `IDX_note_revisions_utcDateLastEdited` ON `note_revisions` (`utcDateLastEdited`);
CREATE INDEX `IDX_note_revisions_dateCreated` ON `note_revisions` (`dateCreated`);
CREATE INDEX `IDX_note_revisions_dateLastEdited` ON `note_revisions` (`dateLastEdited`);
@@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS "note_contents_mig" (
`noteId` TEXT NOT NULL,
`content` TEXT NULL DEFAULT NULL,
`hash` TEXT DEFAULT "" NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`)
);
INSERT INTO note_contents_mig (noteId, content, hash, dateModified, utcDateModified)
SELECT noteId,
content,
hash,
(SELECT dateModified FROM notes WHERE noteId = note_contents.noteId),
utcDateModified
FROM note_contents;
DROP TABLE note_contents;
ALTER TABLE note_contents_mig RENAME TO note_contents;
@@ -0,0 +1 @@
DELETE FROM options WHERE name = 'keyboardShortcutsActivateParentNote';
@@ -0,0 +1 @@
UPDATE attributes SET name = 'archived' WHERE name = 'hideInAutocomplete';
+18
View File
@@ -0,0 +1,18 @@
const repository = require('../../src/services/repository');
module.exports = () => {
for (const note of repository.getEntities("SELECT * FROM notes WHERE type = 'text' AND isProtected = 0")) {
try {
let content = note.getContent();
content = content
.replace(/<h1/ig, "<h2")
.replace(/<\/h1/ig, "</h2");
note.setContent(content);
}
catch (e) {
console.log(`Changing note content for note ${note.noteId} failed with: ${e.message} ${e.stack}`);
}
}
};
+60 -68
View File
@@ -1,8 +1,3 @@
CREATE TABLE IF NOT EXISTS "source_ids" (
`sourceId` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
PRIMARY KEY(`sourceId`)
);
CREATE TABLE IF NOT EXISTS "api_tokens"
(
apiTokenId TEXT PRIMARY KEY NOT NULL,
@@ -19,13 +14,6 @@ CREATE TABLE IF NOT EXISTS "options"
utcDateCreated TEXT not null,
utcDateModified TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "note_contents" (
`noteId` TEXT NOT NULL,
`content` TEXT NULL DEFAULT NULL,
`hash` TEXT DEFAULT "" NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`)
);
CREATE TABLE recent_notes
(
noteId TEXT not null primary key,
@@ -38,50 +26,6 @@ CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT N
`content` TEXT,
hash TEXT DEFAULT '' NOT NULL,
`utcDateModified` TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS "note_revisions" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL,
`title` TEXT,
`contentLength` INT NOT NULL,
`isErased` INT NOT NULL DEFAULT 0,
`isProtected` INT NOT NULL DEFAULT 0,
`utcDateLastEdited` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
`dateLastEdited` TEXT NOT NULL,
`dateCreated` TEXT NOT NULL,
type TEXT DEFAULT '' NOT NULL,
mime TEXT DEFAULT '' NOT NULL,
hash TEXT DEFAULT '' NOT NULL);
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (`noteId`);
CREATE INDEX `IDX_note_revisions_utcDateCreated` ON `note_revisions` (`utcDateCreated`);
CREATE INDEX `IDX_note_revisions_utcDateLastEdited` ON `note_revisions` (`utcDateLastEdited`);
CREATE INDEX `IDX_note_revisions_dateCreated` ON `note_revisions` (`dateCreated`);
CREATE INDEX `IDX_note_revisions_dateLastEdited` ON `note_revisions` (`dateLastEdited`);
CREATE INDEX IDX_source_ids_utcDateCreated
on source_ids (utcDateCreated);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "note",
`contentLength` INT NOT NULL,
`isProtected` INT NOT NULL DEFAULT 0,
`type` TEXT NOT NULL DEFAULT 'text',
`mime` TEXT NOT NULL DEFAULT 'text/html',
`hash` TEXT DEFAULT "" NOT NULL,
`isDeleted` INT NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`isErased` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`));
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
CREATE INDEX `IDX_notes_type` ON `notes` (`type`);
CREATE INDEX `IDX_notes_dateCreated` ON `notes` (`dateCreated`);
CREATE INDEX `IDX_notes_dateModified` ON `notes` (`dateModified`);
CREATE INDEX `IDX_notes_utcDateModified` ON `notes` (`utcDateModified`);
CREATE INDEX `IDX_notes_utcDateCreated` ON `notes` (`utcDateCreated`);
CREATE TABLE IF NOT EXISTS "branches" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
@@ -117,17 +61,65 @@ CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
on attributes (value);
CREATE TABLE IF NOT EXISTS "sync" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`sourceId` TEXT NOT NULL,
`isSynced` INTEGER default 0 not null,
`utcSyncDate` TEXT NOT NULL);
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
`entityName`,
`entityId`
CREATE TABLE IF NOT EXISTS "entity_changes" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`sourceId` TEXT NOT NULL,
`isSynced` INTEGER default 0 not null,
`utcChangedDate` TEXT NOT NULL);
CREATE UNIQUE INDEX `IDX_entityChanges_entityName_entityId` ON "entity_changes" (
`entityName`,
`entityId`
);
CREATE INDEX `IDX_sync_utcSyncDate` ON `sync` (
`utcSyncDate`
CREATE INDEX `IDX_entityChanges_utcChangedDate` ON "entity_changes" (
`utcChangedDate`
);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "note",
`isProtected` INT NOT NULL DEFAULT 0,
`type` TEXT NOT NULL DEFAULT 'text',
`mime` TEXT NOT NULL DEFAULT 'text/html',
`hash` TEXT DEFAULT "" NOT NULL,
`isDeleted` INT NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`isErased` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`));
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
CREATE INDEX `IDX_notes_type` ON `notes` (`type`);
CREATE INDEX `IDX_notes_dateCreated` ON `notes` (`dateCreated`);
CREATE INDEX `IDX_notes_dateModified` ON `notes` (`dateModified`);
CREATE INDEX `IDX_notes_utcDateModified` ON `notes` (`utcDateModified`);
CREATE INDEX `IDX_notes_utcDateCreated` ON `notes` (`utcDateCreated`);
CREATE TABLE IF NOT EXISTS "note_revisions" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL,
`title` TEXT,
`isErased` INT NOT NULL DEFAULT 0,
`isProtected` INT NOT NULL DEFAULT 0,
`utcDateLastEdited` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
`dateLastEdited` TEXT NOT NULL,
`dateCreated` TEXT NOT NULL,
type TEXT DEFAULT '' NOT NULL,
mime TEXT DEFAULT '' NOT NULL,
hash TEXT DEFAULT '' NOT NULL);
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (`noteId`);
CREATE INDEX `IDX_note_revisions_utcDateCreated` ON `note_revisions` (`utcDateCreated`);
CREATE INDEX `IDX_note_revisions_utcDateLastEdited` ON `note_revisions` (`utcDateLastEdited`);
CREATE INDEX `IDX_note_revisions_dateCreated` ON `note_revisions` (`dateCreated`);
CREATE INDEX `IDX_note_revisions_dateLastEdited` ON `note_revisions` (`dateLastEdited`);
CREATE TABLE IF NOT EXISTS "note_contents" (
`noteId` TEXT NOT NULL,
`content` TEXT NULL DEFAULT NULL,
`hash` TEXT DEFAULT "" NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`)
);
-2
View File
@@ -24,8 +24,6 @@ app.on('window-all-closed', () => {
app.on('ready', async () => {
app.setAppUserModelId('com.github.zadam.trilium');
await sqlInit.dbConnection;
// if db is not initialized -> setup process
// if db is initialized, then we need to wait until the migration process is finished
if (await sqlInit.isDbInitialized()) {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+157 -129
View File
@@ -1,68 +1,59 @@
/*
* !!!!!!! This stylesheet is heavily modified compared to the original for similarity with in-editor look !!!!!!!
* This is used for printing and tar HTML export
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
* CKEditor 5 (v17.0.0) content styles.
* Generated on Fri, 13 Mar 2020 13:27:10 GMT.
.ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
display: none;
}
/*
* CKEditor 5 (v22.0.0) content styles.
* Generated on Thu, 27 Aug 2020 12:13:06 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
*/
:root {
--ck-highlight-marker-blue: #72cdfd;
--ck-highlight-marker-green: #63f963;
--ck-highlight-marker-pink: #fc7999;
--ck-highlight-marker-yellow: #fdfd77;
--ck-highlight-pen-green: #118800;
--ck-highlight-pen-red: #e91313;
--ck-color-mention-background: hsla(341, 100%, 30%, 0.1);
--ck-color-mention-text: hsl(341, 100%, 30%);
--ck-highlight-marker-blue: hsl(201, 97%, 72%);
--ck-highlight-marker-green: hsl(120, 93%, 68%);
--ck-highlight-marker-pink: hsl(345, 96%, 73%);
--ck-highlight-marker-yellow: hsl(60, 97%, 73%);
--ck-highlight-pen-green: hsl(112, 100%, 27%);
--ck-highlight-pen-red: hsl(0, 85%, 49%);
--ck-image-style-spacing: 1.5em;
--ck-todo-list-checkmark-size: 16px;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized img {
width: 100%;
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized > figcaption {
display: block;
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
margin: 1em auto;
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image > img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 50px;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side,
.ck-content .image-style-align-left,
.ck-content .image-style-align-center,
.ck-content .image-style-align-right {
max-width: 50%;
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
max-width: 50%;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left {
@@ -79,6 +70,115 @@
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
margin: 1em auto;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 50px;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized img {
width: 100%;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized > figcaption {
display: block;
}
/* ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: hsl(0, 0%, 20%);
background-color: hsl(0, 0%, 97%);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny {
font-size: .7em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-small {
font-size: .85em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-big {
font-size: 1.4em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-huge {
font-size: 1.8em;
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 1em auto;
display: table;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border: 1px solid hsl(0, 0%, 75%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="rtl"] .table th {
text-align: right;
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="ltr"] .table th {
text-align: left;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
position: relative;
@@ -108,28 +208,13 @@
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: #fff;
background: hsl(0, 0%, 100%);
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
@@ -137,31 +222,6 @@
display: block;
min-width: 15em;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 1em auto;
display: table;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border-color: hsl(0, 0%, 75%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list {
list-style: none;
@@ -229,54 +289,17 @@
.ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: hsl(0, 0%, 20%);
background-color: hsl(0, 0%, 97%);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
border-width: 1px 0 0;
border-style: solid;
border-color: hsl(0, 0%, 37%);
margin: 0;
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* ckeditor5-code-block/theme/codeblock.css */
.ck-content pre {
padding: 1em;
color: #353535;
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
@@ -293,6 +316,11 @@
padding: 0;
border-radius: 0;
}
/* ckeditor5-mention/theme/mention.css */
.ck-content .mention {
background: var(--ck-color-mention-background);
color: var(--ck-color-mention-text);
}
@media print {
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -134,7 +134,7 @@
{name: "SPARQL", mime: "application/sparql-query", mode: "sparql", ext: ["rq", "sparql"], alias: ["sparul"]},
{name: "Spreadsheet", mime: "text/x-spreadsheet", mode: "spreadsheet", alias: ["excel", "formula"]},
{name: "SQL", mime: "text/x-sql", mode: "sql", ext: ["sql"]},
{name: "SQLite", mime: "text/x-sqlite", mode: "sql"},
{name: "SQLite", mimes: ["text/x-sqlite", "text/x-sqlite;schema=trilium"], mode: "sql"},
{name: "Squirrel", mime: "text/x-squirrel", mode: "clike", ext: ["nut"]},
{name: "Stylus", mime: "text/x-styl", mode: "stylus", ext: ["styl"]},
{name: "Swift", mime: "text/x-swift", mode: "swift", ext: ["swift"]},
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -9,12 +9,12 @@
*
* This section is automatically generated from the `skin-common.less` template.
*
* Copyright (c) 2008-2019, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
******************************************************************************/
/*------------------------------------------------------------------------------
* Helpers
File diff suppressed because one or more lines are too long
-35
View File
@@ -1,35 +0,0 @@
/**!
* jquery.fancytree.hotkeys.js
*
* Integrate the 'jQuery.Hotkeys' plugin as Fancytree extension:
* https://github.com/jeresig/jquery.hotkeys/
*
* Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*/
(function($, document) {
"use strict";
var initHotkeys = function(tree, data) {
$.each(data, function(event, keys) {
$.each(keys, function(key, handler) {
$(tree.$container).on(event, null, key, function(evt) {
var node = tree.getActiveNode();
return handler(node, evt);
// return false from the handler will stop default handling.
});
});
});
};
$.ui.fancytree.registerExtension({
name: "hotkeys",
version: "@VERSION",
hotkeys: { },
treeInit: function(ctx) {
this._superApply(arguments);
initHotkeys(this, ctx.options.hotkeys);
}
});
}(jQuery, document));
+756 -482
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2779 -5218
View File
File diff suppressed because it is too large Load Diff
+37 -36
View File
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.42.4",
"version": "0.44.3-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -13,78 +13,79 @@
"url": "https://github.com/zadam/trilium.git"
},
"scripts": {
"start-server": "TRILIUM_ENV=dev node ./src/www",
"start-electron": "TRILIUM_ENV=dev electron .",
"start-server": "cross-env TRILIUM_ENV=dev node ./src/www",
"start-electron": "cross-env TRILIUM_ENV=dev electron .",
"build-backend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
"build-frontend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/collapsible_widget.js",
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
"webpack": "npx webpack -c webpack-desktop.config.js && npx webpack -c webpack-mobile.config.js && npx webpack -c webpack-setup.config.js"
"webpack": "npx webpack -c webpack-desktop.config.js && npx webpack -c webpack-mobile.config.js && npx webpack -c webpack-setup.config.js",
"test": "jasmine",
"test-es6": "node -r esm spec-es6/attribute_parser.spec.js ",
"test-all": "npm run test && npm run test-es6"
},
"dependencies": {
"async-mutex": "0.2.2",
"axios": "0.19.2",
"async-mutex": "0.2.4",
"axios": "0.20.0",
"better-sqlite3": "7.1.1",
"body-parser": "1.19.0",
"cls-hooked": "4.2.2",
"commonmark": "0.29.1",
"commonmark": "0.29.2",
"cookie-parser": "1.4.5",
"csurf": "1.11.0",
"dayjs": "1.8.26",
"dayjs": "1.8.36",
"debug": "4.1.1",
"ejs": "3.1.2",
"electron-debug": "3.0.1",
"electron-dl": "3.0.0",
"ejs": "3.1.5",
"electron-debug": "3.1.0",
"electron-dl": "3.0.2",
"electron-find": "1.0.6",
"electron-window-state": "5.0.3",
"express": "4.17.1",
"express-session": "1.17.1",
"file-type": "14.3.0",
"fs-extra": "9.0.0",
"helmet": "3.22.0",
"file-type": "15.0.1",
"fs-extra": "9.0.1",
"helmet": "4.1.1",
"html": "1.0.0",
"html2plaintext": "2.1.2",
"http-proxy-agent": "4.0.1",
"https-proxy-agent": "5.0.0",
"image-type": "4.1.0",
"imagemin": "7.0.1",
"imagemin-giflossy": "5.1.10",
"imagemin-mozjpeg": "8.0.0",
"imagemin-pngquant": "8.0.0",
"ini": "1.3.5",
"is-svg": "4.2.1",
"jimp": "0.10.3",
"jimp": "0.16.1",
"mime-types": "2.1.27",
"multer": "1.4.2",
"node-abi": "2.16.0",
"open": "7.0.3",
"node-abi": "2.19.1",
"open": "7.2.1",
"portscanner": "2.2.0",
"rand-token": "1.0.1",
"rcedit": "2.1.1",
"request": "^2.88.2",
"rimraf": "3.0.2",
"sanitize-filename": "1.6.3",
"sanitize-html": "1.27.4",
"sax": "1.2.4",
"semver": "7.3.2",
"serve-favicon": "2.5.0",
"session-file-store": "1.4.0",
"simple-node-logger": "18.12.24",
"sqlite": "4.0.7",
"sqlite3": "4.1.1",
"string-similarity": "4.0.1",
"tar-stream": "2.1.2",
"striptags": "3.1.1",
"turndown": "6.0.0",
"turndown-plugin-gfm": "1.0.2",
"unescape": "1.0.1",
"ws": "7.2.5",
"yauzl": "^2.10.0",
"yazl": "^2.5.1"
"ws": "7.3.1",
"yauzl": "2.10.0",
"yazl": "2.5.1"
},
"devDependencies": {
"electron": "9.0.0",
"electron-builder": "22.6.0",
"electron-packager": "14.2.1",
"electron-rebuild": "1.10.1",
"jsdoc": "3.6.4",
"cross-env": "7.0.2",
"electron": "9.3.1",
"electron-builder": "22.8.1",
"electron-packager": "15.1.0",
"electron-rebuild": "2.0.3",
"esm": "3.2.25",
"jasmine": "3.6.1",
"jsdoc": "3.6.5",
"lorem-ipsum": "2.0.3",
"webpack": "5.0.0-beta.16",
"rcedit": "2.2.0",
"webpack": "5.0.0-beta.32",
"webpack-cli": "4.0.0-beta.8"
},
"optionalDependencies": {
+111
View File
@@ -0,0 +1,111 @@
import attributeParser from '../src/public/app/services/attribute_parser.js';
import {describe, it, expect, execute} from './mini_test.js';
describe("Lexing", () => {
it("simple label", () => {
expect(attributeParser.lex("#label").map(t => t.text))
.toEqual(["#label"]);
});
it("simple label with trailing spaces", () => {
expect(attributeParser.lex(" #label ").map(t => t.text))
.toEqual(["#label"]);
});
it("inherited label", () => {
expect(attributeParser.lex("#label(inheritable)").map(t => t.text))
.toEqual(["#label", "(", "inheritable", ")"]);
expect(attributeParser.lex("#label ( inheritable ) ").map(t => t.text))
.toEqual(["#label", "(", "inheritable", ")"]);
});
it("label with value", () => {
expect(attributeParser.lex("#label=Hallo").map(t => t.text))
.toEqual(["#label", "=", "Hallo"]);
});
it("label with value", () => {
const tokens = attributeParser.lex("#label=Hallo");
expect(tokens[0].startIndex).toEqual(0);
expect(tokens[0].endIndex).toEqual(5);
});
it("relation with value", () => {
expect(attributeParser.lex('~relation=#root/RclIpMauTOKS/NFi2gL4xtPxM').map(t => t.text))
.toEqual(["~relation", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"]);
});
it("use quotes to define value", () => {
expect(attributeParser.lex("#'label a'='hello\"` world'").map(t => t.text))
.toEqual(["#label a", "=", 'hello"` world']);
expect(attributeParser.lex('#"label a" = "hello\'` world"').map(t => t.text))
.toEqual(["#label a", "=", "hello'` world"]);
expect(attributeParser.lex('#`label a` = `hello\'" world`').map(t => t.text))
.toEqual(["#label a", "=", "hello'\" world"]);
});
});
describe("Parser", () => {
it("simple label", () => {
const attrs = attributeParser.parse(["#token"].map(t => ({text: t})));
expect(attrs.length).toEqual(1);
expect(attrs[0].type).toEqual('label');
expect(attrs[0].name).toEqual('token');
expect(attrs[0].isInheritable).toBeFalsy();
expect(attrs[0].value).toBeFalsy();
});
it("inherited label", () => {
const attrs = attributeParser.parse(["#token", "(", "inheritable", ")"].map(t => ({text: t})));
expect(attrs.length).toEqual(1);
expect(attrs[0].type).toEqual('label');
expect(attrs[0].name).toEqual('token');
expect(attrs[0].isInheritable).toBeTruthy();
expect(attrs[0].value).toBeFalsy();
});
it("label with value", () => {
const attrs = attributeParser.parse(["#token", "=", "val"].map(t => ({text: t})));
expect(attrs.length).toEqual(1);
expect(attrs[0].type).toEqual('label');
expect(attrs[0].name).toEqual('token');
expect(attrs[0].value).toEqual("val");
});
it("relation", () => {
let attrs = attributeParser.parse(["~token", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"].map(t => ({text: t})));
expect(attrs.length).toEqual(1);
expect(attrs[0].type).toEqual('relation');
expect(attrs[0].name).toEqual("token");
expect(attrs[0].value).toEqual('NFi2gL4xtPxM');
attrs = attributeParser.parse(["~token", "=", "#NFi2gL4xtPxM"].map(t => ({text: t})));
expect(attrs.length).toEqual(1);
expect(attrs[0].type).toEqual('relation');
expect(attrs[0].name).toEqual("token");
expect(attrs[0].value).toEqual('NFi2gL4xtPxM');
});
});
describe("error cases", () => {
it("error cases", () => {
expect(() => attributeParser.lexAndParse('~token'))
.toThrow('Relation "~token" in "~token" should point to a note.');
expect(() => attributeParser.lexAndParse("#a&b/s"))
.toThrow(`Attribute name "a&b/s" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`);
expect(() => attributeParser.lexAndParse("#"))
.toThrow(`Attribute name is empty, please fill the name.`);
});
});
execute();
+81
View File
@@ -0,0 +1,81 @@
export function describe(name, cb) {
console.log(`Running ${name}`);
cb();
}
export async function it(name, cb) {
console.log(` Running ${name}`);
cb();
}
let errorCount = 0;
export function expect(val) {
return {
toEqual: comparedVal => {
const jsonVal = JSON.stringify(val);
const comparedJsonVal = JSON.stringify(comparedVal);
if (jsonVal !== comparedJsonVal) {
console.trace("toEqual check failed.");
console.error(`expected: ${comparedJsonVal}`);
console.error(`got: ${jsonVal}`);
errorCount++;
}
},
toBeTruthy: () => {
if (!val) {
console.trace("toBeTruthy failed.");
console.error(`expected: truthy value`);
console.error(`got: ${val}`);
errorCount++;
}
},
toBeFalsy: () => {
if (!!val) {
console.trace("toBeFalsy failed.");
console.error(`expected: null, false, undefined, 0 or empty string`);
console.error(`got: ${val}`);
errorCount++;
}
},
toThrow: errorMessage => {
try {
val();
}
catch (e) {
if (e.message !== errorMessage) {
console.trace("toThrow caught exception, but messages differ");
console.error(`expected: ${errorMessage}`);
console.error(`got: ${e.message}`);
console.error(`${e.stack}`);
errorCount++;
}
return;
}
console.trace("toThrow did not catch any exception.");
console.error(`expected: ${errorMessage}`);
console.error(`got: [none]`);
errorCount++;
}
}
}
export function execute() {
console.log("");
if (errorCount) {
console.log(`!!!${errorCount} tests failed!!!`);
}
else {
console.log("All tests passed!");
}
}
+150
View File
@@ -0,0 +1,150 @@
const lex = require('../../src/services/search/services/lex.js');
describe("Lexer fulltext", () => {
it("simple lexing", () => {
expect(lex("hello world").fulltextTokens.map(t => t.token))
.toEqual(["hello", "world"]);
});
it("use quotes to keep words together", () => {
expect(lex("'hello world' my friend").fulltextTokens.map(t => t.token))
.toEqual(["hello world", "my", "friend"]);
expect(lex('"hello world" my friend').fulltextTokens.map(t => t.token))
.toEqual(["hello world", "my", "friend"]);
expect(lex('`hello world` my friend').fulltextTokens.map(t => t.token))
.toEqual(["hello world", "my", "friend"]);
});
it("you can use different quotes and other special characters inside quotes", () => {
expect(lex("'i can use \" or ` or #~=*' without problem").fulltextTokens.map(t => t.token))
.toEqual(["i can use \" or ` or #~=*", "without", "problem"]);
});
it("I can use backslash to escape quotes", () => {
expect(lex("hello \\\"world\\\"").fulltextTokens.map(t => t.token))
.toEqual(["hello", '"world"']);
expect(lex("hello \\\'world\\\'").fulltextTokens.map(t => t.token))
.toEqual(["hello", "'world'"]);
expect(lex("hello \\\`world\\\`").fulltextTokens.map(t => t.token))
.toEqual(["hello", '`world`']);
expect(lex('"hello \\\"world\\\"').fulltextTokens.map(t => t.token))
.toEqual(['hello "world"']);
expect(lex("'hello \\\'world\\\''").fulltextTokens.map(t => t.token))
.toEqual(["hello 'world'"]);
expect(lex("`hello \\\`world\\\``").fulltextTokens.map(t => t.token))
.toEqual(["hello `world`"]);
expect(lex("\\#token").fulltextTokens.map(t => t.token))
.toEqual(["#token"]);
});
it("quote inside a word does not have a special meaning", () => {
const lexResult = lex("d'Artagnan is dead #hero = d'Artagnan");
expect(lexResult.fulltextTokens.map(t => t.token))
.toEqual(["d'artagnan", "is", "dead"]);
expect(lexResult.expressionTokens.map(t => t.token))
.toEqual(['#hero', '=', "d'artagnan"]);
});
it("if quote is not ended then it's just one long token", () => {
expect(lex("'unfinished quote").fulltextTokens.map(t => t.token))
.toEqual(["unfinished quote"]);
});
it("parenthesis and symbols in fulltext section are just normal characters", () => {
expect(lex("what's u=p <b(r*t)h>").fulltextTokens.map(t => t.token))
.toEqual(["what's", "u=p", "<b(r*t)h>"]);
});
it("operator characters in expressions are separate tokens", () => {
expect(lex("# abc+=-def**-+d").expressionTokens.map(t => t.token))
.toEqual(["#", "abc", "+=-", "def", "**-+", "d"]);
});
it("escaping special characters", () => {
expect(lex("hello \\#\\~\\'").fulltextTokens.map(t => t.token))
.toEqual(["hello", "#~'"]);
});
});
describe("Lexer expression", () => {
it("simple attribute existence", () => {
expect(lex("#label ~relation").expressionTokens.map(t => t.token))
.toEqual(["#label", "~relation"]);
});
it("simple label operators", () => {
expect(lex("#label*=*text").expressionTokens.map(t => t.token))
.toEqual(["#label", "*=*", "text"]);
});
it("simple label operator with in quotes and without", () => {
expect(lex("#label*=*'text'").expressionTokens)
.toEqual([
{token: "#label", inQuotes: false, startIndex: 0, endIndex: 5},
{token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8},
{token: "text", inQuotes: true, startIndex: 10, endIndex: 13}
]);
expect(lex("#label*=*text").expressionTokens)
.toEqual([
{token: "#label", inQuotes: false, startIndex: 0, endIndex: 5},
{token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8},
{token: "text", inQuotes: false, startIndex: 9, endIndex: 12}
]);
});
it("note. prefix also separates fulltext from expression", () => {
expect(lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map(t => t.token))
.toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]);
});
it("note. prefix in quotes will note start expression", () => {
expect(lex(`hello fulltext "note.txt"`).expressionTokens.map(t => t.token))
.toEqual([]);
expect(lex(`hello fulltext "note.txt"`).fulltextTokens.map(t => t.token))
.toEqual(["hello", "fulltext", "note.txt"]);
});
it("complex expressions with and, or and parenthesis", () => {
expect(lex(`# (#label=text OR #second=text) AND ~relation`).expressionTokens.map(t => t.token))
.toEqual(["#", "(", "#label", "=", "text", "or", "#second", "=", "text", ")", "and", "~relation"]);
});
it("dot separated properties", () => {
expect(lex(`# ~author.title = 'Hugh Howey' AND note.'book title' = 'Silo'`).expressionTokens.map(t => t.token))
.toEqual(["#", "~author", ".", "title", "=", "hugh howey", "and", "note", ".", "book title", "=", "silo"]);
});
it("negation of label and relation", () => {
expect(lex(`#!capital ~!neighbor`).expressionTokens.map(t => t.token))
.toEqual(["#!capital", "~!neighbor"]);
});
it("negation of sub-expression", () => {
expect(lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map(t => t.token))
.toEqual(["#", "not", "(", "#capital", ")", "and", "note", ".", "noteid", "!=", "root"]);
});
});
describe("Lexer invalid queries and edge cases", () => {
it("concatenated attributes", () => {
expect(lex("#label~relation").expressionTokens.map(t => t.token))
.toEqual(["#label", "~relation"]);
});
it("trailing escape \\", () => {
expect(lex('abc \\').fulltextTokens.map(t => t.token))
.toEqual(["abc", "\\"]);
});
});
+70
View File
@@ -0,0 +1,70 @@
const Note = require('../../src/services/note_cache/entities/note.js');
const Branch = require('../../src/services/note_cache/entities/branch.js');
const Attribute = require('../../src/services/note_cache/entities/attribute.js');
const noteCache = require('../../src/services/note_cache/note_cache.js');
const randtoken = require('rand-token').generator({source: 'crypto'});
/** @return {Note} */
function findNoteByTitle(searchResults, title) {
return searchResults
.map(sr => noteCache.notes[sr.noteId])
.find(note => note.title === title);
}
class NoteBuilder {
constructor(note) {
this.note = note;
}
label(name, value = '', isInheritable = false) {
new Attribute(noteCache, {
attributeId: id(),
noteId: this.note.noteId,
type: 'label',
isInheritable,
name,
value
});
return this;
}
relation(name, targetNote) {
new Attribute(noteCache, {
attributeId: id(),
noteId: this.note.noteId,
type: 'relation',
name,
value: targetNote.noteId
});
return this;
}
child(childNoteBuilder, prefix = "") {
new Branch(noteCache, {
branchId: id(),
noteId: childNoteBuilder.note.noteId,
parentNoteId: this.note.noteId,
prefix
});
return this;
}
}
function id() {
return randtoken.generate(10);
}
function note(title, type = 'text', mime = 'text/html') {
const note = new Note(noteCache, {noteId: id(), title, type, mime});
return new NoteBuilder(note);
}
module.exports = {
NoteBuilder,
findNoteByTitle,
note
};
+24
View File
@@ -0,0 +1,24 @@
const handleParens = require('../../src/services/search/services/handle_parens.js');
describe("Parens handler", () => {
it("handles parens", () => {
const input = ["(", "hello", ")", "and", "(", "(", "pick", "one", ")", "and", "another", ")"]
.map(token => ({token}));
expect(handleParens(input))
.toEqual([
[
{token: "hello"}
],
{token: "and"},
[
[
{token: "pick"},
{token: "one"}
],
{token: "and"},
{token: "another"}
]
]);
});
});
+239
View File
@@ -0,0 +1,239 @@
const SearchContext = require("../../src/services/search/search_context.js");
const parse = require('../../src/services/search/services/parse.js');
function tokens(toks, cur = 0) {
return toks.map(arg => {
if (Array.isArray(arg)) {
return tokens(arg, cur);
}
else {
cur += arg.length;
return {
token: arg,
inQuotes: false,
startIndex: cur - arg.length,
endIndex: cur - 1
};
}
});
}
describe("Parser", () => {
it("fulltext parser without content", () => {
const rootExp = parse({
fulltextTokens: tokens(["hello", "hi"]),
expressionTokens: [],
searchContext: new SearchContext({includeNoteContent: false})
});
expect(rootExp.constructor.name).toEqual("AndExp");
expect(rootExp.subExpressions[0].constructor.name).toEqual("NoteCacheFlatTextExp");
expect(rootExp.subExpressions[0].tokens).toEqual(["hello", "hi"]);
});
it("fulltext parser with content", () => {
const rootExp = parse({
fulltextTokens: tokens(["hello", "hi"]),
expressionTokens: [],
searchContext: new SearchContext({includeNoteContent: true})
});
expect(rootExp.constructor.name).toEqual("AndExp");
expect(rootExp.subExpressions[0].constructor.name).toEqual("OrExp");
expect(rootExp.subExpressions[1].constructor.name).toEqual("PropertyComparisonExp");
const subs = rootExp.subExpressions[0].subExpressions;
expect(subs[0].constructor.name).toEqual("NoteCacheFlatTextExp");
expect(subs[0].tokens).toEqual(["hello", "hi"]);
expect(subs[1].constructor.name).toEqual("NoteContentProtectedFulltextExp");
expect(subs[1].tokens).toEqual(["hello", "hi"]);
expect(subs[2].constructor.name).toEqual("NoteContentUnprotectedFulltextExp");
expect(subs[2].tokens).toEqual(["hello", "hi"]);
});
it("simple label comparison", () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["#mylabel", "=", "text"]),
searchContext: new SearchContext()
});
expect(rootExp.constructor.name).toEqual("LabelComparisonExp");
expect(rootExp.attributeType).toEqual("label");
expect(rootExp.attributeName).toEqual("mylabel");
expect(rootExp.comparator).toBeTruthy();
});
it("simple attribute negation", () => {
let rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["#!mylabel"]),
searchContext: new SearchContext()
});
expect(rootExp.constructor.name).toEqual("NotExp");
expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp");
expect(rootExp.subExpression.attributeType).toEqual("label");
expect(rootExp.subExpression.attributeName).toEqual("mylabel");
rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["~!myrelation"]),
searchContext: new SearchContext()
});
expect(rootExp.constructor.name).toEqual("NotExp");
expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp");
expect(rootExp.subExpression.attributeType).toEqual("relation");
expect(rootExp.subExpression.attributeName).toEqual("myrelation");
});
it("simple label AND", () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]),
searchContext: new SearchContext(true)
});
expect(rootExp.constructor.name).toEqual("AndExp");
const [firstSub, secondSub] = rootExp.subExpressions;
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSub.attributeName).toEqual("first");
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
expect(secondSub.attributeName).toEqual("second");
});
it("simple label AND without explicit AND", () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]),
searchContext: new SearchContext()
});
expect(rootExp.constructor.name).toEqual("AndExp");
const [firstSub, secondSub] = rootExp.subExpressions;
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSub.attributeName).toEqual("first");
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
expect(secondSub.attributeName).toEqual("second");
});
it("simple label OR", () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]),
searchContext: new SearchContext()
});
expect(rootExp.constructor.name).toEqual("OrExp");
const [firstSub, secondSub] = rootExp.subExpressions;
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSub.attributeName).toEqual("first");
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
expect(secondSub.attributeName).toEqual("second");
});
it("fulltext and simple label", () => {
const rootExp = parse({
fulltextTokens: tokens(["hello"]),
expressionTokens: tokens(["#mylabel", "=", "text"]),
searchContext: new SearchContext()
});
expect(rootExp.constructor.name).toEqual("AndExp");
const [firstSub, secondSub] = rootExp.subExpressions;
expect(firstSub.constructor.name).toEqual("AndExp");
expect(firstSub.subExpressions[0].constructor.name).toEqual("NoteCacheFlatTextExp");
expect(firstSub.subExpressions[0].tokens).toEqual(["hello"]);
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
expect(secondSub.attributeName).toEqual("mylabel");
});
it("label sub-expression", () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]),
searchContext: new SearchContext()
});
expect(rootExp.constructor.name).toEqual("OrExp");
const [firstSub, secondSub] = rootExp.subExpressions;
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSub.attributeName).toEqual("first");
expect(secondSub.constructor.name).toEqual("AndExp");
const [firstSubSub, secondSubSub] = secondSub.subExpressions;
expect(firstSubSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSubSub.attributeName).toEqual("second");
expect(secondSubSub.constructor.name).toEqual("LabelComparisonExp");
expect(secondSubSub.attributeName).toEqual("third");
});
});
describe("Invalid expressions", () => {
it("incomplete comparison", () => {
const searchContext = new SearchContext();
parse({
fulltextTokens: [],
expressionTokens: tokens(["#first", "="]),
searchContext
});
expect(searchContext.error).toEqual('Misplaced or incomplete expression "="')
});
it("comparison between labels is impossible", () => {
let searchContext = new SearchContext();
searchContext.originalQuery = "#first = #second";
parse({
fulltextTokens: [],
expressionTokens: tokens(["#first", "=", "#second"]),
searchContext
});
expect(searchContext.error).toEqual(`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`);
searchContext = new SearchContext();
searchContext.originalQuery = "#first = note.relations.second";
parse({
fulltextTokens: [],
expressionTokens: tokens(["#first", "=", "note", ".", "relations", "second"]),
searchContext
});
expect(searchContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`);
const rootExp = parse({
fulltextTokens: [],
expressionTokens: [
{ token: "#first", inQuotes: false },
{ token: "=", inQuotes: false },
{ token: "#second", inQuotes: true },
],
searchContext: new SearchContext()
});
expect(rootExp.constructor.name).toEqual("LabelComparisonExp");
expect(rootExp.attributeType).toEqual("label");
expect(rootExp.attributeName).toEqual("first");
expect(rootExp.comparator).toBeTruthy();
});
});
+634
View File
@@ -0,0 +1,634 @@
const searchService = require('../../src/services/search/services/search.js');
const Note = require('../../src/services/note_cache/entities/note.js');
const Branch = require('../../src/services/note_cache/entities/branch.js');
const SearchContext = require('../../src/services/search/search_context.js');
const dateUtils = require('../../src/services/date_utils.js');
const noteCache = require('../../src/services/note_cache/note_cache.js');
const {NoteBuilder, findNoteByTitle, note} = require('./note_cache_mocking.js');
describe("Search", () => {
let rootNote;
beforeEach(() => {
noteCache.reset();
rootNote = new NoteBuilder(new Note(noteCache, {noteId: 'root', title: 'root'}));
new Branch(noteCache, {branchId: 'root', noteId: 'root', parentNoteId: 'none'});
});
it("simple path match", () => {
rootNote
.child(note("Europe")
.child(note("Austria"))
);
const searchContext = new SearchContext();
const searchResults = searchService.findNotesWithQuery('europe austria', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("normal search looks also at attributes", () => {
const austria = note("Austria");
const vienna = note("Vienna");
rootNote
.child(austria
.relation('capital', vienna))
.child(vienna
.label('inhabitants', '1888776'));
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('capital', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('inhabitants', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Vienna")).toBeTruthy();
});
it("normal search looks also at type and mime", () => {
rootNote
.child(note("Effective Java", 'book', ''))
.child(note("Hello World.java", 'code', 'text/x-java'));
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('book', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Effective Java")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('text', searchContext); // should match mime
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Hello World.java")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('java', searchContext);
expect(searchResults.length).toEqual(2);
});
it("only end leafs are results", () => {
rootNote
.child(note("Europe")
.child(note("Austria"))
);
const searchContext = new SearchContext();
const searchResults = searchService.findNotesWithQuery('europe', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy();
});
it("only end leafs are results", () => {
rootNote
.child(note("Europe")
.child(note("Austria")
.label('capital', 'Vienna'))
);
const searchContext = new SearchContext();
const searchResults = searchService.findNotesWithQuery('Vienna', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("label comparison with short syntax", () => {
rootNote
.child(note("Europe")
.child(note("Austria")
.label('capital', 'Vienna'))
.child(note("Czech Republic")
.label('capital', 'Prague'))
);
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('#capital=Vienna', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("label comparison with full syntax", () => {
rootNote
.child(note("Europe")
.child(note("Austria")
.label('capital', 'Vienna'))
.child(note("Czech Republic")
.label('capital', 'Prague'))
);
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('# note.labels.capital=Prague', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("numeric label comparison", () => {
rootNote
.child(note("Europe")
.label('country', '', true)
.child(note("Austria")
.label('population', '8859000'))
.child(note("Czech Republic")
.label('population', '10650000'))
);
const searchContext = new SearchContext();
const searchResults = searchService.findNotesWithQuery('#country #population >= 10000000', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("numeric label comparison fallback to string comparison", () => {
// dates should not be coerced into numbers which would then give wrong numbers
rootNote
.child(note("Europe")
.label('country', '', true)
.child(note("Austria")
.label('established', '1955-07-27'))
.child(note("Czech Republic")
.label('established', '1993-01-01'))
.child(note("Hungary")
.label('established', '1920-06-04'))
);
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('#established <= "1955-01-01"', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Hungary")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('#established > "1955-01-01"', searchContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("smart date comparisons", () => {
// dates should not be coerced into numbers which would then give wrong numbers
rootNote
.child(note("My note")
.label('year', new Date().getFullYear().toString())
.label('month', dateUtils.localNowDate().substr(0, 7))
.label('date', dateUtils.localNowDate())
.label('dateTime', dateUtils.localNowDateTime())
);
const searchContext = new SearchContext();
function test(query, expectedResultCount) {
const searchResults = searchService.findNotesWithQuery(query, searchContext);
expect(searchResults.length).toEqual(expectedResultCount);
if (expectedResultCount === 1) {
expect(findNoteByTitle(searchResults, "My note")).toBeTruthy();
}
}
test("#year = YEAR", 1);
test("#year = 'YEAR'", 0);
test("#year >= YEAR", 1);
test("#year <= YEAR", 1);
test("#year < YEAR+1", 1);
test("#year < YEAR + 1", 1);
test("#year < year + 1", 1);
test("#year > YEAR+1", 0);
test("#month = MONTH", 1);
test("#month = month", 1);
test("#month = 'MONTH'", 0);
test("#date = TODAY", 1);
test("#date = today", 1);
test("#date = 'today'", 0);
test("#date > TODAY", 0);
test("#date > TODAY-1", 1);
test("#date > TODAY - 1", 1);
test("#date < TODAY+1", 1);
test("#date < TODAY + 1", 1);
test("#date < 'TODAY + 1'", 1);
test("#dateTime <= NOW+10", 1);
test("#dateTime <= NOW + 10", 1);
test("#dateTime < NOW-10", 0);
test("#dateTime >= NOW-10", 1);
test("#dateTime < NOW-10", 0);
});
it("logical or", () => {
rootNote
.child(note("Europe")
.label('country', '', true)
.child(note("Austria")
.label('languageFamily', 'germanic'))
.child(note("Czech Republic")
.label('languageFamily', 'slavic'))
.child(note("Hungary")
.label('languageFamily', 'finnougric'))
);
const searchContext = new SearchContext();
const searchResults = searchService.findNotesWithQuery('#languageFamily = slavic OR #languageFamily = germanic', searchContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("fuzzy attribute search", () => {
rootNote
.child(note("Europe")
.label('country', '', true)
.child(note("Austria")
.label('languageFamily', 'germanic'))
.child(note("Czech Republic")
.label('languageFamily', 'slavic')));
let searchContext = new SearchContext({fuzzyAttributeSearch: false});
let searchResults = searchService.findNotesWithQuery('#language', searchContext);
expect(searchResults.length).toEqual(0);
searchResults = searchService.findNotesWithQuery('#languageFamily=ger', searchContext);
expect(searchResults.length).toEqual(0);
searchContext = new SearchContext({fuzzyAttributeSearch: true});
searchResults = searchService.findNotesWithQuery('#language', searchContext);
expect(searchResults.length).toEqual(2);
searchResults = searchService.findNotesWithQuery('#languageFamily=ger', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("filter by note property", () => {
rootNote
.child(note("Europe")
.child(note("Austria"))
.child(note("Czech Republic")));
const searchContext = new SearchContext();
const searchResults = searchService.findNotesWithQuery('# note.title =* czech', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("filter by note's parent", () => {
rootNote
.child(note("Europe")
.child(note("Austria"))
.child(note("Czech Republic")
.child(note("Prague")))
)
.child(note("Asia")
.child(note('Taiwan')));
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe', searchContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('# note.parents.title = Asia', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Taiwan")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('# note.parents.parents.title = Europe', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Prague")).toBeTruthy();
});
it("filter by note's ancestor", () => {
rootNote
.child(note("Europe")
.child(note("Austria"))
.child(note("Czech Republic")
.child(note("Prague").label('city')))
)
.child(note("Asia")
.child(note('Taiwan')
.child(note('Taipei').label('city')))
);
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('#city AND note.ancestors.title = Europe', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Prague")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('#city AND note.ancestors.title = Asia', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Taipei")).toBeTruthy();
});
it("filter by note's child", () => {
rootNote
.child(note("Europe")
.child(note("Austria")
.child(note("Vienna")))
.child(note("Czech Republic")
.child(note("Prague"))))
.child(note("Oceania")
.child(note('Australia')));
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('# note.children.title =* Aust', searchContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Oceania")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('# note.children.title =* Aust AND note.children.title *= republic', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('# note.children.children.title = Prague', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy();
});
it("filter by relation's note properties using short syntax", () => {
const austria = note("Austria");
const portugal = note("Portugal");
rootNote
.child(note("Europe")
.child(austria)
.child(note("Czech Republic")
.relation('neighbor', austria.note))
.child(portugal)
.child(note("Spain")
.relation('neighbor', portugal.note))
);
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('# ~neighbor.title = Austria', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('# ~neighbor.title = Portugal', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Spain")).toBeTruthy();
});
it("filter by relation's note properties using long syntax", () => {
const austria = note("Austria");
const portugal = note("Portugal");
rootNote
.child(note("Europe")
.child(austria)
.child(note("Czech Republic")
.relation('neighbor', austria.note))
.child(portugal)
.child(note("Spain")
.relation('neighbor', portugal.note))
);
const searchContext = new SearchContext();
const searchResults = searchService.findNotesWithQuery('# note.relations.neighbor.title = Austria', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("filter by multiple level relation", () => {
const austria = note("Austria");
const slovakia = note("Slovakia");
const italy = note("Italy");
const ukraine = note("Ukraine");
rootNote
.child(note("Europe")
.child(austria
.relation('neighbor', italy.note)
.relation('neighbor', slovakia.note))
.child(note("Czech Republic")
.relation('neighbor', austria.note)
.relation('neighbor', slovakia.note))
.child(slovakia
.relation('neighbor', ukraine.note))
.child(ukraine)
);
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('# note.relations.neighbor.relations.neighbor.title = Italy', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('# note.relations.neighbor.relations.neighbor.title = Ukraine', searchContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("test note properties", () => {
const austria = note("Austria");
austria.relation('myself', austria.note);
austria.label('capital', 'Vienna');
austria.label('population', '8859000');
rootNote
.child(note("Asia"))
.child(note("Europe")
.child(austria
.child(note("Vienna"))
.child(note("Sebastian Kurz"))
)
)
.child(note("Mozart")
.child(austria));
austria.note.isProtected = false;
austria.note.dateCreated = '2020-05-14 12:11:42.001+0200';
austria.note.dateModified = '2020-05-14 13:11:42.001+0200';
austria.note.utcDateCreated = '2020-05-14 10:11:42.001Z';
austria.note.utcDateModified = '2020-05-14 11:11:42.001Z';
austria.note.contentLength = 1001;
const searchContext = new SearchContext();
function test(propertyName, value, expectedResultCount) {
const searchResults = searchService.findNotesWithQuery(`# note.${propertyName} = ${value}`, searchContext);
expect(searchResults.length).toEqual(expectedResultCount);
}
test("type", "text", 6);
test("type", "code", 0);
test("mime", "text/html", 6);
test("mime", "application/json", 0);
test("isProtected", "false", 7);
test("isProtected", "true", 0);
test("dateCreated", "'2020-05-14 12:11:42.001+0200'", 1);
test("dateCreated", "wrong", 0);
test("dateModified", "'2020-05-14 13:11:42.001+0200'", 1);
test("dateModified", "wrong", 0);
test("utcDateCreated", "'2020-05-14 10:11:42.001Z'", 1);
test("utcDateCreated", "wrong", 0);
test("utcDateModified", "'2020-05-14 11:11:42.001Z'", 1);
test("utcDateModified", "wrong", 0);
test("parentCount", "2", 1);
test("parentCount", "3", 0);
test("childrenCount", "2", 1);
test("childrenCount", "10", 0);
test("attributeCount", "3", 1);
test("attributeCount", "4", 0);
test("labelCount", "2", 1);
test("labelCount", "3", 0);
test("relationCount", "1", 1);
test("relationCount", "2", 0);
});
it("test order by", () => {
const italy = note("Italy").label("capital", "Rome");
const slovakia = note("Slovakia").label("capital", "Bratislava");
const austria = note("Austria").label("capital", "Vienna");
const ukraine = note("Ukraine").label("capital", "Kiev");
rootNote
.child(note("Europe")
.child(ukraine)
.child(slovakia)
.child(austria)
.child(italy));
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy note.title', searchContext);
expect(searchResults.length).toEqual(4);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Austria");
expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy");
expect(noteCache.notes[searchResults[2].noteId].title).toEqual("Slovakia");
expect(noteCache.notes[searchResults[3].noteId].title).toEqual("Ukraine");
searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy note.labels.capital', searchContext);
expect(searchResults.length).toEqual(4);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Slovakia");
expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Ukraine");
expect(noteCache.notes[searchResults[2].noteId].title).toEqual("Italy");
expect(noteCache.notes[searchResults[3].noteId].title).toEqual("Austria");
searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy note.labels.capital DESC', searchContext);
expect(searchResults.length).toEqual(4);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Austria");
expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy");
expect(noteCache.notes[searchResults[2].noteId].title).toEqual("Ukraine");
expect(noteCache.notes[searchResults[3].noteId].title).toEqual("Slovakia");
searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy note.labels.capital DESC limit 2', searchContext);
expect(searchResults.length).toEqual(2);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Austria");
expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy");
searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 0', searchContext);
expect(searchResults.length).toEqual(0);
searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', searchContext);
expect(searchResults.length).toEqual(4);
});
it("test not(...)", () => {
const italy = note("Italy").label("capital", "Rome");
const slovakia = note("Slovakia").label("capital", "Bratislava");
rootNote
.child(note("Europe")
.child(slovakia)
.child(italy));
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('# not(#capital) and note.noteId != root', searchContext);
expect(searchResults.length).toEqual(1);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Europe");
searchResults = searchService.findNotesWithQuery('#!capital and note.noteId != root', searchContext);
expect(searchResults.length).toEqual(1);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Europe");
});
it("test note.text *=* something", () => {
const italy = note("Italy").label("capital", "Rome");
const slovakia = note("Slovakia").label("capital", "Bratislava");
rootNote
.child(note("Europe")
.child(slovakia)
.child(italy));
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('# note.text *=* rati and note.noteId != root', searchContext);
expect(searchResults.length).toEqual(1);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Slovakia");
});
it("test that fulltext does not match archived notes", () => {
const italy = note("Italy").label("capital", "Rome");
const slovakia = note("Slovakia").label("capital", "Bratislava");
rootNote
.child(note("Reddit").label('archived', '', true)
.child(note('Post X'))
.child(note('Post Y')))
.child(note ('Reddit is bad'));
const searchContext = new SearchContext();
let searchResults = searchService.findNotesWithQuery('reddit', searchContext);
expect(searchResults.length).toEqual(1);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Reddit is bad");
});
// FIXME: test what happens when we order without any filter criteria
// it("comparison between labels", () => {
// rootNote
// .child(note("Europe")
// .child(note("Austria")
// .label('capital', 'Vienna')
// .label('largestCity', 'Vienna'))
// .child(note("Canada")
// .label('capital', 'Ottawa')
// .label('largestCity', 'Toronto'))
// .child(note("Czech Republic")
// .label('capital', 'Prague')
// .label('largestCity', 'Prague'))
// );
//
// const searchContext = new SearchContext();
//
// const searchResults = searchService.findNotesWithQuery('#capital = #largestCity', searchContext);
// expect(searchResults.length).toEqual(2);
// expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
// expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
// })
});
+86
View File
@@ -0,0 +1,86 @@
const {note} = require('./note_cache_mocking.js');
const ValueExtractor = require('../../src/services/search/value_extractor.js');
const noteCache = require('../../src/services/note_cache/note_cache.js');
describe("Value extractor", () => {
beforeEach(() => {
noteCache.reset();
});
it("simple title extraction", async () => {
const europe = note("Europe").note;
const valueExtractor = new ValueExtractor(["note", "title"]);
expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(europe)).toEqual("Europe");
});
it("label extraction", async () => {
const austria = note("Austria")
.label("Capital", "Vienna")
.note;
let valueExtractor = new ValueExtractor(["note", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria)).toEqual("vienna");
valueExtractor = new ValueExtractor(["#capital"]);
expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria)).toEqual("vienna");
});
it("parent/child property extraction", async () => {
const vienna = note("Vienna");
const europe = note("Europe")
.child(note("Austria")
.child(vienna));
let valueExtractor = new ValueExtractor(["note", "children", "children", "title"]);
expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(europe.note)).toEqual("Vienna");
valueExtractor = new ValueExtractor(["note", "parents", "parents", "title"]);
expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(vienna.note)).toEqual("Europe");
});
it("extract through relation", async () => {
const czechRepublic = note("Czech Republic").label("capital", "Prague");
const slovakia = note("Slovakia").label("capital", "Bratislava");
const austria = note("Austria")
.relation('neighbor', czechRepublic.note)
.relation('neighbor', slovakia.note);
let valueExtractor = new ValueExtractor(["note", "relations", "neighbor", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria.note)).toEqual("prague");
valueExtractor = new ValueExtractor(["~neighbor", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria.note)).toEqual("prague");
});
});
describe("Invalid value extractor property path", () => {
it('each path must start with "note" (or label/relation)',
() => expect(new ValueExtractor(["neighbor"]).validate()).toBeTruthy());
it("extra path element after terminal label",
() => expect(new ValueExtractor(["~neighbor", "labels", "capital", "noteId"]).validate()).toBeTruthy());
it("extra path element after terminal title",
() => expect(new ValueExtractor(["note", "title", "isProtected"]).validate()).toBeTruthy());
it("relation name and note property is missing",
() => expect(new ValueExtractor(["note", "relations"]).validate()).toBeTruthy());
it("relation is specified but target note property is not specified",
() => expect(new ValueExtractor(["note", "relations", "myrel"]).validate()).toBeTruthy());
});
+12
View File
@@ -0,0 +1,12 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.js",
"**/*[sS]pec.mjs"
],
"helpers": [
"helpers/**/*.js"
],
"stopSpecOnExpectationFailure": false,
"random": true
}
+22 -5
View File
@@ -1,7 +1,24 @@
const anonymizationService = require('./services/anonymization');
const backupService = require('./services/backup');
const sqlInit = require('./services/sql_init');
require('./entities/entity_constructor');
anonymizationService.anonymize().then(filePath => {
console.log("Anonymized file has been saved to:", filePath);
sqlInit.dbReady.then(async () => {
try {
console.log("Starting anonymization...");
process.exit(0);
});
const resp = await backupService.anonymize();
if (resp.success) {
console.log("Anonymized file has been saved to: " + resp.anonymizedFilePath);
process.exit(0);
} else {
console.log("Anonymization failed.");
}
}
catch (e) {
console.error(e.message, e.stack);
}
process.exit(1);
});
+14 -26
View File
@@ -7,11 +7,11 @@ const bodyParser = require('body-parser');
const helmet = require('helmet');
const session = require('express-session');
const FileStore = require('session-file-store')(session);
const os = require('os');
const sessionSecret = require('./services/session_secret');
const cls = require('./services/cls');
require('./entities/entity_constructor');
const dataDir = require('./services/data_dir');
require('./services/handlers');
require('./services/hoisted_note_loader');
require('./services/note_cache/note_cache_loader');
const app = express();
@@ -20,25 +20,15 @@ app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(helmet({
hidePoweredBy: false // deactivated because electron 4.0 crashes on this right after startup
hidePoweredBy: false, // deactivated because electron 4.0 crashes on this right after startup
contentSecurityPolicy: {
directives: {
defaultSrc: ["*", "'unsafe-inline'", "'unsafe-eval'"],
imgSrc: ["'self' data:"]
}
}
}));
app.use((req, res, next) => {
log.request(req);
next();
});
app.use((req, res, next) => {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
cls.init(() => {
cls.namespace.set("Hi");
next();
});
});
app.use(bodyParser.json({limit: '500mb'}));
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
@@ -56,7 +46,7 @@ const sessionParser = session({
},
store: new FileStore({
ttl: 30 * 24 * 3600,
path: os.tmpdir() + '/trilium-sessions'
path: dataDir.TRILIUM_DATA_DIR + '/sessions'
})
});
app.use(sessionParser);
@@ -89,12 +79,10 @@ app.use((req, res, next) => {
// error handler
app.use((err, req, res, next) => {
if (err && err.message && (
err.message.includes("Invalid package")
|| (err.message.includes("Router not found for request") && err.message.includes("node_modules"))
|| (err.message.includes("Router not found for request") && err.message.includes(".js.map"))
(err.message.includes("Router not found for request") && err.message.includes(".js.map"))
|| (err.message.includes("Router not found for request") && err.message.includes(".css.map"))
)) {
// electron 6 outputs a lot of such errors which do not seem important
// ignore
}
else {
log.info(err);
@@ -120,4 +108,4 @@ require('./services/scheduler');
module.exports = {
app,
sessionParser
};
};
+32 -30
View File
@@ -1,20 +1,21 @@
"use strict";
const Entity = require('./entity');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
const sql = require('../services/sql');
const promotedAttributeDefinitionParser = require("../services/promoted_attribute_definition_parser");
/**
* Attribute is key value pair owned by a note.
*
* @property {string} attributeId
* @property {string} noteId
* @property {string} type
* @property {string} name
* @property {string} attributeId - immutable
* @property {string} noteId - immutable
* @property {string} type - immutable
* @property {string} name - immutable
* @property {string} value
* @property {int} position
* @property {boolean} isInheritable
* @property {boolean} isInheritable - immutable
* @property {boolean} isDeleted
* @property {string|null} deleteId - ID identifying delete transaction
* @property {string} utcDateCreated
@@ -31,27 +32,19 @@ class Attribute extends Entity {
super(row);
this.isInheritable = !!this.isInheritable;
if (this.isDefinition()) {
try {
this.value = JSON.parse(this.value);
}
catch (e) {
}
}
}
/**
* @returns {Promise<Note|null>}
* @returns {Note|null}
*/
async getNote() {
return await repository.getNote(this.noteId);
getNote() {
return this.repository.getNote(this.noteId);
}
/**
* @returns {Promise<Note|null>}
* @returns {Note|null}
*/
async getTargetNote() {
getTargetNote() {
if (this.type !== 'relation') {
throw new Error(`Attribute ${this.attributeId} is not relation`);
}
@@ -60,17 +53,31 @@ class Attribute extends Entity {
return null;
}
return await repository.getNote(this.value);
return this.repository.getNote(this.value);
}
/**
* @return {boolean}
*/
isDefinition() {
return this.type === 'label-definition' || this.type === 'relation-definition';
return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:'));
}
async beforeSaving() {
getDefinition() {
return promotedAttributeDefinitionParser.parse(this.value);
}
getDefinedName() {
if (this.type === 'label' && this.name.startsWith('label:')) {
return this.name.substr(6);
} else if (this.type === 'label' && this.name.startsWith('relation:')) {
return this.name.substr(9);
} else {
return this.name;
}
}
beforeSaving() {
if (!this.value) {
if (this.type === 'relation') {
throw new Error(`Cannot save relation ${this.name} since it does not target any note.`);
@@ -81,7 +88,7 @@ class Attribute extends Entity {
}
if (this.position === undefined) {
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [this.noteId]);
this.position = 1 + sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [this.noteId]);
}
if (!this.isInheritable) {
@@ -103,19 +110,14 @@ class Attribute extends Entity {
}
}
// cannot be static!
updatePojo(pojo) {
delete pojo.__note;
}
createClone(type, name, value) {
createClone(type, name, value, isInheritable) {
return new Attribute({
noteId: this.noteId,
type: type,
name: name,
value: value,
position: this.position,
isInheritable: this.isInheritable,
isInheritable: isInheritable,
isDeleted: false,
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified
+12 -13
View File
@@ -2,16 +2,15 @@
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
const repository = require('../services/repository');
const sql = require('../services/sql');
/**
* Branch represents note's placement in the tree - it's essentially pair of noteId and parentNoteId.
* Each note can have multiple (at least one) branches, meaning it can be placed into multiple places in the tree.
*
* @property {string} branchId - primary key
* @property {string} noteId
* @property {string} parentNoteId
* @property {string} branchId - primary key, immutable
* @property {string} noteId - immutable
* @property {string} parentNoteId - immutable
* @property {int} notePosition
* @property {string} prefix
* @property {boolean} isExpanded
@@ -28,19 +27,19 @@ class Branch extends Entity {
// 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", "isDeleted", "deleteId", "prefix"]; }
/** @returns {Promise<Note|null>} */
async getNote() {
return await repository.getNote(this.noteId);
/** @returns {Note|null} */
getNote() {
return this.repository.getNote(this.noteId);
}
/** @returns {Promise<Note|null>} */
async getParentNote() {
return await repository.getNote(this.parentNoteId);
/** @returns {Note|null} */
getParentNote() {
return this.repository.getNote(this.parentNoteId);
}
async beforeSaving() {
beforeSaving() {
if (this.notePosition === undefined) {
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 10;
}
@@ -77,4 +76,4 @@ class Branch extends Entity {
}
}
module.exports = Branch;
module.exports = Branch;
+13 -11
View File
@@ -1,6 +1,7 @@
"use strict";
const utils = require('../services/utils');
let repo = null;
class Entity {
/**
@@ -25,14 +26,7 @@ class Entity {
const origHash = this.hash;
this.hash = this.generateHash();
if (this.forcedChange) {
this.isChanged = true;
delete this.forcedChange;
}
else {
this.isChanged = origHash !== this.hash;
}
this.isChanged = origHash !== this.hash;
}
generateIdIfNecessary() {
@@ -51,11 +45,19 @@ class Entity {
return utils.hash(contentToHash).substr(0, 10);
}
async save() {
await require('../services/repository').updateEntity(this);
get repository() {
if (!repo) {
repo = require('../services/repository');
}
return repo;
}
save() {
this.repository.updateEntity(this);
return this;
}
}
module.exports = Entity;
module.exports = Entity;
+1 -3
View File
@@ -1,3 +1,4 @@
const repository = require('../services/repository');
const Note = require('../entities/note');
const NoteRevision = require('../entities/note_revision');
const Branch = require('../entities/branch');
@@ -5,7 +6,6 @@ const Attribute = require('../entities/attribute');
const RecentNote = require('../entities/recent_note');
const ApiToken = require('../entities/api_token');
const Option = require('../entities/option');
const repository = require('../services/repository');
const cls = require('../services/cls');
const ENTITY_NAME_TO_ENTITY = {
@@ -71,5 +71,3 @@ module.exports = {
createEntityFromRow,
getEntityFromEntityName
};
repository.setEntityConstructor(module.exports);
+186 -198
View File
@@ -3,16 +3,13 @@
const Entity = require('./entity');
const Attribute = require('./attribute');
const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
const sql = require('../services/sql');
const utils = require('../services/utils');
const dateUtils = require('../services/date_utils');
const syncTableService = require('../services/sync_table');
const entityChangesService = require('../services/entity_changes.js');
const LABEL = 'label';
const LABEL_DEFINITION = 'label-definition';
const RELATION = 'relation';
const RELATION_DEFINITION = 'relation-definition';
/**
* This represents a Note which is a central object in the Trilium Notes project.
@@ -21,7 +18,6 @@ const RELATION_DEFINITION = 'relation-definition';
* @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 {int} contentLength - length of content
* @property {boolean} isProtected - true if note is protected
* @property {boolean} isDeleted - true if note is deleted
* @property {string|null} deleteId - ID identifying delete transaction
@@ -67,14 +63,14 @@ class Note extends Entity {
* part of Note entity with it's own sync. Reasons behind this hybrid design has been:
*
* - content can be quite large and it's not necessary to load it / fill memory for any note access even if we don't need a content, especially for bulk operations like search
* - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and sync rows)
* - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records)
* - but to the user note content and title changes are one and the same - single dateModified (so all changes must go through Note and content is not a separate entity)
*/
/** @returns {Promise<*>} */
async getContent(silentNotFoundError = false) {
/** @returns {*} */
getContent(silentNotFoundError = false) {
if (this.content === undefined) {
const res = await sql.getRow(`SELECT content, hash FROM note_contents WHERE noteId = ?`, [this.noteId]);
const res = sql.getRow(`SELECT content, hash FROM note_contents WHERE noteId = ?`, [this.noteId]);
if (!res) {
if (silentNotFoundError) {
@@ -107,9 +103,20 @@ class Note extends Entity {
}
}
/** @returns {Promise<*>} */
async getJsonContent() {
const content = await this.getContent();
/** @returns {{contentLength, dateModified, utcDateModified}} */
getContentMetadata() {
return sql.getRow(`
SELECT
LENGTH(content) AS contentLength,
dateModified,
utcDateModified
FROM note_contents
WHERE noteId = ?`, [this.noteId]);
}
/** @returns {*} */
getJsonContent() {
const content = this.getContent();
if (!content || !content.trim()) {
return null;
@@ -118,24 +125,24 @@ class Note extends Entity {
return JSON.parse(content);
}
/** @returns {Promise} */
async setContent(content) {
setContent(content) {
if (content === null || content === undefined) {
throw new Error(`Cannot set null content to note ${this.noteId}`);
}
content = Buffer.isBuffer(content) ? content : Buffer.from(content);
// force updating note itself so that dateModified is represented correctly even for the content
this.forcedChange = true;
this.contentLength = content.byteLength;
await this.save();
if (this.isStringNote()) {
content = content.toString();
}
else {
content = Buffer.isBuffer(content) ? content : Buffer.from(content);
}
this.content = content;
const pojo = {
noteId: this.noteId,
content: content,
dateModified: dateUtils.localNowDateTime(),
utcDateModified: dateUtils.utcNowDateTime(),
hash: utils.hash(this.noteId + "|" + content.toString())
};
@@ -149,14 +156,13 @@ class Note extends Entity {
}
}
await sql.upsert("note_contents", "noteId", pojo);
sql.upsert("note_contents", "noteId", pojo);
await syncTableService.addNoteContentSync(this.noteId);
entityChangesService.addNoteContentEntityChange(this.noteId);
}
/** @returns {Promise} */
async setJsonContent(content) {
await this.setContent(JSON.stringify(content, null, '\t'));
setJsonContent(content) {
this.setContent(JSON.stringify(content, null, '\t'));
}
/** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
@@ -204,8 +210,8 @@ class Note extends Entity {
return null;
}
async loadOwnedAttributesToCache() {
this.__ownedAttributeCache = await repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
loadOwnedAttributesToCache() {
this.__ownedAttributeCache = this.repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
return this.__ownedAttributeCache;
}
@@ -215,11 +221,11 @@ class Note extends Entity {
*
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise<Attribute[]>} note's "owned" attributes - excluding inherited ones
* @returns {Attribute[]} note's "owned" attributes - excluding inherited ones
*/
async getOwnedAttributes(type, name) {
getOwnedAttributes(type, name) {
if (!this.__ownedAttributeCache) {
await this.loadOwnedAttributesToCache();
this.loadOwnedAttributesToCache();
}
if (type && name) {
@@ -237,31 +243,31 @@ class Note extends Entity {
}
/**
* @returns {Promise<Attribute>} attribute belonging to this specific note (excludes inherited attributes)
* @returns {Attribute} attribute belonging to this specific note (excludes inherited attributes)
*
* This method can be significantly faster than the getAttribute()
*/
async getOwnedAttribute(type, name) {
const attrs = await this.getOwnedAttributes(type, name);
getOwnedAttribute(type, name) {
const attrs = this.getOwnedAttributes(type, name);
return attrs.length > 0 ? attrs[0] : null;
}
/**
* @returns {Promise<Attribute[]>} relations targetting this specific note
* @returns {Attribute[]} relations targetting this specific note
*/
async getTargetRelations() {
return await repository.getEntities("SELECT * FROM attributes WHERE type = 'relation' AND isDeleted = 0 AND value = ?", [this.noteId]);
getTargetRelations() {
return this.repository.getEntities("SELECT * FROM attributes WHERE type = 'relation' AND isDeleted = 0 AND value = ?", [this.noteId]);
}
/**
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise<Attribute[]>} all note's attributes, including inherited ones
* @returns {Attribute[]} all note's attributes, including inherited ones
*/
async getAttributes(type, name) {
getAttributes(type, name) {
if (!this.__attributeCache) {
await this.loadAttributesToCache();
this.loadAttributesToCache();
}
if (type && name) {
@@ -280,67 +286,51 @@ class Note extends Entity {
/**
* @param {string} [name] - label name to filter
* @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones
* @returns {Attribute[]} all note's labels (attributes with type label), including inherited ones
*/
async getLabels(name) {
return await this.getAttributes(LABEL, name);
getLabels(name) {
return this.getAttributes(LABEL, name);
}
/**
* @param {string} [name] - label name to filter
* @returns {Promise<Attribute[]>} all note's labels (attributes with type label), excluding inherited ones
* @returns {Attribute[]} all note's labels (attributes with type label), excluding inherited ones
*/
async getOwnedLabels(name) {
return await this.getOwnedAttributes(LABEL, name);
}
/**
* @param {string} [name] - label name to filter
* @returns {Promise<Attribute[]>} all note's label definitions, including inherited ones
*/
async getLabelDefinitions(name) {
return await this.getAttributes(LABEL_DEFINITION, name);
getOwnedLabels(name) {
return this.getOwnedAttributes(LABEL, name);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones
* @returns {Attribute[]} all note's relations (attributes with type relation), including inherited ones
*/
async getRelations(name) {
return await this.getAttributes(RELATION, name);
getRelations(name) {
return this.getAttributes(RELATION, name);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), excluding inherited ones
* @returns {Attribute[]} all note's relations (attributes with type relation), excluding inherited ones
*/
async getOwnedRelations(name) {
return await this.getOwnedAttributes(RELATION, name);
getOwnedRelations(name) {
return this.getOwnedAttributes(RELATION, name);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise<Note[]>}
* @returns {Note[]}
*/
async getRelationTargets(name) {
const relations = await this.getRelations(name);
getRelationTargets(name) {
const relations = this.getRelations(name);
const targets = [];
for (const relation of relations) {
targets.push(await relation.getTargetNote());
targets.push(relation.getTargetNote());
}
return targets;
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise<Attribute[]>} all note's relation definitions including inherited ones
*/
async getRelationDefinitions(name) {
return await this.getAttributes(RELATION_DEFINITION, name);
}
/**
* Clear note's attributes cache to force fresh reload for next attribute request.
* Cache is note instance scoped.
@@ -350,9 +340,8 @@ class Note extends Entity {
this.__ownedAttributeCache = null;
}
/** @returns {Promise<void>} */
async loadAttributesToCache() {
const attributes = await repository.getEntities(`
loadAttributesToCache() {
const attributes = this.repository.getEntities(`
WITH RECURSIVE
tree(noteId, level) AS (
SELECT ?, 0
@@ -391,15 +380,15 @@ class Note extends Entity {
return firstDefinitionIndex === index;
}
else {
const definitionAttr = attributes.find(el => el.type === attr.type + '-definition' && el.name === attr.name);
const definitionAttr = attributes.find(el => el.type === 'label' && el.name === attr.type + ':' + attr.name);
if (!definitionAttr) {
return true;
}
const definition = definitionAttr.value;
const definition = definitionAttr.getDefinition();
if (definition.multiplicityType === 'multivalue') {
if (definition.multiplicity === 'multi') {
return true;
}
else {
@@ -417,28 +406,28 @@ class Note extends Entity {
/**
* @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)
* @returns {boolean} true if note has an attribute with given type and name (including inherited)
*/
async hasAttribute(type, name) {
return !!await this.getAttribute(type, name);
hasAttribute(type, name) {
return !!this.getAttribute(type, name);
}
/**
* @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 (excluding inherited)
* @returns {boolean} true if note has an attribute with given type and name (excluding inherited)
*/
async hasOwnedAttribute(type, name) {
return !!await this.getOwnedAttribute(type, name);
hasOwnedAttribute(type, name) {
return !!this.getOwnedAttribute(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.
* @returns {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();
getAttribute(type, name) {
const attributes = this.getAttributes();
return attributes.find(attr => attr.type === type && attr.name === name);
}
@@ -446,10 +435,10 @@ class Note extends Entity {
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<string|null>} attribute value of given type and name or null if no such attribute exists.
* @returns {string|null} 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);
getAttributeValue(type, name) {
const attr = this.getAttribute(type, name);
return attr ? attr.value : null;
}
@@ -457,10 +446,10 @@ class Note extends Entity {
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<string|null>} attribute value of given type and name or null if no such attribute exists.
* @returns {string|null} attribute value of given type and name or null if no such attribute exists.
*/
async getOwnedAttributeValue(type, name) {
const attr = await this.getOwnedAttribute(type, name);
getOwnedAttributeValue(type, name) {
const attr = this.getOwnedAttribute(type, name);
return attr ? attr.value : null;
}
@@ -472,14 +461,13 @@ class Note extends Entity {
* @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) {
toggleAttribute(type, enabled, name, value) {
if (enabled) {
await this.setAttribute(type, name, value);
this.setAttribute(type, name, value);
}
else {
await this.removeAttribute(type, name, value);
this.removeAttribute(type, name, value);
}
}
@@ -489,16 +477,15 @@ class Note extends Entity {
* @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.loadOwnedAttributesToCache();
setAttribute(type, name, value) {
const attributes = this.loadOwnedAttributesToCache();
let attr = attributes.find(attr => attr.type === type && attr.name === name);
if (attr) {
if (attr.value !== value) {
attr.value = value;
await attr.save();
attr.save();
this.invalidateAttributeCache();
}
@@ -511,7 +498,7 @@ class Note extends Entity {
value: value !== undefined ? value : ""
});
await attr.save();
attr.save();
this.invalidateAttributeCache();
}
@@ -523,15 +510,14 @@ class Note extends Entity {
* @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.loadOwnedAttributesToCache();
removeAttribute(type, name, value) {
const attributes = this.loadOwnedAttributesToCache();
for (const attribute of attributes) {
if (attribute.type === type && attribute.name === name && (value === undefined || value === attribute.value)) {
attribute.isDeleted = true;
await attribute.save();
attribute.save();
this.invalidateAttributeCache();
}
@@ -539,121 +525,123 @@ class Note extends Entity {
}
/**
* @return {Promise<Attribute>}
* @return {Attribute}
*/
async addAttribute(type, name, value = "") {
addAttribute(type, name, value = "", isInheritable = false, position = 1000) {
const attr = new Attribute({
noteId: this.noteId,
type: type,
name: name,
value: value
value: value,
isInheritable: isInheritable,
position: position
});
await attr.save();
attr.save();
this.invalidateAttributeCache();
return attr;
}
async addLabel(name, value = "") {
return await this.addAttribute(LABEL, name, value);
addLabel(name, value = "", isInheritable = false) {
return this.addAttribute(LABEL, name, value, isInheritable);
}
async addRelation(name, targetNoteId) {
return await this.addAttribute(RELATION, name, targetNoteId);
addRelation(name, targetNoteId, isInheritable = false) {
return this.addAttribute(RELATION, name, targetNoteId, isInheritable);
}
/**
* @param {string} name - label name
* @returns {Promise<boolean>} true if label exists (including inherited)
* @returns {boolean} true if label exists (including inherited)
*/
async hasLabel(name) { return await this.hasAttribute(LABEL, name); }
hasLabel(name) { return this.hasAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise<boolean>} true if label exists (excluding inherited)
* @returns {boolean} true if label exists (excluding inherited)
*/
async hasOwnedLabel(name) { return await this.hasOwnedAttribute(LABEL, name); }
hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<boolean>} true if relation exists (including inherited)
* @returns {boolean} true if relation exists (including inherited)
*/
async hasRelation(name) { return await this.hasAttribute(RELATION, name); }
hasRelation(name) { return this.hasAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise<boolean>} true if relation exists (excluding inherited)
* @returns {boolean} true if relation exists (excluding inherited)
*/
async hasOwnedRelation(name) { return await this.hasOwnedAttribute(RELATION, name); }
hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise<Attribute|null>} label if it exists, null otherwise
* @returns {Attribute|null} label if it exists, null otherwise
*/
async getLabel(name) { return await this.getAttribute(LABEL, name); }
getLabel(name) { return this.getAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise<Attribute|null>} label if it exists, null otherwise
* @returns {Attribute|null} label if it exists, null otherwise
*/
async getOwnedLabel(name) { return await this.getOwnedAttribute(LABEL, name); }
getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<Attribute|null>} relation if it exists, null otherwise
* @returns {Attribute|null} relation if it exists, null otherwise
*/
async getRelation(name) { return await this.getAttribute(RELATION, name); }
getRelation(name) { return this.getAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise<Attribute|null>} relation if it exists, null otherwise
* @returns {Attribute|null} relation if it exists, null otherwise
*/
async getOwnedRelation(name) { return await this.getOwnedAttribute(RELATION, name); }
getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise<string|null>} label value if label exists, null otherwise
* @returns {string|null} label value if label exists, null otherwise
*/
async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); }
getLabelValue(name) { return this.getAttributeValue(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise<string|null>} label value if label exists, null otherwise
* @returns {string|null} label value if label exists, null otherwise
*/
async getOwnedLabelValue(name) { return await this.getOwnedAttributeValue(LABEL, name); }
getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<string|null>} relation value if relation exists, null otherwise
* @returns {string|null} relation value if relation exists, null otherwise
*/
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
getRelationValue(name) { return this.getAttributeValue(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise<string|null>} relation value if relation exists, null otherwise
* @returns {string|null} relation value if relation exists, null otherwise
*/
async getOwnedRelationValue(name) { return await this.getOwnedAttributeValue(RELATION, name); }
getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); }
/**
* @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
* @returns {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);
getRelationTarget(name) {
const relation = this.getRelation(name);
return relation ? await repository.getNote(relation.value) : null;
return relation ? this.repository.getNote(relation.value) : null;
}
/**
* @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
* @returns {Note|null} target note of the relation or null (if target is empty or note was not found)
*/
async getOwnedRelationTarget(name) {
const relation = await this.getOwnedRelation(name);
getOwnedRelationTarget(name) {
const relation = this.getOwnedRelation(name);
return relation ? await repository.getNote(relation.value) : null;
return relation ? this.repository.getNote(relation.value) : null;
}
/**
@@ -662,9 +650,8 @@ class Note extends Entity {
* @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); }
toggleLabel(enabled, name, value) { return this.toggleAttribute(LABEL, enabled, name, value); }
/**
* Based on enabled, relation is either set or removed.
@@ -672,51 +659,46 @@ class Note extends Entity {
* @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); }
toggleRelation(enabled, name, value) { return this.toggleAttribute(RELATION, enabled, name, value); }
/**
* Update's given label's value or creates it if it doesn't exist
*
* @param {string} name - label name
* @param {string} [value] - label value
* @returns {Promise<void>}
*/
async setLabel(name, value) { return await this.setAttribute(LABEL, name, value); }
setLabel(name, value) { return this.setAttribute(LABEL, name, value); }
/**
* Update's given relation's value or creates it if it doesn't exist
*
* @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); }
setRelation(name, value) { return 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); }
removeLabel(name, value) { return 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); }
removeRelation(name, value) { return this.removeAttribute(RELATION, name, value); }
/**
* @return {Promise<string[]>} return list of all descendant noteIds of this note. Returning just noteIds because number of notes can be huge. Includes also this note's noteId
* @return {string[]} return list of all descendant noteIds of this note. Returning just noteIds because number of notes can be huge. Includes also this note's noteId
*/
async getDescendantNoteIds() {
return await sql.getColumn(`
getDescendantNoteIds() {
return sql.getColumn(`
WITH RECURSIVE
tree(noteId) AS (
SELECT ?
@@ -736,9 +718,9 @@ class Note extends Entity {
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @param {string} [value] - attribute value
* @returns {Promise<Note[]>}
* @returns {Note[]}
*/
async getDescendantNotesWithAttribute(type, name, value) {
getDescendantNotesWithAttribute(type, name, value) {
const params = [this.noteId, name];
let valueCondition = "";
@@ -747,7 +729,7 @@ class Note extends Entity {
valueCondition = " AND attributes.value = ?";
}
const notes = await repository.getEntities(`
const notes = this.repository.getEntities(`
WITH RECURSIVE
tree(noteId) AS (
SELECT ?
@@ -774,36 +756,36 @@ class Note extends Entity {
*
* @param {string} name - label name
* @param {string} [value] - label value
* @returns {Promise<Note[]>}
* @returns {Note[]}
*/
async getDescendantNotesWithLabel(name, value) { return await this.getDescendantNotesWithAttribute(LABEL, name, value); }
getDescendantNotesWithLabel(name, value) { return this.getDescendantNotesWithAttribute(LABEL, name, value); }
/**
* Finds descendant 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[]>}
* @returns {Note[]}
*/
async getDescendantNotesWithRelation(name, value) { return await this.getDescendantNotesWithAttribute(RELATION, name, value); }
getDescendantNotesWithRelation(name, value) { return this.getDescendantNotesWithAttribute(RELATION, name, value); }
/**
* Returns note revisions of this note.
*
* @returns {Promise<NoteRevision[]>}
* @returns {NoteRevision[]}
*/
async getRevisions() {
return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
getRevisions() {
return this.repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
}
/**
* Get list of links coming out of this note.
*
* @deprecated - not intended for general use
* @returns {Promise<Attribute[]>}
* @returns {Attribute[]}
*/
async getLinks() {
return await repository.getEntities(`
getLinks() {
return this.repository.getEntities(`
SELECT *
FROM attributes
WHERE noteId = ? AND
@@ -813,24 +795,24 @@ class Note extends Entity {
}
/**
* @returns {Promise<Branch[]>}
* @returns {Branch[]}
*/
async getBranches() {
return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
getBranches() {
return this.repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
}
/**
* @returns {boolean} - true if note has children
*/
async hasChildren() {
return (await this.getChildNotes()).length > 0;
hasChildren() {
return (this.getChildNotes()).length > 0;
}
/**
* @returns {Promise<Note[]>} child notes of this note
* @returns {Note[]} child notes of this note
*/
async getChildNotes() {
return await repository.getEntities(`
getChildNotes() {
return this.repository.getEntities(`
SELECT notes.*
FROM branches
JOIN notes USING(noteId)
@@ -841,10 +823,10 @@ class Note extends Entity {
}
/**
* @returns {Promise<Branch[]>} child branches of this note
* @returns {Branch[]} child branches of this note
*/
async getChildBranches() {
return await repository.getEntities(`
getChildBranches() {
return this.repository.getEntities(`
SELECT branches.*
FROM branches
WHERE branches.isDeleted = 0
@@ -853,10 +835,10 @@ class Note extends Entity {
}
/**
* @returns {Promise<Note[]>} parent notes of this note (note can have multiple parents because of cloning)
* @returns {Note[]} parent notes of this note (note can have multiple parents because of cloning)
*/
async getParentNotes() {
return await repository.getEntities(`
getParentNotes() {
return this.repository.getEntities(`
SELECT parent_notes.*
FROM
branches AS child_tree
@@ -867,17 +849,17 @@ class Note extends Entity {
}
/**
* @return {Promise<string[][]>} - array of notePaths (each represented by array of noteIds constituting the particular note path)
* @return {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path)
*/
async getAllNotePaths() {
getAllNotePaths() {
if (this.noteId === 'root') {
return [['root']];
}
const notePaths = [];
for (const parentNote of await this.getParentNotes()) {
for (const parentPath of await parentNote.getAllNotePaths()) {
for (const parentNote of this.getParentNotes()) {
for (const parentPath of parentNote.getAllNotePaths()) {
parentPath.push(this.noteId);
notePaths.push(parentPath);
}
@@ -886,12 +868,22 @@ class Note extends Entity {
return notePaths;
}
getRelationDefinitions() {
return this.getLabels()
.filter(l => l.name.startsWith("relation:"));
}
getLabelDefinitions() {
return this.getLabels()
.filter(l => l.name.startsWith("relation:"));
}
/**
* @param ancestorNoteId
* @return {Promise<boolean>} - true if ancestorNoteId occurs in at least one of the note's paths
* @return {boolean} - true if ancestorNoteId occurs in at least one of the note's paths
*/
async isDescendantOfNote(ancestorNoteId) {
const notePaths = await this.getAllNotePaths();
isDescendantOfNote(ancestorNoteId) {
const notePaths = this.getAllNotePaths();
return notePaths.some(path => path.includes(ancestorNoteId));
}
@@ -909,10 +901,6 @@ class Note extends Entity {
this.utcDateCreated = dateUtils.utcNowDateTime();
}
if (this.contentLength === undefined) {
this.contentLength = -1;
}
super.beforeSaving();
if (this.isChanged) {
+12 -19
View File
@@ -2,11 +2,10 @@
const Entity = require('./entity');
const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
const utils = require('../services/utils');
const sql = require('../services/sql');
const dateUtils = require('../services/date_utils');
const syncTableService = require('../services/sync_table');
const entityChangesService = require('../services/entity_changes.js');
/**
* NoteRevision represents snapshot of note's title and content at some point in the past. It's used for seamless note versioning.
@@ -16,7 +15,6 @@ const syncTableService = require('../services/sync_table');
* @property {string} type
* @property {string} mime
* @property {string} title
* @property {int} contentLength
* @property {boolean} isErased
* @property {boolean} isProtected
* @property {string} dateLastEdited
@@ -30,11 +28,12 @@ const syncTableService = require('../services/sync_table');
class NoteRevision extends Entity {
static get entityName() { return "note_revisions"; }
static get primaryKeyName() { return "noteRevisionId"; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "contentLength", "isErased", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "isErased", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }
constructor(row) {
super(row);
this.isErased = !!this.isErased;
this.isProtected = !!this.isProtected;
if (this.isProtected) {
@@ -47,8 +46,8 @@ class NoteRevision extends Entity {
}
}
async getNote() {
return await repository.getNote(this.noteId);
getNote() {
return this.repository.getNote(this.noteId);
}
/** @returns {boolean} true if the note has string content (not binary) */
@@ -65,10 +64,10 @@ class NoteRevision extends Entity {
* This is the same approach as is used for Note's content.
*/
/** @returns {Promise<*>} */
async getContent(silentNotFoundError = false) {
/** @returns {*} */
getContent(silentNotFoundError = false) {
if (this.content === undefined) {
const res = await sql.getRow(`SELECT content, hash FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]);
const res = sql.getRow(`SELECT content, hash FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]);
if (!res) {
if (silentNotFoundError) {
@@ -101,13 +100,7 @@ class NoteRevision extends Entity {
}
}
/** @returns {Promise} */
async setContent(content) {
// force updating note itself so that utcDateModified is represented correctly even for the content
this.forcedChange = true;
this.contentLength = content === null ? 0 : content.length;
await this.save();
setContent(content) {
this.content = content;
const pojo = {
@@ -126,9 +119,9 @@ class NoteRevision extends Entity {
}
}
await sql.upsert("note_revision_contents", "noteRevisionId", pojo);
sql.upsert("note_revision_contents", "noteRevisionId", pojo);
await syncTableService.addNoteRevisionContentSync(this.noteRevisionId);
entityChangesService.addNoteRevisionContentEntityChange(this.noteRevisionId);
}
beforeSaving() {
@@ -155,4 +148,4 @@ class NoteRevision extends Entity {
}
}
module.exports = NoteRevision;
module.exports = NoteRevision;
+7 -6
View File
@@ -17,7 +17,6 @@ export async function showDialog(widget) {
$addLinkTitleSettings.toggle(!textTypeWidget.hasSelection());
updateTitleFormGroupVisibility();
$addLinkTitleSettings.find('input[type=radio]').on('change', updateTitleFormGroupVisibility);
// with selection hyper link is implied
@@ -28,6 +27,8 @@ export async function showDialog(widget) {
$addLinkTitleSettings.find("input[value='reference-link']").prop("checked", true);
}
updateTitleFormGroupVisibility();
utils.openDialog($dialog);
$autoComplete.val('').trigger('focus');
@@ -42,11 +43,11 @@ export async function showDialog(widget) {
noteAutocompleteService.initNoteAutocomplete($autoComplete);
$autoComplete.on('autocomplete:selected', function(event, suggestion, dataset) {
if (!suggestion.path) {
if (!suggestion.notePath) {
return false;
}
const noteId = treeService.getNoteIdFromNotePath(suggestion.path);
const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath);
if (noteId) {
setDefaultLinkTitle(noteId);
@@ -54,7 +55,7 @@ export async function showDialog(widget) {
});
$autoComplete.on('autocomplete:cursorchanged', function(event, suggestion, dataset) {
const noteId = treeService.getNoteIdFromNotePath(suggestion.path);
const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath);
if (noteId) {
setDefaultLinkTitle(noteId);
@@ -75,7 +76,7 @@ function updateTitleFormGroupVisibility() {
}
$form.on('submit', () => {
const notePath = $autoComplete.getSelectedPath();
const notePath = $autoComplete.getSelectedNotePath();
if (notePath) {
$dialog.modal('hide');
@@ -89,4 +90,4 @@ $form.on('submit', () => {
}
return false;
});
});
-314
View File
@@ -1,314 +0,0 @@
import server from '../services/server.js';
import toastService from "../services/toast.js";
import treeService from "../services/tree.js";
import attributeAutocompleteService from "../services/attribute_autocomplete.js";
import utils from "../services/utils.js";
import linkService from "../services/link.js";
import libraryLoader from "../services/library_loader.js";
import noteAutocompleteService from "../services/note_autocomplete.js";
import appContext from "../services/app_context.js";
const $dialog = $("#attributes-dialog");
const $saveAttributesButton = $("#save-attributes-button");
const $ownedAttributesBody = $('#owned-attributes-table tbody');
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 = 10;
// 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;
position += 10;
});
};
async function showAttributes(noteId, attributes) {
const ownedAttributes = attributes.filter(attr => attr.noteId === noteId);
for (const attr of ownedAttributes) {
attr.labelValue = attr.type === 'label' ? attr.value : '';
attr.relationValue = attr.type === 'relation' ? (await treeService.getNoteTitle(attr.value)) : '';
attr.selectedPath = attr.type === 'relation' ? attr.value : '';
attr.labelDefinition = (attr.type === 'label-definition' && attr.value) ? attr.value : {
labelType: "text",
multiplicityType: "singlevalue",
isPromoted: true,
numberPrecision: 0
};
attr.relationDefinition = (attr.type === 'relation-definition' && attr.value) ? attr.value : {
multiplicityType: "singlevalue",
inverseRelation: "",
isPromoted: true
};
delete attr.value;
}
self.ownedAttributes(ownedAttributes.map(ko.observable));
addLastEmptyRow();
const inheritedAttributes = attributes.filter(attr => attr.noteId !== noteId);
self.inheritedAttributes(inheritedAttributes);
}
this.loadAttributes = async function() {
const noteId = appContext.tabManager.getActiveTabNoteId();
const attributes = await server.get('notes/' + noteId + '/attributes');
await showAttributes(noteId, attributes);
// attribute might not be rendered immediatelly so could not focus
setTimeout(() => $(".attribute-type-select:last").trigger('focus'), 1000);
};
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) || self.isEmptyRelationTarget(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.trigger('focus');
if (!isValid()) {
alert("Please fix all validation errors and try saving again.");
return;
}
self.updateAttributePositions();
const noteId = appContext.tabManager.getActiveTabNoteId();
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 = treeService.getNoteIdFromNotePath(attr.selectedPath);
}
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(noteId, attributes);
toastService.showMessage("Attributes have been saved.");
};
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,
numberPrecision: 0
},
relationDefinition: {
multiplicityType: "singlevalue",
inverseRelation: "",
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]();
if (cur.name.trim() || cur.isDeleted) {
return false;
}
if (cur.attributeId) {
// name is empty and attribute already exists so this is NO-GO
return true;
}
if (cur.type === 'relation-definition' || cur.type === 'label-definition') {
// for definitions there's no possible empty value so we always require name
return true;
}
if (cur.type === 'label' && cur.labelValue) {
return true;
}
if (cur.type === 'relation' && cur.relationValue) {
return true;
}
return false;
};
this.isEmptyRelationTarget = function(index) {
const cur = self.ownedAttributes()[index]();
return cur.type === "relation" && !cur.isDeleted && cur.name && !cur.relationValue;
};
this.getTargetAttribute = function(target) {
const context = ko.contextFor(target);
const index = context.$index();
return self.ownedAttributes()[index];
}
}
let attributesModel;
function initKoPlugins() {
ko.bindingHandlers.noteLink = {
init: async function (element, valueAccessor, allBindings, viewModel, bindingContext) {
const noteId = ko.unwrap(valueAccessor());
if (noteId) {
const link = await linkService.createNoteLink(noteId);
$(element).append(link);
}
}
};
ko.bindingHandlers.noteAutocomplete = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
noteAutocompleteService.initNoteAutocomplete($(element));
$(element).setSelectedPath(bindingContext.$data.selectedPath);
$(element).on('autocomplete:selected', function (event, suggestion, dataset) {
bindingContext.$data.selectedPath = $(element).val().trim() ? suggestion.path : '';
});
}
};
}
export async function showDialog() {
await libraryLoader.requireLibrary(libraryLoader.KNOCKOUT);
// lazily apply bindings on first use
if (!attributesModel) {
attributesModel = new AttributesModel();
initKoPlugins();
ko.applyBindings(attributesModel, $dialog[0]);
}
await attributesModel.loadAttributes();
utils.openDialog($dialog);
}
$dialog.on('focus', '.attribute-name', function (e) {
attributeAutocompleteService.initAttributeNameAutocomplete({
$el: $(this),
attributeType: () => {
const attribute = attributesModel.getTargetAttribute(this);
return (attribute().type === 'relation' || attribute().type === 'relation-definition') ? 'relation' : 'label';
},
open: true
});
});
$dialog.on('focus', '.label-value', function (e) {
attributeAutocompleteService.initLabelValueAutocomplete({
$el: $(this),
open: true
})
});
+6 -5
View File
@@ -39,20 +39,21 @@ export async function showDialog(noteIds) {
}
async function cloneNotesTo(notePath) {
const targetNoteId = treeService.getNoteIdFromNotePath(notePath);
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
const targetBranchId = await treeCache.getBranchId(parentNoteId, noteId);
for (const cloneNoteId of clonedNoteIds) {
await branchService.cloneNoteTo(cloneNoteId, targetNoteId, $clonePrefix.val());
await branchService.cloneNoteTo(cloneNoteId, targetBranchId, $clonePrefix.val());
const clonedNote = await treeCache.getNote(cloneNoteId);
const targetNote = await treeCache.getNote(targetNoteId);
const targetNote = await treeCache.getBranch(targetBranchId).getNote();
toastService.showMessage(`Note "${clonedNote.title}" has been cloned into ${targetNote.title}`);
}
}
$form.on('submit', () => {
const notePath = $noteAutoComplete.getSelectedPath();
const notePath = $noteAutoComplete.getSelectedNotePath();
if (notePath) {
$dialog.modal('hide');
@@ -64,4 +65,4 @@ $form.on('submit', () => {
}
return false;
});
});
+4 -1
View File
@@ -12,6 +12,7 @@ const $shrinkImagesCheckbox = $("#shrink-images-checkbox");
const $textImportedAsTextCheckbox = $("#text-imported-as-text-checkbox");
const $codeImportedAsCodeCheckbox = $("#code-imported-as-code-checkbox");
const $explodeArchivesCheckbox = $("#explode-archives-checkbox");
const $replaceUnderscoresWithSpacesCheckbox = $("#replace-underscores-with-spaces-checkbox");
let parentNoteId = null;
@@ -23,6 +24,7 @@ export async function showDialog(noteId) {
$textImportedAsTextCheckbox.prop("checked", true);
$codeImportedAsCodeCheckbox.prop("checked", true);
$explodeArchivesCheckbox.prop("checked", true);
$replaceUnderscoresWithSpacesCheckbox.prop("checked", true);
parentNoteId = noteId;
@@ -48,7 +50,8 @@ async function importIntoNote(parentNoteId) {
shrinkImages: boolToString($shrinkImagesCheckbox),
textImportedAsText: boolToString($textImportedAsTextCheckbox),
codeImportedAsCode: boolToString($codeImportedAsCodeCheckbox),
explodeArchives: boolToString($explodeArchivesCheckbox)
explodeArchives: boolToString($explodeArchivesCheckbox),
replaceUnderscoresWithSpaces: boolToString($replaceUnderscoresWithSpacesCheckbox)
};
$dialog.modal('hide');
+2 -2
View File
@@ -38,7 +38,7 @@ async function includeNote(notePath) {
}
$form.on('submit', () => {
const notePath = $autoComplete.getSelectedPath();
const notePath = $autoComplete.getSelectedNotePath();
if (notePath) {
$dialog.modal('hide');
@@ -50,4 +50,4 @@ $form.on('submit', () => {
}
return false;
});
});
+24 -8
View File
@@ -6,21 +6,38 @@ const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
const $showInFullTextButton = $("#show-in-full-text-button");
export async function showDialog() {
$autoComplete.val('');
let lastOpenedTs = 0;
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
export async function showDialog() {
utils.openDialog($dialog);
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true })
.on('autocomplete:selected', function(event, suggestion, dataset) {
if (!suggestion.path) {
if (!suggestion.notePath) {
return false;
}
appContext.tabManager.getActiveTabContext().setNote(suggestion.path);
appContext.tabManager.getActiveTabContext().setNote(suggestion.notePath);
});
noteAutocompleteService.showRecentNotes($autoComplete);
// if you open the Jump To dialog soon after using it previously it can often mean that you
// actually want to search for the same thing (e.g. you opened the wrong note at first try)
// so we'll keep the content.
// if it's outside of this time limit then we assume it's a completely new search and show recent notes instead.
if (Date.now() - lastOpenedTs > KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000) {
noteAutocompleteService.showRecentNotes($autoComplete);
}
else {
$autoComplete
// hack, the actual search value is stored in <pre> element next to the search input
// this is important because the search input value is replaced with the suggestion note's title
.autocomplete("val", $autoComplete.next().text())
.trigger('focus')
.trigger('select');
}
lastOpenedTs = Date.now();
}
function showInFullText(e) {
@@ -30,9 +47,8 @@ function showInFullText(e) {
const searchText = $autoComplete.val();
searchNotesService.resetSearch();
searchNotesService.showSearch();
searchNotesService.doSearch(searchText);
appContext.triggerCommand('showSearch', {searchText});
appContext.triggerCommand('searchForResults', {searchText});
$dialog.modal('hide');
}
+8 -8
View File
@@ -32,27 +32,27 @@ export async function showDialog(branchIds) {
noteAutocompleteService.showRecentNotes($noteAutoComplete);
}
async function moveNotesTo(parentNoteId) {
await branchService.moveToParentNote(movedBranchIds, parentNoteId);
async function moveNotesTo(parentBranchId) {
await branchService.moveToParentNote(movedBranchIds, parentBranchId);
const parentNote = await treeCache.getNote(parentNoteId);
const parentBranch = treeCache.getBranch(parentBranchId);
const parentNote = await parentBranch.getNote();
toastService.showMessage(`Selected notes have been moved into ${parentNote.title}`);
}
$form.on('submit', () => {
const notePath = $noteAutoComplete.getSelectedPath();
const notePath = $noteAutoComplete.getSelectedNotePath();
if (notePath) {
$dialog.modal('hide');
const noteId = treeService.getNoteIdFromNotePath(notePath);
moveNotesTo(noteId);
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
treeCache.getBranchId(parentNoteId, noteId).then(branchId => moveNotesTo(branchId));
}
else {
console.error("No path to move to.");
}
return false;
});
});
+18 -14
View File
@@ -8,7 +8,7 @@ const TPL = `
<br/>
<br/>
<button id="fill-sync-rows-button" class="btn">Fill sync rows</button>
<button id="fill-entity-changes-button" class="btn">Fill entity changes records</button>
<br/>
<br/>
@@ -17,23 +17,22 @@ const TPL = `
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button><br/><br/>
<h4>Debugging</h4>
<h4>Anonymize database</h4>
<p>This action will create a new copy of the database and anonymize it (remove all note content and leave only structure and some non-sensitive metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
<button id="anonymize-button" class="btn">Save anonymized database</button><br/><br/>
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
<h4>Backup database</h4>
<button id="backup-database-button" class="btn">Backup database</button>
<p>Trilium has automatic backup (daily, weekly, monthly), but you can also trigger a manual backup here.</p>
<br/>
<br/>
<button id="backup-database-button" class="btn">Backup database now</button><br/><br/>
<h4>Vacuum database</h4>
<p>This will rebuild database which will typically result in smaller database file. No data will be actually changed.</p>
<p>This will rebuild the database which will typically result in a smaller database file. No data will be actually changed.</p>
<button id="vacuum-database-button" class="btn">Vacuum database</button>`;
@@ -42,7 +41,7 @@ export default class AdvancedOptions {
$("#options-advanced").html(TPL);
this.$forceFullSyncButton = $("#force-full-sync-button");
this.$fillSyncRowsButton = $("#fill-sync-rows-button");
this.$fillEntityChangesButton = $("#fill-entity-changes-button");
this.$anonymizeButton = $("#anonymize-button");
this.$backupDatabaseButton = $("#backup-database-button");
this.$vacuumDatabaseButton = $("#vacuum-database-button");
@@ -54,16 +53,21 @@ export default class AdvancedOptions {
toastService.showMessage("Full sync triggered");
});
this.$fillSyncRowsButton.on('click', async () => {
await server.post('sync/fill-sync-rows');
this.$fillEntityChangesButton.on('click', async () => {
await server.post('sync/fill-entity-changes');
toastService.showMessage("Sync rows filled successfully");
});
this.$anonymizeButton.on('click', async () => {
await server.post('anonymization/anonymize');
const resp = await server.post('database/anonymize');
toastService.showMessage("Created anonymized database");
if (!resp.success) {
toastService.showError("Could not create anonymized database, check backend logs for details");
}
else {
toastService.showMessage(`Created anonymized database in ${resp.anonymizedFilePath}`, 10000);
}
});
this.$backupDatabaseButton.on('click', async () => {
@@ -20,7 +20,7 @@ const TPL = `
</div>
<div class="form-group">
<label for="new-password2">New password once more</label>
<label for="new-password2">New password Confirmation</label>
<input class="form-control" id="new-password2" type="password">
</div>
@@ -75,4 +75,4 @@ export default class ChangePasswordOptions {
return false;
}
}
}
+4 -4
View File
@@ -43,7 +43,7 @@ const TPL = `
<h4>Note erasure timeout</h4>
<p>Deleted notes are at first only marked as deleted and it is possible to recover them
from Recent Notes dialog. After period of time, deleted notes are "erased" which means
from Recent Notes dialog. After a period of time, deleted notes are "erased" which means
their content is not recoverable anymore. This setting allows you to configure the length
of the period between deleting and erasing the note.</p>
@@ -56,8 +56,8 @@ const TPL = `
<div>
<h4>Protected session timeout</h4>
<p>Protected session timeout is a time period after which the protected session is wiped out from
browser's memory. This is measured from the last interaction with protected notes. See <a href="https://github.com/zadam/trilium/wiki/Protected-notes" class="external">wiki</a> for more info.</p>
<p>Protected session timeout is a time period after which the protected session is wiped from
the browser's memory. This is measured from the last interaction with protected notes. See <a href="https://github.com/zadam/trilium/wiki/Protected-notes" class="external">wiki</a> for more info.</p>
<div class="form-group">
<label for="protected-session-timeout-in-seconds">Protected session timeout (in seconds)</label>
@@ -68,7 +68,7 @@ const TPL = `
<div>
<h4>Note revisions snapshot interval</h4>
<p>Note revision snapshot time interval is time in seconds after which new note revision will be created for the note. See <a href="https://github.com/zadam/trilium/wiki/Note-revisions" class="external">wiki</a> for more info.</p>
<p>Note revision snapshot time interval is time in seconds after which a new note revision will be created for the note. See <a href="https://github.com/zadam/trilium/wiki/Note-revisions" class="external">wiki</a> for more info.</p>
<div class="form-group">
<label for="note-revision-snapshot-time-interval-in-seconds">Note revision snapshot time interval (in seconds)</label>
+3 -3
View File
@@ -19,7 +19,7 @@ const TPL = `
<label for="sync-proxy">Sync proxy server (optional)</label>
<input class="form-control" id="sync-proxy" placeholder="https://<host>:<port>">
<p><strong>Note:</strong> If you leave proxy setting blank, system proxy will be used (applies to desktop/electron build only)</p>
<p><strong>Note:</strong> If you leave the proxy setting blank, the system proxy will be used (applies to desktop/electron build only)</p>
</div>
<div style="display: flex; justify-content: space-between;">
@@ -33,7 +33,7 @@ const TPL = `
<h4>Sync test</h4>
<p>This will test connection and handshake to the sync server. If sync server isn't initialized, this will set it up to sync with local document.</p>
<p>This will test the connection and handshake to the sync server. If the sync server isn't initialized, this will set it up to sync with the local document.</p>
<button id="test-sync-button" class="btn">Test sync</button>`;
@@ -78,4 +78,4 @@ export default class SyncOptions {
return false;
}
}
}
+7 -7
View File
@@ -16,18 +16,18 @@ export async function showDialog(ancestorNoteId) {
ancestorNoteId = hoistedNoteService.getHoistedNoteId();
}
const result = await server.get('recent-changes/' + ancestorNoteId);
const recentChangesRows = await server.get('recent-changes/' + ancestorNoteId);
// preload all notes into cache
await treeCache.getNotes(result.map(r => r.noteId), true);
await treeCache.getNotes(recentChangesRows.map(r => r.noteId), true);
$content.empty();
if (result.length === 0) {
if (recentChangesRows.length === 0) {
$content.append("No changes yet ...");
}
const groupedByDate = groupByDate(result);
const groupedByDate = groupByDate(recentChangesRows);
for (const [dateDay, dayChanges] of groupedByDate) {
const $changesList = $('<ul>');
@@ -68,7 +68,7 @@ export async function showDialog(ancestorNoteId) {
}
else {
const note = await treeCache.getNote(change.noteId);
const notePath = await treeService.getSomeNotePath(note);
const notePath = treeService.getSomeNotePath(note);
if (notePath) {
$noteLink = await linkService.createNoteLink(notePath, {
@@ -95,10 +95,10 @@ export async function showDialog(ancestorNoteId) {
}
}
function groupByDate(result) {
function groupByDate(rows) {
const groupedByDate = new Map();
for (const row of result) {
for (const row of rows) {
const dateDay = row.date.substr(0, 10);
if (!groupedByDate.has(dateDay)) {
-138
View File
@@ -1,138 +0,0 @@
import libraryLoader from '../services/library_loader.js';
import server from '../services/server.js';
import toastService from "../services/toast.js";
import utils from "../services/utils.js";
const $dialog = $("#sql-console-dialog");
const $query = $('#sql-console-query');
const $executeButton = $('#sql-console-execute');
const $tableSchemas = $("#sql-console-table-schemas");
const $resultContainer = $("#result-container");
let codeEditor;
$dialog.on("shown.bs.modal", e => initEditor());
export async function showDialog() {
await showTableSchemas();
utils.openDialog($dialog);
}
async function initEditor() {
if (!codeEditor) {
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
// removing Escape binding so that Escape will propagate to the dialog (which will close on escape)
delete CodeMirror.keyMap.basic["Esc"];
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false}
});
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
codeEditor.setValue(`SELECT title, isProtected, type, mime FROM notes WHERE noteId = 'root';
---
SELECT noteId, parentNoteId, notePosition, prefix FROM branches WHERE branchId = 'root';`);
}
codeEditor.focus();
}
async function execute() {
// execute the selected text or the whole content if there's no selection
let sqlQuery = codeEditor.getSelection();
if (!sqlQuery) {
sqlQuery = codeEditor.getValue();
}
const result = await server.post("sql/execute", {
query: sqlQuery
});
if (!result.success) {
toastService.showError(result.error);
return;
}
else {
toastService.showMessage("Query was executed successfully.");
}
const results = result.results;
$resultContainer.empty();
for (const rows of results) {
if (rows.length === 0) {
continue;
}
const $table = $('<table class="table table-striped">');
$resultContainer.append($table);
const result = rows[0];
const $row = $("<tr>");
for (const key in result) {
$row.append($("<th>").html(key));
}
$table.append($row);
for (const result of rows) {
const $row = $("<tr>");
for (const key in result) {
$row.append($("<td>").html(result[key]));
}
$table.append($row);
}
}
}
async function showTableSchemas() {
const tables = await server.get('sql/schema');
$tableSchemas.empty();
for (const table of tables) {
const $tableLink = $('<button class="btn">').text(table.name);
const $columns = $("<ul>");
for (const column of table.columns) {
$columns.append(
$("<li>")
.append($("<span>").text(column.name))
.append($("<span>").text(column.type))
);
}
$tableSchemas.append($tableLink).append(" ");
$tableLink
.tooltip({
html: true,
placement: 'bottom',
boundary: 'window',
title: $columns[0].outerHTML
})
.on('click', () => codeEditor.setValue("SELECT * FROM " + table.name + " LIMIT 100"));
}
}
utils.bindElShortcut($query, 'ctrl+return', execute);
$executeButton.on('click', execute);
+58 -11
View File
@@ -1,3 +1,5 @@
import promotedAttributeDefinitionParser from '../services/promoted_attribute_definition_parser.js';
class Attribute {
constructor(treeCache, row) {
this.treeCache = treeCache;
@@ -19,26 +21,71 @@ class Attribute {
/** @param {int} position */
this.position = row.position;
/** @param {boolean} isInheritable */
this.isInheritable = row.isInheritable;
this.isInheritable = !!row.isInheritable;
}
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
getNote() {
return this.treeCache.notes[this.noteId];
}
get jsonValue() {
try {
return JSON.parse(this.value);
}
catch (e) {
return null;
}
get targetNoteId() { // alias
return this.type === 'relation' ? this.value : undefined;
}
get isAutoLink() {
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
}
get toString() {
return `Attribute(attributeId=${this.attributeId}, type=${this.type}, name=${this.name}, value=${this.value})`;
}
/**
* @return {boolean} - returns true if this attribute has the potential to influence the note in the argument.
* That can happen in multiple ways:
* 1. attribute is owned by the note
* 2. attribute is owned by the template of the note
* 3. attribute is owned by some note's ancestor and is inheritable
*/
isAffecting(affectedNote) {
if (!affectedNote) {
return false;
}
const attrNote = this.getNote();
if (!attrNote) {
// the note (owner of the attribute) is not even loaded into the cache so it should not affect anything else
return false;
}
const owningNotes = [affectedNote, ...affectedNote.getTemplateNotes()];
for (const owningNote of owningNotes) {
if (owningNote.noteId === attrNote.noteId) {
return true;
}
}
if (this.isInheritable) {
for (const owningNote of owningNotes) {
if (owningNote.hasAncestor(attrNote)) {
return true;
}
}
}
return false;
}
isDefinition() {
return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:'));
}
getDefinition() {
return promotedAttributeDefinitionParser.parse(this.value);
}
}
export default Attribute;
export default Attribute;
+5
View File
@@ -28,6 +28,11 @@ class Branch {
return this.treeCache.getNote(this.noteId);
}
/** @returns {NoteShort} */
getNoteFromCache() {
return this.treeCache.getNoteFromCache(this.noteId);
}
/** @returns {NoteShort} */
async getParentNote() {
return this.treeCache.getNote(this.parentNoteId);
+15 -2
View File
@@ -6,9 +6,14 @@ class NoteComplement {
/** @param {string} */
this.noteId = row.noteId;
/** @param {string} */
/**
* @param {string} - can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images)
*/
this.content = row.content;
/** @param {int} */
this.contentLength = row.contentLength;
/** @param {string} */
this.dateCreated = row.dateCreated;
@@ -20,7 +25,15 @@ class NoteComplement {
/** @param {string} */
this.utcDateModified = row.utcDateModified;
// "combined" date modified give larger out of note's and note_content's dateModified
/** @param {string} */
this.combinedDateModified = row.combinedDateModified;
/** @param {string} */
this.combinedUtcDateModified = row.combinedUtcDateModified;
}
}
export default NoteComplement;
export default NoteComplement;
+81 -37
View File
@@ -3,9 +3,7 @@ import Attribute from './attribute.js';
import noteAttributeCache from "../services/note_attribute_cache.js";
const LABEL = 'label';
const LABEL_DEFINITION = 'label-definition';
const RELATION = 'relation';
const RELATION_DEFINITION = 'relation-definition';
/**
* FIXME: since there's no "full note" anymore we can rename this to Note
@@ -45,8 +43,6 @@ class NoteShort {
this.noteId = row.noteId;
/** @param {string} */
this.title = row.title;
/** @param {int} */
this.contentLength = row.contentLength;
/** @param {boolean} */
this.isProtected = !!row.isProtected;
/** @param {string} one of 'text', 'code', 'file' or 'render' */
@@ -58,6 +54,10 @@ class NoteShort {
}
addParent(parentNoteId, branchId) {
if (parentNoteId === 'none') {
return;
}
if (!this.parents.includes(parentNoteId)) {
this.parents.push(parentNoteId);
}
@@ -72,6 +72,10 @@ class NoteShort {
this.childToBranch[childNoteId] = branchId;
this.sortChildren();
}
sortChildren() {
const branchIdPos = {};
for (const branchId of Object.values(this.childToBranch)) {
@@ -170,34 +174,41 @@ class NoteShort {
* @returns {Attribute[]} all note's attributes, including inherited ones
*/
getAttributes(type, name) {
if (!(this.noteId in noteAttributeCache)) {
const ownedAttributes = this.getOwnedAttributes();
return this.__filterAttrs(this.__getCachedAttributes([]), type, name);
}
const attrArrs = [
ownedAttributes
];
__getCachedAttributes(path) {
// notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates
// when template instance is a parent of template itself
if (path.includes(this.noteId)) {
return [];
}
for (const templateAttr of ownedAttributes.filter(oa => oa.type === 'relation' && oa.name === 'template')) {
const templateNote = this.treeCache.notes[templateAttr.value];
if (templateNote) {
attrArrs.push(templateNote.getAttributes());
}
}
if (!(this.noteId in noteAttributeCache.attributes)) {
const newPath = [...path, this.noteId];
const attrArrs = [ this.getOwnedAttributes() ];
if (this.noteId !== 'root') {
for (const parentNote of this.getParentNotes()) {
// these virtual parent-child relationships are also loaded into frontend tree cache
if (parentNote.type !== 'search') {
attrArrs.push(parentNote.getInheritableAttributes());
attrArrs.push(parentNote.__getInheritableAttributes(newPath));
}
}
}
for (const templateAttr of attrArrs.flat().filter(attr => attr.type === 'relation' && attr.name === 'template')) {
const templateNote = this.treeCache.notes[templateAttr.value];
if (templateNote && templateNote.noteId !== this.noteId) {
attrArrs.push(templateNote.__getCachedAttributes(newPath));
}
}
noteAttributeCache.attributes[this.noteId] = attrArrs.flat();
}
return this.__filterAttrs(noteAttributeCache.attributes[this.noteId], type, name);
return noteAttributeCache.attributes[this.noteId];
}
__filterAttrs(attributes, type, name) {
@@ -212,8 +223,8 @@ class NoteShort {
}
}
getInheritableAttributes() {
const attrs = this.getAttributes();
__getInheritableAttributes(path) {
const attrs = this.__getCachedAttributes(path);
return attrs.filter(attr => attr.isInheritable);
}
@@ -234,14 +245,6 @@ class NoteShort {
return this.getAttributes(LABEL, name);
}
/**
* @param {string} [name] - label name to filter
* @returns {Attribute[]} all note's label definitions, including inherited ones
*/
getLabelDefinitions(name) {
return this.getAttributes(LABEL_DEFINITION, name);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Attribute[]} all note's relations (attributes with type relation), including inherited ones
@@ -258,14 +261,6 @@ class NoteShort {
return this.getAttributes(RELATION, name);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Attribute[]} all note's relation definitions including inherited ones
*/
getRelationDefinitions(name) {
return this.getAttributes(RELATION_DEFINITION, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
@@ -425,6 +420,35 @@ class NoteShort {
return targets;
}
/**
* @returns {NoteShort[]}
*/
getTemplateNotes() {
const relations = this.getRelations('template');
return relations.map(rel => this.treeCache.notes[rel.value]);
}
hasAncestor(ancestorNote) {
if (this.noteId === ancestorNote.noteId) {
return true;
}
for (const templateNote of this.getTemplateNotes()) {
if (templateNote.hasAncestor(ancestorNote)) {
return true;
}
}
for (const parentNote of this.getParentNotes()) {
if (parentNote.hasAncestor(ancestorNote)) {
return true;
}
}
return false;
}
/**
* Clear note's attributes cache to force fresh reload for next attribute request.
* Cache is note instance scoped.
@@ -443,6 +467,26 @@ class NoteShort {
.map(attributeId => this.treeCache.attributes[attributeId]);
}
/**
* Get relations which target this note
*
* @returns {NoteShort[]}
*/
async getTargetRelationSourceNotes() {
const targetRelations = this.getTargetRelations();
return await this.treeCache.getNotes(targetRelations.map(tr => tr.noteId));
}
/**
* Return note complement which is most importantly note's content
*
* @return {Promise<NoteComplement>}
*/
async getNoteComplement() {
return await this.treeCache.getNoteComplement(this.noteId);
}
get toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`;
}
@@ -460,4 +504,4 @@ class NoteShort {
}
}
export default NoteShort;
export default NoteShort;
@@ -8,8 +8,8 @@ import NoteTitleWidget from "../widgets/note_title.js";
import RunScriptButtonsWidget from "../widgets/run_script_buttons.js";
import NoteTypeWidget from "../widgets/note_type.js";
import NoteActionsWidget from "../widgets/note_actions.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import AttributeListWidget from "../widgets/attribute_list.js";
export default class DesktopExtraWindowLayout {
constructor(customWidgets) {
@@ -23,24 +23,26 @@ export default class DesktopExtraWindowLayout {
.setParent(appContext)
.id('root-widget')
.css('height', '100vh')
.child(new FlexContainer('row')
.child(new FlexContainer('row').overflowing()
.child(new GlobalMenuWidget())
.child(new TabRowWidget())
.child(new TitleBarButtonsWidget()))
.child(new FlexContainer('row')
.collapsible()
.child(new FlexContainer('column').id('center-pane').css('flex-grow', '1')
.filling()
.child(new FlexContainer('column').id('center-pane').filling()
.child(new FlexContainer('row').class('title-row')
.cssBlock('.title-row > * { margin: 5px; }')
.overflowing()
.cssBlock('.title-row > * { margin: 5px 5px 0 5px; }')
.child(new NoteTitleWidget())
.child(new RunScriptButtonsWidget().hideInZenMode())
.child(new NoteTypeWidget().hideInZenMode())
.child(new NoteActionsWidget().hideInZenMode())
)
.child(new TabCachingWidget(() => new PromotedAttributesWidget()))
.child(new TabCachingWidget(() => new AttributeListWidget()))
.child(new TabCachingWidget(() => new NoteDetailWidget()))
.child(...this.customWidgets.get('center-pane'))
)
);
}
}
}
@@ -11,19 +11,19 @@ import NoteTreeWidget from "../widgets/note_tree.js";
import TabCachingWidget from "../widgets/tab_caching_widget.js";
import NotePathsWidget from "../widgets/note_paths.js";
import NoteTitleWidget from "../widgets/note_title.js";
import AttributeListWidget from "../widgets/attribute_list.js";
import RunScriptButtonsWidget from "../widgets/run_script_buttons.js";
import NoteTypeWidget from "../widgets/note_type.js";
import NoteActionsWidget from "../widgets/note_actions.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import NoteInfoWidget from "../widgets/collapsible_widgets/note_info.js";
import CalendarWidget from "../widgets/collapsible_widgets/calendar.js";
import AttributesWidget from "../widgets/collapsible_widgets/attributes.js";
import LinkMapWidget from "../widgets/collapsible_widgets/link_map.js";
import NoteRevisionsWidget from "../widgets/collapsible_widgets/note_revisions.js";
import SimilarNotesWidget from "../widgets/collapsible_widgets/similar_notes.js";
import SimilarNotesWidget from "../widgets/similar_notes.js";
import WhatLinksHereWidget from "../widgets/collapsible_widgets/what_links_here.js";
import SidePaneToggles from "../widgets/side_pane_toggles.js";
import EditedNotesWidget from "../widgets/collapsible_widgets/edited_notes.js";
const RIGHT_PANE_CSS = `
<style>
@@ -108,7 +108,8 @@ export default class DesktopMainWindowLayout {
.setParent(appContext)
.id('root-widget')
.css('height', '100vh')
.child(new FlexContainer('row')
.child(new FlexContainer('row').overflowing()
.css('height', '36px')
.child(new GlobalMenuWidget())
.child(new TabRowWidget())
.child(new TitleBarButtonsWidget()))
@@ -128,14 +129,16 @@ export default class DesktopMainWindowLayout {
)
.child(new FlexContainer('column').id('center-pane')
.child(new FlexContainer('row').class('title-row')
.cssBlock('.title-row > * { margin: 5px; }')
.cssBlock('.title-row > * { margin: 5px 5px 0 5px; }')
.overflowing()
.child(new NoteTitleWidget())
.child(new RunScriptButtonsWidget().hideInZenMode())
.child(new NoteTypeWidget().hideInZenMode())
.child(new NoteActionsWidget().hideInZenMode())
)
.child(new TabCachingWidget(() => new PromotedAttributesWidget()))
.child(new TabCachingWidget(() => new AttributeListWidget()))
.child(new TabCachingWidget(() => new NoteDetailWidget()))
.child(new TabCachingWidget(() => new SimilarNotesWidget()))
.child(...this.customWidgets.get('center-pane'))
)
.child(new SidePaneContainer('right')
@@ -143,10 +146,9 @@ export default class DesktopMainWindowLayout {
.hideInZenMode()
.child(new NoteInfoWidget())
.child(new TabCachingWidget(() => new CalendarWidget()))
.child(new TabCachingWidget(() => new AttributesWidget()))
.child(new TabCachingWidget(() => new EditedNotesWidget()))
.child(new TabCachingWidget(() => new LinkMapWidget()))
.child(new TabCachingWidget(() => new NoteRevisionsWidget()))
.child(new TabCachingWidget(() => new SimilarNotesWidget()))
.child(new TabCachingWidget(() => new WhatLinksHereWidget()))
.child(...this.customWidgets.get('right-pane'))
)
+5 -5
View File
@@ -76,13 +76,13 @@ export default class MobileLayout {
.child(new NoteTreeWidget("main").cssBlock(FANCYTREE_CSS)))
.child(new ScreenContainer("detail", "column")
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-8")
.child(new FlexContainer('row')
.child(new FlexContainer('row').overflowing()
.css('font-size', 'larger')
.css('align-items', 'center')
.child(new MobileDetailMenuWidget())
.child(new NoteTitleWidget()
.css('padding', '10px')
.css('font-size', 'larger'))
.child(new NoteTitleWidget())
.child(new CloseDetailButtonWidget()))
.child(new NoteDetailWidget()
.css('padding', '5px 20px 10px 0')));
}
}
}
+3 -2
View File
@@ -11,6 +11,7 @@ import Component from "../widgets/component.js";
import keyboardActionsService from "./keyboard_actions.js";
import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js";
import MainTreeExecutors from "./main_tree_executors.js";
import protectedSessionHolder from "./protected_session_holder.js";
class AppContext extends Component {
constructor(isMainWindow) {
@@ -27,8 +28,6 @@ class AppContext extends Component {
async start() {
await Promise.all([treeCache.initializedPromise, options.initializedPromise]);
$("#loading-indicator").hide();
this.showWidgets();
this.tabManager.loadTabs();
@@ -111,6 +110,8 @@ const appContext = new AppContext(window.glob.isMainWindow);
// we should save all outstanding changes before the page/app is closed
$(window).on('beforeunload', () => {
protectedSessionHolder.resetSessionCookie();
appContext.triggerEvent('beforeUnload');
});
@@ -26,6 +26,12 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) {
cb(result);
}
}]);
$el.on('autocomplete:opened', () => {
if ($el.attr("readonly")) {
$el.autocomplete('close');
}
});
}
if (open) {
@@ -33,9 +39,9 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) {
}
}
async function initLabelValueAutocomplete({ $el, open }) {
async function initLabelValueAutocomplete({ $el, open, nameCallback }) {
if (!$el.hasClass("aa-input")) {
const attributeName = $el.parent().parent().find('.attribute-name').val();
const attributeName = nameCallback();
if (attributeName.trim() === "") {
return;
@@ -64,6 +70,12 @@ async function initLabelValueAutocomplete({ $el, open }) {
cb(filtered);
}
}]);
$el.on('autocomplete:opened', () => {
if ($el.attr("readonly")) {
$el.autocomplete('close');
}
})
}
if (open) {
@@ -74,4 +86,4 @@ async function initLabelValueAutocomplete({ $el, open }) {
export default {
initAttributeNameAutocomplete,
initLabelValueAutocomplete
}
}

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