Compare commits

..

238 Commits

Author SHA1 Message Date
zadam 1af98056bb release 0.46.0-beta 2021-02-15 22:29:35 +01:00
zadam c0edcc1bfe split icon list data into separate file 2021-02-15 22:18:37 +01:00
zadam 597e595c32 filtering for the icon picker 2021-02-15 22:11:38 +01:00
zadam e212a37c84 fixes for SQL console 2021-02-15 20:44:55 +01:00
zadam c41448f5da consistency check for wrong lastSyncedPush 2021-02-15 20:44:31 +01:00
zadam aab49946f8 prototype of note icon picker 2021-02-14 23:27:28 +01:00
zadam cc4d04416b added "delete note revisions" search action 2021-02-14 21:35:13 +01:00
zadam 66c5606d46 implemented "rawContent", #1637 2021-02-14 19:27:31 +01:00
zadam 50bcf45113 fixed "duplicate subtree" creating extra entities in case subtrees contains multiple clones 2021-02-14 12:14:33 +01:00
zadam 08cba1e1ce fixed "duplicate subtree" 2021-02-14 11:43:31 +01:00
zadam f528799fed added "limit" search modifier to search definition 2021-02-13 23:52:52 +01:00
zadam a78c8ddad7 small search help improvements 2021-02-13 23:38:31 +01:00
zadam 3cc93157bf basic support for audio/video preview in file notes (no seeking) 2021-02-13 23:20:22 +01:00
zadam 3dbd80d5a6 relocated note paths widget into note title row 2021-02-13 22:47:06 +01:00
zadam 076b4a6651 small tweaks 2021-02-13 21:52:31 +01:00
zadam 378987e61c added icon to note title row 2021-02-13 20:07:08 +01:00
zadam 5eb850bf59 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/services/search/services/search.js
2021-02-13 12:27:10 +01:00
zadam 4914160590 fix correct application of <small> tag, closes #1635 2021-02-13 12:23:50 +01:00
zadam 00c106aba6 fix update of value from autocomplete, #1557 2021-02-13 12:11:58 +01:00
zadam 5e6d2c8336 db migration fix 2021-02-12 23:52:16 +01:00
zadam d45a59aa4e sync fixes 2021-02-12 23:30:11 +01:00
zadam 9f19cf8046 don't sync recent_notes 2021-02-12 22:39:38 +01:00
zadam c0dfd23191 fix merge conflict 2021-02-11 23:08:37 +01:00
zadam b9539117d8 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/services/auth.js
#	src/services/setup.js
2021-02-11 23:07:20 +01:00
zadam d616a77d6b release 0.45.10 2021-02-11 23:05:48 +01:00
zadam 5b679930de revert accidentally committed auth changes, #1629 2021-02-11 23:04:42 +01:00
zadam 3d7beefad0 sync fixes 2021-02-11 22:50:32 +01:00
zadam 8850de51f5 sync fixes 2021-02-10 22:56:23 +01:00
zadam 18b13657fd don't compare hashes of note_reordering entity_changes 2021-02-09 23:27:09 +01:00
zadam 43ac712fca fix searchForNotes API method to look for archived notes as well 2021-02-09 22:32:25 +01:00
zadam bf231d31a2 fancytree 2.38.0 2021-02-09 21:33:55 +01:00
zadam 7dd19c0366 correct reloading of note subtree when relevant labels change 2021-02-09 20:26:10 +01:00
zadam 0040334e89 fix infinite cycle when template is descendant of an instance 2021-02-09 20:15:14 +01:00
zadam 96a5cb23f3 Merge remote-tracking branch 'origin/stable' 2021-02-08 21:16:14 +01:00
zadam 067ca9ab16 fallback for incorrectly parsed pane widths, #1621 2021-02-08 20:51:38 +01:00
zadam be65e2e62f fixed custom header authentication 2021-02-07 21:50:34 +01:00
zadam bed7bdfd00 "move to new window" respects note hoisting of original tab 2021-02-07 21:27:09 +01:00
zadam cd5be59413 tab row is detecting workspace changes 2021-02-07 20:55:49 +01:00
zadam 2a67d9eae2 added unhoist button to the tree node + onclick fixes 2021-02-06 19:58:12 +01:00
zadam 0f05d6258e small fixes 2021-02-05 23:19:43 +01:00
zadam 45516fa7b4 Merge remote-tracking branch 'origin/stable' 2021-02-05 22:54:50 +01:00
zadam 70708b36ef using custom header for sync authorization to avoid tripping security proxies 2021-02-05 21:59:56 +01:00
zadam 397f0d1326 fix collapse tree 2021-02-04 23:08:16 +01:00
zadam 6469937393 fix note tree flickering (note cache was not updated when expanded status changed) 2021-02-04 22:05:32 +01:00
zadam 48cb4d2ab9 one line overflow for note properties 2021-02-02 23:36:27 +01:00
zadam 3a94b04ef4 quick search impl finished 2021-02-01 23:51:04 +01:00
zadam d406df48a8 edited notes doesn't wrap long note titles 2021-02-01 20:55:35 +01:00
zadam 9447d3f9b5 quick search widget WIP 2021-01-31 22:45:45 +01:00
zadam 2ac78c2e03 global menu does not need "menu" title 2021-01-31 20:36:30 +01:00
zadam 17073096cf disable property widgets if not applicable 2021-01-31 20:07:56 +01:00
zadam 6068bd7c44 added note properties widget 2021-01-31 12:15:36 +01:00
zadam 56a35b85a6 Merge remote-tracking branch 'origin/stable' 2021-01-30 22:49:19 +01:00
zadam eb1a6b78ff updated schema 2021-01-30 22:17:29 +01:00
zadam 60d000b9cb VACUUM for migration 2021-01-30 22:12:38 +01:00
zadam 61ddd8afc6 small fixes to migration scripts 2021-01-30 22:00:09 +01:00
zadam 69bdcdd74a changed sidebar widget expanders to be consistent with tree expanders 2021-01-30 21:28:10 +01:00
zadam 17c6f53397 fix "twitching" tree nodes on hover 2021-01-30 21:03:39 +01:00
zadam 1c51419db5 image properties converted to collapsible section container 2021-01-30 20:21:44 +01:00
zadam 1ba1f28006 let's try "display: flex" fix for the include note positioning bug, #1590 2021-01-30 20:07:56 +01:00
zadam 6b306ac426 alert for missing file preview 2021-01-30 15:59:59 +01:00
zadam ae26f22114 class moving 2021-01-30 15:50:46 +01:00
zadam 552d213817 file properties converted to collapsible section container 2021-01-30 15:47:24 +01:00
zadam 8fb7dd451e small fixes 2021-01-30 11:50:45 +01:00
zadam 01a82bed09 extra window layout updated to be consistent with main window 2021-01-29 23:36:33 +01:00
zadam 77bc5f9944 disabling tree expand animation to avoid "recursion" errors 2021-01-29 23:33:41 +01:00
zadam 6081c3540e refactoring 2021-01-29 22:44:59 +01:00
zadam 0a10764ed4 Merge remote-tracking branch 'origin/stable' 2021-01-29 21:19:48 +01:00
zadam 8019e23ee4 note list displays correct paging 2021-01-28 23:29:10 +01:00
zadam 5eb9b8fe42 included notes are not displayed in children overview 2021-01-28 23:15:58 +01:00
zadam d19a929237 removed auto book as it is no longer needed 2021-01-28 22:34:35 +01:00
zadam 67e0e0888e Merge remote-tracking branch 'origin/stable' 2021-01-28 22:17:46 +01:00
zadam 810fd9e819 fix glitches on drag & drop, fixes #1587 2021-01-28 21:19:01 +01:00
zadam 2dc742f5b8 don't recompress animated images, #1585 2021-01-28 20:17:57 +01:00
zadam 48b7108da0 add meta theme-color tag to mobile version, #1576 2021-01-27 20:58:20 +01:00
zadam fbabdef272 added ancestor depth search criteria 2021-01-26 23:25:18 +01:00
zadam a1f67e830d search script UI 2021-01-26 22:22:17 +01:00
zadam ee78413ecb improvements to the search spec 2021-01-26 15:54:41 +01:00
zadam b936a63db3 improvements to the search spec help button 2021-01-26 14:44:53 +01:00
zadam 80354d09fb search definition more fixed layout 2021-01-26 14:10:34 +01:00
zadam 87925f72a3 search definition action refactoring 2021-01-26 10:48:28 +01:00
zadam 898c941333 search definition refactoring 2021-01-26 10:42:55 +01:00
zadam 8b0a1e546d search fixes 2021-01-25 23:43:36 +01:00
zadam ec351137d4 search definition refactoring 2021-01-25 21:24:02 +01:00
zadam cf5127dcec split up search definition options to more classes 2021-01-24 22:30:53 +01:00
zadam 345de752d1 Merge remote-tracking branch 'origin/stable' 2021-01-23 21:53:21 +01:00
zadam 7b0fd639f6 improved heuristic of displaing attributes in note list and tooltips #1558 2021-01-23 21:41:02 +01:00
zadam 6408a47a8a Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/services/note_cache/entities/note.js
2021-01-23 21:09:47 +01:00
zadam 872e81fe1f search now supports searching / ordering by note size 2021-01-22 22:20:17 +01:00
zadam 480aec1667 size calculation is always on demand 2021-01-22 15:16:08 +01:00
zadam 35ea877c7d Merge remote-tracking branch 'origin/stable' 2021-01-21 22:06:17 +01:00
zadam 41ca4b8d2e fix relation map view 2021-01-21 22:05:56 +01:00
zadam 8375a1b5ab optimize usage of note path and attributes in the note tooltip, #1558 2021-01-21 21:48:06 +01:00
zadam c33d496cf3 note info widget provides basic info on note (and subtree) size 2021-01-21 20:02:40 +01:00
zadam 200982655f WIP for counting note and subtree size 2021-01-20 22:17:40 +01:00
zadam d67e1552ee more impl for search actions 2021-01-20 21:45:30 +01:00
zadam ce09e4a1eb basic backend implementation of search actions 2021-01-20 20:31:24 +01:00
zadam 7bf6ec3ff2 bootstrap 4.6.0 2021-01-19 22:28:43 +01:00
zadam 1f923403de UI for search actions (WIP) 2021-01-19 22:10:24 +01:00
zadam cb2361e9c4 Merge remote-tracking branch 'origin/master' 2021-01-19 19:45:57 +01:00
zadam d7e46263be UI fixes to search definition + backend support 2021-01-18 22:52:07 +01:00
王登辉 138903cf28 fix config.ini Network host not work (#1546) 2021-01-18 17:46:48 +01:00
zadam 3fa2535862 basic interactivity of search definition actions 2021-01-17 23:01:01 +01:00
zadam 5bb490e1ff basic interactivity of search definition 2021-01-17 21:11:01 +01:00
zadam f2e44e5617 fix tree's enhanced item buttons 2021-01-15 23:05:19 +01:00
zadam 58a93f748b Merge remote-tracking branch 'origin/stable' 2021-01-15 22:23:56 +01:00
zadam 199b49c1f7 fix partial syncs 2021-01-14 23:22:13 +01:00
zadam cbc1f63732 Merge remote-tracking branch 'origin/stable' 2021-01-14 21:53:05 +01:00
zadam 3f2e8717b3 sanitize also clipper content, #1532 2021-01-14 21:52:44 +01:00
zadam cd858a73c1 Merge remote-tracking branch 'origin/stable' 2021-01-11 22:49:08 +01:00
zadam 69353f12b5 partial sync requests WIP 2021-01-11 22:48:51 +01:00
zadam 7f8b19aee4 WIP partial sync requests 2021-01-10 21:56:40 +01:00
zadam cd653b9f0c add link dialog should recognize external links, closes #1521 2021-01-08 21:44:43 +01:00
zadam 9fcd659df4 mock up of search definition changes 2021-01-08 19:57:49 +01:00
zadam c08c4531db fix empty range items handling 2021-01-05 14:24:25 +01:00
zadam f8bec35893 use selected text to apply filtering for add link dialog, closes #1515 2021-01-05 14:22:11 +01:00
zadam 7aadd57d2f search help dropdown button 2020-12-30 23:09:29 +01:00
zadam 02d9752abf refactoring of sql console into separate widgets 2020-12-29 22:27:31 +01:00
zadam 1d64129572 lazy loading of note list 2020-12-27 22:19:27 +01:00
zadam 95760f1d28 don't display similar notes under search 2020-12-25 13:06:58 +01:00
zadam 75c3bd8ae3 Merge remote-tracking branch 'origin/stable' 2020-12-25 13:02:05 +01:00
zadam 479d6ba964 Merge remote-tracking branch 'origin/stable' 2020-12-25 12:56:34 +01:00
zadam 9f472b353a simplified table stats 2020-12-23 21:22:41 +01:00
zadam 4c614aee90 note paths CSS tweaks 2020-12-23 21:03:01 +01:00
zadam f17be0ef9b Merge remote-tracking branch 'origin/stable' 2020-12-22 22:38:37 +01:00
zadam 7dde78e98a Merge remote-tracking branch 'origin/stable' 2020-12-21 23:19:25 +01:00
zadam 8c0662ab82 fix displaying protected notes outside of protected session 2020-12-21 23:10:08 +01:00
zadam cc3addebd4 date notes respect their parent's isProtected status, #1483 2020-12-21 23:00:39 +01:00
zadam a1b2e22ba5 add possibility to add whole iso date as date pattern, #1496 2020-12-20 22:49:42 +01:00
zadam f8936f3bc8 use trilium.sid as a session ID to avoid session ID conflicts 2020-12-20 22:20:21 +01:00
Steven Tang 015c9d2ccd rename session cookie from connect.sid to trilium (#1495)
to prevent collisions with other express-session apps
2020-12-20 22:18:57 +01:00
zadam 95ccfd2521 use bootstrap default monospace fonts for code notes, fixes #1489 2020-12-18 22:37:29 +01:00
zadam 6c15d299e0 isErased fixes 2020-12-18 22:35:40 +01:00
zadam f8089ba370 fixed saved search refresh 2020-12-17 21:19:52 +01:00
zadam 053162fef2 bootstrap 4.5.3 2020-12-17 15:20:20 +01:00
zadam 71bbf2eb16 close note type dropdown after click 2020-12-17 15:19:22 +01:00
zadam f723433970 fix deleting notes 2020-12-17 15:04:04 +01:00
zadam 1cc7917b6e renamed remnants of "sync" to "entity_change" 2020-12-16 22:17:42 +01:00
zadam cdc2721ac1 DB migration fixes 2020-12-16 20:58:43 +01:00
zadam 4a15127e58 removal of isErased from old entities 2020-12-16 15:05:36 +01:00
zadam c5ec57e08e cleanup of isErased obsolete usage 2020-12-16 15:01:20 +01:00
zadam d0578971f7 fix manual erasing note revisions 2020-12-16 14:36:24 +01:00
zadam 1b0bb22273 drop isDeleted index 2020-12-15 15:30:46 +01:00
zadam bf653a9a5c ckeditor 24 content styles 2020-12-15 15:18:45 +01:00
zadam 5467dc62cd extra case sensitivity tests 2020-12-15 15:15:41 +01:00
zadam 96eff4c410 fixed search tests 2020-12-15 15:09:00 +01:00
zadam 3a12181a57 search fixes WIP 2020-12-14 23:59:05 +01:00
zadam fccce2ff1e small fixes 2020-12-14 23:04:56 +01:00
zadam c949dcb449 fixes and optimizations 2020-12-14 22:12:26 +01:00
zadam 8b99f065d5 fixes of recent entity changes 2020-12-14 14:17:51 +01:00
zadam 6d7b9e0db3 erasing rows of deleted entities 2020-12-14 13:58:02 +01:00
zadam 248fa780e8 erasing rows of deleted entities 2020-12-14 13:47:33 +01:00
zadam 20c7c657da moving isErased into entity_changes 2020-12-14 13:15:32 +01:00
zadam 0a896cc19c tree fixes 2020-12-13 23:27:42 +01:00
zadam 0bb548d0d1 ckeditor 24 2020-12-13 20:27:57 +01:00
zadam de1584eab8 note list improvements 2020-12-13 20:13:57 +01:00
zadam 60e2026850 improved note list pager for many pages 2020-12-12 12:07:15 +01:00
zadam 5098dda376 load initial tree from note cache 2020-12-11 22:06:12 +01:00
zadam 35e47f866a fix root branch 2020-12-11 15:27:57 +01:00
zadam 199d89c1a6 optimizations and fixes 2020-12-11 15:24:44 +01:00
zadam be543737a9 sorting search results occurences in the note title 2020-12-11 13:54:41 +01:00
zadam 0b6fa4ab06 Merge remote-tracking branch 'origin/optimize-tree' 2020-12-10 22:54:16 +01:00
zadam 8f1b2fa226 move entity hash to entity_changes table 2020-12-10 22:53:52 +01:00
zadam 5010175b84 adding few missing isErased = 0 conditions 2020-12-10 21:56:48 +01:00
zadam 0ac42608f7 faster tree loading of many notes at once #1480 2020-12-10 21:27:21 +01:00
zadam 7f3ef2cb8b move entity hash to entity_changes table WIP 2020-12-07 23:04:17 +01:00
zadam 16c4b8aa43 added also "sqlConsoleHome" 2020-12-07 09:19:27 +01:00
zadam 4078b32e9d fix "show in full text search" in jump to note dialog 2020-12-05 23:19:02 +01:00
zadam 90d33f56c3 implemented "search in subtree" 2020-12-05 23:00:28 +01:00
zadam b0e5ab7533 smaller search improvements 2020-12-04 22:57:54 +01:00
zadam 526bd81b50 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/public/app/widgets/attribute_list.js
2020-12-04 22:09:27 +01:00
zadam a1fb84f14d unification of tooltip and note list renderers 2020-12-03 22:54:24 +01:00
zadam bc520edd19 fix usage of async .getHeaders() 2020-12-03 21:50:41 +01:00
zadam 5f4f0d3086 experimental markdown-like headings 2020-12-01 22:01:58 +01:00
zadam a68c61b2f0 improvements to saved search 2020-11-30 23:20:12 +01:00
zadam 2a6978c349 added button for entering workspace 2020-11-29 22:32:31 +01:00
zadam 1c5425c9eb workaround for filter + create child problems 2020-11-29 20:55:24 +01:00
zadam 2338a13749 added "add child note" button to note tree 2020-11-27 23:13:48 +01:00
zadam 6df6fe03fa fix display of attr detail 2020-11-27 22:38:15 +01:00
zadam b0217ee04c added new "create note" button 2020-11-27 22:21:13 +01:00
zadam 2a54ce803e layout tweaks 2020-11-27 22:02:55 +01:00
zadam 642bf9d113 layout tweaks 2020-11-27 21:57:41 +01:00
zadam 31746e7baf Merge remote-tracking branch 'origin/master' 2020-11-27 20:40:40 +01:00
zadam 48b1703725 moved "global buttons" on the tree 2020-11-27 20:40:32 +01:00
baddate 72d6440de7 add chinese version README. (#1465) 2020-11-27 08:37:08 +01:00
zadam eaed7ec86f improvements in search UI 2020-11-26 23:00:27 +01:00
zadam 02043d9109 note list is now separate widget which allows more flexibility in its usage 2020-11-25 23:18:30 +01:00
zadam b9c0823abf added workspace icon and background color 2020-11-25 20:25:55 +01:00
zadam 61f30373d4 hoisting improvements 2020-11-24 23:24:05 +01:00
zadam 9889166afc Merge remote-tracking branch 'origin/stable' 2020-11-24 23:19:16 +01:00
zadam 35d5036aaa hoisting fixes 2020-11-24 22:32:22 +01:00
zadam 14f5b46ece Merge remote-tracking branch 'origin/stable' 2020-11-24 20:31:16 +01:00
zadam ac636b6649 WIP per-tab hoisting 2020-11-23 23:11:21 +01:00
zadam 52b8162d01 WIP per-tab hoisting 2020-11-23 22:52:48 +01:00
zadam 167b6974fe Merge remote-tracking branch 'origin/stable' 2020-11-23 21:20:06 +01:00
zadam 4bd86a6dfa WIP per-tab hoisting 2020-11-22 23:05:02 +01:00
zadam 5bbec118e4 Merge remote-tracking branch 'origin/stable' 2020-11-22 22:48:26 +01:00
zadam 473229f03a Merge remote-tracking branch 'origin/stable' 2020-11-21 20:44:37 +01:00
zadam af1fd5bd06 improvements to collapsible section container 2020-11-19 23:02:25 +01:00
zadam c737a3adc9 Merge remote-tracking branch 'origin/stable' 2020-11-19 21:42:49 +01:00
zadam 8e2bf7795c Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/routes/api/attributes.js
#	src/routes/api/sender.js
2020-11-18 22:42:37 +01:00
ohdearaugustin 4fd9e7f14f Drop privileges (#1422) 2020-11-15 20:51:47 +01:00
zadam 7859aca006 Merge remote-tracking branch 'origin/stable' 2020-11-11 21:28:11 +01:00
zadam 1403acd808 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/public/app/services/note_content_renderer.js
2020-11-08 21:06:48 +01:00
Praxis | Darian e258de329c Use user theme on mobile (#1374) 2020-11-02 20:53:43 +01:00
zadam 044bcf6f05 Merge remote-tracking branch 'origin/stable' 2020-11-01 21:25:38 +01:00
zadam 2c028f7c45 new CollapsibleSectionContainer WIP 2020-10-31 22:47:15 +01:00
zadam 435200ec5a new CollapsibleSectionContainer WIP 2020-10-30 22:57:26 +01:00
zadam 3d808d638a Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	package-lock.json
#	package.json
#	src/public/app/services/note_content_renderer.js
2020-10-30 15:40:57 +01:00
zadam bb0ee0b36c expand all / collapse all will re-render list instead of doing that manually 2020-10-26 22:52:12 +01:00
zadam b70ee742e8 Merge remote-tracking branch 'origin/math2'
# Conflicts:
#	package-lock.json
2020-10-26 21:44:18 +01:00
zadam 4281111344 initial integration of note list renderer into search results 2020-10-25 23:02:12 +01:00
zadam 37e111d8a9 trim content for grid view 2020-10-25 11:00:27 +01:00
zadam 4202af96c8 image scaling improvements 2020-10-25 00:12:07 +02:00
zadam d0efd67d5e grid view improvements, remembering grid/list status 2020-10-24 23:50:32 +02:00
zadam aeb6c34d27 note list rendering WIP 2020-10-24 00:02:38 +02:00
zadam a3f4fc7762 fix case where parents of templates are not loaded 2020-10-24 00:02:10 +02:00
zadam 6517188b39 fix tree loading (partial revert of previous commit) 2020-10-24 00:00:24 +02:00
zadam c4acecd510 fix loading unnecessary notes into the cache 2020-10-23 23:19:29 +02:00
zadam c7d8bddf24 conversion of note list renderer to class 2020-10-23 22:11:39 +02:00
zadam 29c1d05540 frontend dayjs update 2020-10-23 21:24:16 +02:00
zadam 55f04436c0 saving expanded status as label 2020-10-23 00:11:44 +02:00
zadam 4ce2eaa919 note list paging 2020-10-22 23:50:52 +02:00
zadam 489e064932 expand/collapse all 2020-10-22 23:07:35 +02:00
zadam 1369603ed9 note list renderer WIP 2020-10-22 22:49:22 +02:00
zadam 79431d8b4b Merge remote-tracking branch 'origin/master'
# Conflicts:
#	package-lock.json
#	package.json
#	src/routes/api/sender.js
2020-10-21 23:23:55 +02:00
zadam 4591899df0 Merge branch 'math2'
# Conflicts:
#	package-lock.json
#	package.json
2020-10-21 23:23:19 +02:00
zadam dfa7d3f186 Update README.md 2020-10-21 22:40:34 +02:00
Casey Doran a7c13ceacc if the sender request contains a label and/or a labelValue, apply it (#1310) 2020-10-16 19:40:30 +02:00
zadam b2b3c80192 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	package-lock.json
#	package.json
2020-10-15 20:47:10 +02:00
zadam badb678b69 note list renderer improvements, still WIP 2020-10-08 23:46:48 +02:00
zadam 5933b17b47 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	libraries/ckeditor/ckeditor.js.map
2020-10-07 23:29:10 +02:00
zadam 761912c3bd basic separation of note list renderer from book type 2020-10-07 23:08:17 +02:00
zadam 99f35b2588 Merge remote-tracking branch 'origin/stable' 2020-10-06 21:03:28 +02:00
FliegendeWurst 51eb9f18f7 Jump to note dialog: fix duplicate event triggers (#1283)
This fixes an issue where the current path (above the note tree) displays parts multiple times, e.g. A / A / B / B instead of A / B.

The same issue could also affect other event handlers, but I didn't look around the codebase further.
2020-10-02 21:12:13 +02:00
zadam 4b8ca31091 Merge remote-tracking branch 'origin/stable' 2020-09-30 22:50:33 +02:00
zadam 2706f1b02f Merge remote-tracking branch 'origin/stable' 2020-09-26 23:28:50 +02:00
zadam 6710476768 new saved search UI wip 2020-09-25 23:52:24 +02:00
zadam de30095737 change salts on password change + more robust handling of decryption failures 2020-09-25 20:55:45 +02:00
237 changed files with 16298 additions and 3925 deletions
+2
View File
@@ -23,5 +23,7 @@ RUN set -x \
# Bundle app source
COPY . .
USER node
EXPOSE 8080
CMD [ "node", "./src/www" ]
+58
View File
@@ -0,0 +1,58 @@
# Trilium注意事项
[![Join the chat at https://gitter.im/trilium-notes/Lobby](https://badges.gitter.im/trilium-notes/Lobby.svg)](https://gitter.im/trilium-notes/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Trilium Notes是一个分层的笔记应用程序,专注于建立大型个人知识库。请参阅[屏幕截图](https://github.com/zadam/trilium/wiki/Screenshot-tour)以快速了解:
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
## 特性
* 笔记可以排列成任意深的树。单个笔记可以放在树中的多个位置(请参阅[克隆](https://github.com/zadam/trilium/wiki/Cloning-notes)
* 丰富的所见即所得笔记编辑功能,包括带有markdown[自动格式化功能的](https://github.com/zadam/trilium/wiki/Text-notes#autoformat)表格,图像和[数学](https://github.com/zadam/trilium/wiki/Text-notes#math-support)
* 支持编辑[使用源代码的笔记](https://github.com/zadam/trilium/wiki/Code-notes),包括语法高亮显示
* 笔记之间快速[导航](https://github.com/zadam/trilium/wiki/Note-navigation),全文搜索和[笔记挂起](https://github.com/zadam/trilium/wiki/Note-hoisting)
* 无缝[笔记版本控制](https://github.com/zadam/trilium/wiki/Note-revisions)
* 笔记[属性](https://github.com/zadam/trilium/wiki/Attributes)可用于笔记组织,查询和高级[脚本编写](https://github.com/zadam/trilium/wiki/Scripts)
* [同步](https://github.com/zadam/trilium/wiki/Synchronization)与自托管同步服务器
* 具有按笔记粒度的强大的[笔记加密](https://github.com/zadam/trilium/wiki/Protected-notes)
* [关系图](https://github.com/zadam/trilium/wiki/Relation-map)和[链接图](https://github.com/zadam/trilium/wiki/Link-map),用于可视化笔记及其关系
* [脚本](https://github.com/zadam/trilium/wiki/Scripts)-请参阅[高级展示](https://github.com/zadam/trilium/wiki/Advanced-showcases)
* 可用性和性能均能很好地扩展至超过10万个笔记
* 针对智能手机和平板电脑进行触摸优化的[移动前端](https://github.com/zadam/trilium/wiki/Mobile-frontend)
* [夜间主题](https://github.com/zadam/trilium/wiki/Themes)
* [Evernote](https://github.com/zadam/trilium/wiki/Evernote-import)和[Markdown导入导出](https://github.com/zadam/trilium/wiki/Markdown)
* [Web Clipper](https://github.com/zadam/trilium/wiki/Web-clipper)可轻松保存Web内容
## 构建
Trilium是作为桌面应用程序(Linux和Windows)或服务器上托管的Web应用程序(Linux)提供的。Mac OS桌面版本可用,但[不受支持](https://github.com/zadam/trilium/wiki/FAQ#mac-os-support)。
* 如果要在桌面上使用Trilium,请从[最新版本](https://github.com/zadam/trilium/releases/latest)下载适用于您平台的二进制[版本](https://github.com/zadam/trilium/releases/latest),解压缩该软件包并运行`trilium`可执行文件。
* 如果要在服务器上安装Trilium,请遵循[此页面](https://github.com/zadam/trilium/wiki/Server-installation)。
* 当前仅支持(经过测试)最新的Chrome和Firefox浏览器。
## 文档
[有关文档页面的完整列表,请参见Wiki。](https://github.com/zadam/trilium/wiki/)
您还可以阅读[个人知识库模式](https://github.com/zadam/trilium/wiki/Patterns-of-personal-knowledge-base),以获取有关如何使用Trilium的灵感。
## 贡献
使用基于浏览器的开发环境
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/zadam/trilium)
或在本地克隆并运行
```
npm install
npm run start-server
```
## 致谢
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - 市场上最好的所见即所得编辑器,互动性强且聆听能力强的团队
* [FancyTree](https://github.com/mar10/fancytree) - 一个非常丰富的关于树的库,强大的没有对手。没有它,Trilium Notes将不会如此。
* [CodeMirror](https://github.com/codemirror/CodeMirror) - 支持大量语言的代码编辑器
* [jsPlumb](https://github.com/jsplumb/jsplumb)强大的可视化连接库。- 用于[关系图](https://github.com/zadam/trilium/wiki/Relation-map)和[链接图](https://github.com/zadam/trilium/wiki/Link-map)
+2 -2
View File
@@ -8,7 +8,7 @@ Trilium Notes is a hierarchical note taking application with focus on building l
## Features
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://github.com/zadam/trilium/wiki/Cloning-notes))
* Rich WYSIWYG note editing including e.g. tables and images with markdown [autoformat](https://github.com/zadam/trilium/wiki/Text-notes#autoformat)
* Rich WYSIWYG note editing including e.g. tables, images and [math](https://github.com/zadam/trilium/wiki/Text-notes#math-support) with markdown [autoformat](https://github.com/zadam/trilium/wiki/Text-notes#autoformat)
* Support for editing [notes with source code](https://github.com/zadam/trilium/wiki/Code-notes), including syntax highlighting
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation), full text search and [note hoisting](https://github.com/zadam/trilium/wiki/Note-hoisting)
* Seamless [note versioning](https://github.com/zadam/trilium/wiki/Note-revisions)
@@ -54,4 +54,4 @@ npm run start-server
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team
* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map)
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map)
BIN
View File
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
DELETE FROM options WHERE name IN (
'noteInfoWidget',
'attributesWidget',
'linkMapWidget',
'noteRevisionsWidget',
'whatLinksHereWidget',
'codeNotesMimeTypes',
'similarNotesWidget',
'editedNotesWidget',
'calendarWidget',
'sidebarMinWidth',
'sidebarWidthPercent',
'showSidebarInNewTab',
'hoistedNoteId'
);
@@ -0,0 +1,23 @@
const repository = require('../../src/services/repository');
module.exports = () => {
for (const note of repository.getEntities("SELECT * FROM notes WHERE type = 'search' AND isProtected = 0 AND isDeleted = 0")) {
try {
let origContent = note.getJsonContent();
if (!origContent) {
continue;
}
note.addLabel('searchString', origContent.searchString);
note.setContent('');
note.mime = 'text/plain';
note.save();
}
catch (e) {
console.log(`Changing note content for note ${note.noteId} failed with: ${e.message} ${e.stack}`);
}
}
};
@@ -0,0 +1,69 @@
CREATE TABLE IF NOT EXISTS "mig_entity_changes" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`hash` TEXT NOT NULL,
`sourceId` TEXT NOT NULL,
`isErased` INT NOT NULL,
`utcDateChanged` TEXT NOT NULL,
`isSynced` INTEGER NOT NULL);
INSERT INTO mig_entity_changes (id, entityName, entityId, hash, sourceId, isSynced, utcDateChanged, isErased)
SELECT id, entityName, entityId, '', sourceId, isSynced, utcChangedDate, 0 FROM entity_changes;
UPDATE mig_entity_changes SET isErased = (SELECT isErased FROM notes WHERE noteId = entityId) WHERE entityName = 'notes';
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM notes WHERE noteId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'notes';
UPDATE mig_entity_changes SET isErased = (SELECT isErased FROM notes WHERE noteId = entityId) WHERE entityName = 'note_contents';
UPDATE mig_entity_changes SET isErased = (
SELECT isErased
FROM attributes
JOIN notes USING(noteId)
WHERE attributeId = entityId
) WHERE entityName = 'attributes';
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM attributes WHERE attributeId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'attributes';
UPDATE mig_entity_changes SET isErased = (
SELECT isErased
FROM branches
JOIN notes USING(noteId)
WHERE branchId = entityId
) WHERE entityName = 'branches';
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM branches WHERE branchId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'branches';
UPDATE mig_entity_changes SET isErased = (
SELECT isErased
FROM note_revisions
WHERE noteRevisionId = entityId
) WHERE entityName = 'note_revisions';
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM note_revisions WHERE noteRevisionId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'note_revisions';
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateCreated FROM api_tokens WHERE apiTokenId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'api_tokens';
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM note_contents WHERE noteId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'note_contents';
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM note_revision_contents WHERE noteRevisionId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'note_revision_contents';
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM options WHERE name = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'options';
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateCreated FROM recent_notes WHERE noteId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'options';
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`
);
DELETE FROM attributes WHERE noteId IN (SELECT noteId FROM notes WHERE isErased = 1);
DELETE FROM branches WHERE noteId IN (SELECT noteId FROM notes WHERE isErased = 1);
DELETE FROM note_contents WHERE noteId IN (SELECT noteId FROM notes WHERE isErased = 1);
DELETE FROM note_revision_contents WHERE noteRevisionId IN (
SELECT noteRevisionId FROM note_revisions WHERE isErased = 1
);
DELETE FROM note_revisions WHERE isErased = 1;
DELETE FROM notes WHERE isErased = 1;
UPDATE entity_changes SET isErased = COALESCE((SELECT isErased FROM entity_changes AS sub WHERE sub.entityId = entity_changes.entityId AND sub.entityName = 'note_revisions'), 0) WHERE entityName = 'note_revision_contents';
@@ -0,0 +1,198 @@
CREATE TABLE IF NOT EXISTS "mig_entity_changes" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`hash` TEXT NOT NULL,
`isErased` INT NOT NULL,
`sourceId` TEXT NOT NULL,
`isSynced` INTEGER NOT NULL,
`utcDateChanged` TEXT NOT NULL
);
INSERT INTO mig_entity_changes (id, entityName, entityId, hash, sourceId, isSynced, isErased, utcDateChanged)
SELECT id, entityName, entityId, '', sourceId, isSynced, isErased, utcDateChanged FROM entity_changes;
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM api_tokens WHERE apiTokenId = entityId), '') WHERE entityName = 'api_tokens';
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM attributes WHERE attributeId = entityId), '') WHERE entityName = 'attributes';
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM branches WHERE branchId = entityId), '') WHERE entityName = 'branches';
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM notes WHERE noteId = entityId), '') WHERE entityName = 'notes';
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM note_contents WHERE noteId = entityId), '') WHERE entityName = 'note_contents';
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM note_revisions WHERE noteRevisionId = entityId), '') WHERE entityName = 'note_revisions';
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM note_revision_contents WHERE noteRevisionId = entityId), '') WHERE entityName = 'note_revision_contents';
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM options WHERE name = entityId), '') WHERE entityName = 'options';
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM recent_notes WHERE noteId = entityId), '') WHERE entityName = 'recent_notes';
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 TABLE IF NOT EXISTS "mig_api_tokens"
(
apiTokenId TEXT PRIMARY KEY NOT NULL,
token TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0);
INSERT INTO mig_api_tokens (apiTokenId, token, utcDateCreated, isDeleted)
SELECT apiTokenId, token, utcDateCreated, isDeleted FROM api_tokens;
DROP TABLE api_tokens;
ALTER TABLE mig_api_tokens RENAME TO api_tokens;
CREATE TABLE IF NOT EXISTS "mig_attributes"
(
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
utcDateCreated TEXT not null,
utcDateModified TEXT not null,
isDeleted INT not null,
`deleteId` TEXT DEFAULT NULL,
isInheritable int DEFAULT 0 NULL);
INSERT INTO mig_attributes (attributeId, noteId, type, name, value, position, utcDateCreated, utcDateModified, isDeleted, deleteId, isInheritable)
SELECT attributeId, noteId, type, name, value, position, utcDateCreated, utcDateModified, isDeleted, deleteId, isInheritable FROM attributes;
DROP TABLE attributes;
ALTER TABLE mig_attributes RENAME TO attributes;
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
on attributes (value);
CREATE TABLE IF NOT EXISTS "mig_branches" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`parentNoteId` TEXT NOT NULL,
`notePosition` INTEGER NOT NULL,
`prefix` TEXT,
`isExpanded` INTEGER NOT NULL DEFAULT 0,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`utcDateModified` TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
PRIMARY KEY(`branchId`));
INSERT INTO mig_branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, deleteId, utcDateModified, utcDateCreated)
SELECT branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, deleteId, utcDateModified, utcDateCreated FROM branches;
DROP TABLE branches;
ALTER TABLE mig_branches RENAME TO branches;
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
CREATE TABLE IF NOT EXISTS "mig_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',
`isDeleted` INT NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`));
INSERT INTO mig_notes (noteId, title, isProtected, type, mime, isDeleted, deleteId, dateCreated, dateModified, utcDateCreated, utcDateModified)
SELECT noteId, title, isProtected, type, mime, isDeleted, deleteId, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes;
DROP TABLE notes;
ALTER TABLE mig_notes 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 "mig_note_contents" (
`noteId` TEXT NOT NULL,
`content` TEXT NULL DEFAULT NULL,
`dateModified` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`)
);
INSERT INTO mig_note_contents (noteId, content, dateModified, utcDateModified)
SELECT noteId, content, dateModified, utcDateModified FROM note_contents;
DROP TABLE note_contents;
ALTER TABLE mig_note_contents RENAME TO note_contents;
CREATE TABLE IF NOT EXISTS "mig_note_revisions" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL,
type TEXT DEFAULT '' NOT NULL,
mime TEXT DEFAULT '' NOT NULL,
`title` TEXT,
`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);
INSERT INTO mig_note_revisions (noteRevisionId, noteId, type, mime, title, isProtected, utcDateLastEdited, utcDateCreated, utcDateModified, dateLastEdited, dateCreated)
SELECT noteRevisionId, noteId, type, mime, title, isProtected, utcDateLastEdited, utcDateCreated, utcDateModified, dateLastEdited, dateCreated FROM note_revisions;
DROP TABLE note_revisions;
ALTER TABLE mig_note_revisions 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`);
CREATE TABLE IF NOT EXISTS "mig_note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`content` TEXT,
`utcDateModified` TEXT NOT NULL);
INSERT INTO mig_note_revision_contents (noteRevisionId, content, utcDateModified)
SELECT noteRevisionId, content, utcDateModified FROM note_revision_contents;
DROP TABLE note_revision_contents;
ALTER TABLE mig_note_revision_contents RENAME TO note_revision_contents;
CREATE TABLE IF NOT EXISTS "mig_options"
(
name TEXT not null PRIMARY KEY,
value TEXT,
isSynced INTEGER default 0 not null,
utcDateCreated TEXT not null,
utcDateModified TEXT NOT NULL
);
INSERT INTO mig_options (name, value, isSynced, utcDateCreated, utcDateModified)
SELECT name, value, isSynced, utcDateCreated, utcDateModified FROM options;
DROP TABLE options;
ALTER TABLE mig_options RENAME TO options;
CREATE TABLE mig_recent_notes
(
noteId TEXT not null primary key,
notePath TEXT not null,
utcDateCreated TEXT not null,
isDeleted INT NOT NULL DEFAULT 0
);
INSERT INTO mig_recent_notes (noteId, notePath, utcDateCreated, isDeleted)
SELECT noteId, notePath, utcDateCreated, isDeleted FROM recent_notes;
DROP TABLE recent_notes;
ALTER TABLE mig_recent_notes RENAME TO recent_notes;
@@ -0,0 +1,2 @@
UPDATE options SET name = 'eraseEntitiesAfterTimeInSeconds' WHERE name = 'eraseNotesAfterTimeInSeconds';
UPDATE entity_changes SET entityId = 'eraseEntitiesAfterTimeInSeconds' WHERE entityName = 'options' AND entityId = 'eraseNotesAfterTimeInSeconds';
@@ -0,0 +1,25 @@
CREATE TABLE IF NOT EXISTS "mig_attributes"
(
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
utcDateModified TEXT not null,
isDeleted INT not null,
`deleteId` TEXT DEFAULT NULL,
isInheritable int DEFAULT 0 NULL);
INSERT INTO mig_attributes (attributeId, noteId, type, name, value, position, utcDateModified, isDeleted, deleteId, isInheritable)
SELECT attributeId, noteId, type, name, value, position, utcDateModified, isDeleted, deleteId, isInheritable FROM attributes;
DROP TABLE attributes;
ALTER TABLE mig_attributes RENAME TO attributes;
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
on attributes (value);
@@ -0,0 +1,5 @@
DELETE FROM options WHERE name = 'keyboardShortcutsCreateNoteIntoInbox'; -- in case there is already one which shouldn't (except for in-dev documents)
UPDATE options SET name = 'keyboardShortcutsCreateNoteIntoInbox' WHERE name = 'keyboardShortcutsCreateNoteIntoDayNote';
DELETE FROM options WHERE name = 'keyboardShortcutsShowAttributes';
DELETE FROM entity_changes WHERE entityName = 'options' AND entityId = 'keyboardShortcutsShowAttributes';
@@ -0,0 +1 @@
DROP INDEX IDX_notes_isDeleted;
+1
View File
@@ -0,0 +1 @@
VACUUM
@@ -0,0 +1,2 @@
UPDATE options SET name = 'keyboardShortcutsQuickSearch' WHERE name = 'keyboardShortcutsSearchNotes';
UPDATE entity_changes SET entityId = 'keyboardShortcutsQuickSearch' WHERE entityId = 'keyboardShortcutsSearchNotes' AND entityName = 'options';
@@ -0,0 +1,5 @@
UPDATE entity_changes SET isSynced = COALESCE((
SELECT options.isSynced
FROM options
WHERE options.name = entity_changes.entityId
), 0) WHERE entityName = 'options';
@@ -0,0 +1,14 @@
DELETE FROM entity_changes WHERE entityName = 'recent_notes';
CREATE TABLE IF NOT EXISTS "mig_recent_notes"
(
noteId TEXT not null primary key,
notePath TEXT not null,
utcDateCreated TEXT not null
);
INSERT INTO mig_recent_notes (noteId, notePath, utcDateCreated)
SELECT noteId, notePath, utcDateCreated FROM recent_notes;
DROP TABLE recent_notes;
ALTER TABLE mig_recent_notes RENAME TO recent_notes;
+69 -83
View File
@@ -1,31 +1,19 @@
CREATE TABLE IF NOT EXISTS "entity_changes" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`hash` TEXT NOT NULL,
`isErased` INT NOT NULL,
`sourceId` TEXT NOT NULL,
`isSynced` INTEGER NOT NULL,
`utcDateChanged` TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "api_tokens"
(
apiTokenId TEXT PRIMARY KEY NOT NULL,
token TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0,
hash TEXT DEFAULT "" NOT NULL);
CREATE TABLE IF NOT EXISTS "options"
(
name TEXT not null PRIMARY KEY,
value TEXT,
isSynced INTEGER default 0 not null,
hash TEXT default "" not null,
utcDateCreated TEXT not null,
utcDateModified TEXT NOT NULL
);
CREATE TABLE recent_notes
(
noteId TEXT not null primary key,
notePath TEXT not null,
hash TEXT default "" not null,
utcDateCreated TEXT not null,
isDeleted INT
);
CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`content` TEXT,
hash TEXT DEFAULT '' NOT NULL,
`utcDateModified` TEXT NOT NULL);
apiTokenId TEXT PRIMARY KEY NOT NULL,
token TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0);
CREATE TABLE IF NOT EXISTS "branches" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
@@ -37,10 +25,49 @@ CREATE TABLE IF NOT EXISTS "branches" (
`deleteId` TEXT DEFAULT NULL,
`utcDateModified` TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`branchId`));
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
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',
`isDeleted` INT NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`));
CREATE TABLE IF NOT EXISTS "note_contents" (
`noteId` TEXT NOT NULL,
`content` TEXT NULL DEFAULT NULL,
`dateModified` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`)
);
CREATE TABLE IF NOT EXISTS "note_revisions" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL,
type TEXT DEFAULT '' NOT NULL,
mime TEXT DEFAULT '' NOT NULL,
`title` TEXT,
`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);
CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`content` TEXT,
`utcDateModified` TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS "options"
(
name TEXT not null PRIMARY KEY,
value TEXT,
isSynced INTEGER default 0 not null,
utcDateCreated TEXT not null,
utcDateModified TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "attributes"
(
attributeId TEXT not null primary key,
@@ -49,77 +76,36 @@ CREATE TABLE IF NOT EXISTS "attributes"
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
utcDateCreated TEXT not null,
utcDateModified TEXT not null,
isDeleted INT not null,
`deleteId` TEXT DEFAULT NULL,
hash TEXT default "" not null,
isInheritable int DEFAULT 0 NULL);
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
on attributes (value);
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_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_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
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`)
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
on attributes (value);
CREATE TABLE IF NOT EXISTS "recent_notes"
(
noteId TEXT not null primary key,
notePath TEXT not null,
utcDateCreated TEXT not null
);
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
+24 -23
View File
@@ -5,8 +5,8 @@
}
/*
* CKEditor 5 (v23.1.0) content styles.
* Generated on Thu, 29 Oct 2020 12:17:48 GMT.
* CKEditor 5 (v24.0.0) content styles.
* Generated on Thu, 10 Dec 2020 08:15:26 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
*/
@@ -23,6 +23,20 @@
--ck-todo-list-checkmark-size: 16px;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized img {
width: 100%;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized > figcaption {
display: block;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side {
float: right;
@@ -69,20 +83,6 @@
font-size: .75em;
outline-offset: -1px;
}
/* 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-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
@@ -215,6 +215,13 @@
-ms-user-select: none;
user-select: none;
}
/* ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 1em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list {
list-style: none;
@@ -282,17 +289,11 @@
.ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 1em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-html-embed/theme/htmlembed.css */
.ck-content .raw-html-embed {
margin: 1em auto;
min-width: 15em;
font-style: normal;
}
/* ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
+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
+1 -1
View File
File diff suppressed because one or more lines are too long
+202 -92
View File
@@ -1,6 +1,6 @@
/*! jQuery Fancytree Plugin - 2.37.0 - 2020-09-11T18:58:08Z
/*! jQuery Fancytree Plugin - 2.38.0 - 2021-02-09T20:03:49Z
* https://github.com/mar10/fancytree
* Copyright (c) 2020 Martin Wendt; Licensed MIT
* Copyright (c) 2021 Martin Wendt; Licensed MIT
*/
/*! jQuery UI - v1.12.1 - 2018-05-20
* http://jqueryui.com
@@ -1361,12 +1361,12 @@ var uniqueId = $.fn.extend( {
* Tree view control with support for lazy loading and much more.
* https://github.com/mar10/fancytree/
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
/** Core Fancytree module.
@@ -1530,8 +1530,16 @@ var uniqueId = $.fn.extend( {
// TODO: see qunit.js extractStacktrace()
if (!cond) {
msg = msg ? ": " + msg : "";
msg = "Fancytree assertion failed" + msg;
// consoleApply("assert", [!!cond, msg]);
$.error("Fancytree assertion failed" + msg);
// #1041: Raised exceptions may not be visible in the browser
// console if inside promise chains, so we also print directly:
$.ui.fancytree.error(msg);
// Throw exception:
$.error(msg);
}
}
@@ -5259,6 +5267,13 @@ var uniqueId = $.fn.extend( {
// visit siblings
siblings = parent.children;
nextIdx = siblings.indexOf(node) + siblingOfs;
_assert(
nextIdx >= 0,
"Could not find " +
node +
" in parent's children: " +
parent
);
for (i = nextIdx; i < siblings.length; i++) {
node = siblings[i];
@@ -6660,7 +6675,17 @@ var uniqueId = $.fn.extend( {
if (isActive === flag) {
// Nothing to do
return _getResolvedPromise(node);
} else if (
}
// #1042: don't scroll between mousedown/-up when clicking an embedded link
if (
scroll &&
ctx.originalEvent &&
$(ctx.originalEvent.target).is("a,:checkbox")
) {
node.info("Not scrolling while clicking an embedded link.");
scroll = false;
}
if (
flag &&
!noEvents &&
this._triggerNodeEvent(
@@ -8070,7 +8095,7 @@ var uniqueId = $.fn.extend( {
{
/** Version number `"MAJOR.MINOR.PATCH"`
* @type {string} */
version: "2.37.0", // Set to semver by 'grunt release'
version: "2.38.0", // Set to semver by 'grunt release'
/** @type {string}
* @description `"production" for release builds` */
buildType: "production", // Set to 'production' by 'grunt build'
@@ -8700,13 +8725,13 @@ var uniqueId = $.fn.extend( {
* Add a child counter bubble to tree nodes.
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
// To keep the global namespace clean, we wrap everything in a closure.
@@ -8825,7 +8850,7 @@ var uniqueId = $.fn.extend( {
// Every extension must be registered by a unique name.
name: "childcounter",
// Version information should be compliant with [semver](http://semver.org)
version: "2.37.0",
version: "2.38.0",
// Extension specific options and their defaults.
// This options will be available as `tree.options.childcounter.hideExpanded`
@@ -8931,13 +8956,13 @@ var uniqueId = $.fn.extend( {
* Support faster lookup of nodes by key and shared ref-ids.
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
(function(factory) {
@@ -8958,13 +8983,8 @@ var uniqueId = $.fn.extend( {
/*******************************************************************************
* Private functions and variables
*/
function _assert(cond, msg) {
// TODO: see qunit.js extractStacktrace()
if (!cond) {
msg = msg ? ": " + msg : "";
$.error("Assertion failed" + msg);
}
}
var _assert = $.ui.fancytree.assert;
/* Return first occurrence of member from array. */
function _removeArrayMember(arr, elem) {
@@ -9295,7 +9315,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "clones",
version: "2.37.0",
version: "2.38.0",
// Default options for this extension.
options: {
highlightActiveClones: true, // set 'fancytree-active-clone' on active clones and all peers
@@ -9452,13 +9472,13 @@ var uniqueId = $.fn.extend( {
* Drag-and-drop support (native HTML5).
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
/*
@@ -10056,7 +10076,7 @@ var uniqueId = $.fn.extend( {
// data store list of items representing dragged data can be
// enumerated, but the data itself is unavailable and no new
// data can be added.
var nodeData = node.toDict();
var nodeData = node.toDict(true, dndOpts.sourceCopyHook);
nodeData.treeId = node.tree._id;
json = JSON.stringify(nodeData);
try {
@@ -10476,7 +10496,7 @@ var uniqueId = $.fn.extend( {
$.ui.fancytree.registerExtension({
name: "dnd5",
version: "2.37.0",
version: "2.38.0",
// Default options for this extension.
options: {
autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering
@@ -10498,6 +10518,7 @@ var uniqueId = $.fn.extend( {
scrollSensitivity: 20, // Active top/bottom margin in pixel
scrollSpeed: 5, // Pixel per event
setTextTypeJson: false, // Allow dragging of nodes to different IE windows
sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38
// Events (drag support)
dragStart: null, // Callback(sourceNode, data), return true, to enable dnd drag
dragDrag: $.noop, // Callback(sourceNode, data)
@@ -10607,13 +10628,13 @@ var uniqueId = $.fn.extend( {
* Make node titles editable.
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
(function(factory) {
@@ -10905,7 +10926,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "edit",
version: "2.37.0",
version: "2.38.0",
// Default options for this extension.
options: {
adjustWidthOfs: 4, // null: don't adjust input size to content
@@ -11011,13 +11032,13 @@ var uniqueId = $.fn.extend( {
* Remove or highlight tree nodes, based on a filter.
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
(function(factory) {
@@ -11040,8 +11061,9 @@ var uniqueId = $.fn.extend( {
*/
var KeyNoData = "__not_found__",
escapeHtml = $.ui.fancytree.escapeHtml;
escapeHtml = $.ui.fancytree.escapeHtml,
exoticStartChar = "\uFFF7",
exoticEndChar = "\uFFF8";
function _escapeRegex(str) {
return (str + "").replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
}
@@ -11055,6 +11077,47 @@ var uniqueId = $.fn.extend( {
return s;
}
/**
* @description Marks the matching charecters of `text` either by `mark` or
* by exotic*Chars (if `escapeTitles` is `true`) based on `regexMatchArray`
* which is an array of matching groups.
* @param {string} text
* @param {RegExpMatchArray} regexMatchArray
*/
function _markFuzzyMatchedChars(text, regexMatchArray, escapeTitles) {
// It is extremely infuriating that we can not use `let` or `const` or arrow functions.
// Damn you IE!!!
var matchingIndices = [];
// get the indices of matched characters (Iterate through `RegExpMatchArray`)
for (
var _matchingArrIdx = 1;
_matchingArrIdx < regexMatchArray.length;
_matchingArrIdx++
) {
var _mIdx =
// get matching char index by cumulatively adding
// the matched group length
regexMatchArray[_matchingArrIdx].length +
(_matchingArrIdx === 1 ? 0 : 1) +
(matchingIndices[matchingIndices.length - 1] || 0);
matchingIndices.push(_mIdx);
}
// Map each `text` char to its position and store in `textPoses`.
var textPoses = text.split("");
if (escapeTitles) {
// If escaping the title, then wrap the matchng char within exotic chars
matchingIndices.forEach(function(v) {
textPoses[v] = exoticStartChar + textPoses[v] + exoticEndChar;
});
} else {
// Otherwise, Wrap the matching chars within `mark`.
matchingIndices.forEach(function(v) {
textPoses[v] = "<mark>" + textPoses[v] + "</mark>";
});
}
// Join back the modified `textPoses` to create final highlight markup.
return textPoses.join("");
}
$.ui.fancytree._FancytreeClass.prototype._applyFilterImpl = function(
filter,
branchMode,
@@ -11064,6 +11127,8 @@ var uniqueId = $.fn.extend( {
statusNode,
re,
reHighlight,
reExoticStartChar,
reExoticEndChar,
temp,
prevEnableUpdate,
count = 0,
@@ -11087,14 +11152,29 @@ var uniqueId = $.fn.extend( {
// See https://codereview.stackexchange.com/questions/23899/faster-javascript-fuzzy-string-matching-function/23905#23905
// and http://www.quora.com/How-is-the-fuzzy-search-algorithm-in-Sublime-Text-designed
// and http://www.dustindiaz.com/autocomplete-fuzzy-matching
match = filter.split("").reduce(function(a, b) {
return a + "[^" + b + "]*" + b;
});
match = filter
.split("")
// Escaping the `filter` will not work because,
// it gets further split into individual characters. So,
// escape each character after splitting
.map(_escapeRegex)
.reduce(function(a, b) {
// create capture groups for parts that comes before
// the character
return a + "([^" + b + "]*)" + b;
}, "");
} else {
match = _escapeRegex(filter); // make sure a '.' is treated literally
}
re = new RegExp(".*" + match + ".*", "i");
re = new RegExp(match, "i");
reHighlight = new RegExp(_escapeRegex(filter), "gi");
if (escapeTitles) {
reExoticStartChar = new RegExp(
_escapeRegex(exoticStartChar),
"g"
);
reExoticEndChar = new RegExp(_escapeRegex(exoticEndChar), "g");
}
filter = function(node) {
if (!node.title) {
return false;
@@ -11102,31 +11182,46 @@ var uniqueId = $.fn.extend( {
var text = escapeTitles
? node.title
: extractHtmlText(node.title),
res = !!re.test(text);
// `.match` instead of `.test` to get the capture groups
res = text.match(re);
if (res && opts.highlight) {
if (escapeTitles) {
// #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
// Use some exotic characters to mark matches:
temp = text.replace(reHighlight, function(s) {
return "\uFFF7" + s + "\uFFF8";
});
if (opts.fuzzy) {
temp = _markFuzzyMatchedChars(
text,
res,
escapeTitles
);
} else {
// #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
// Use some exotic characters to mark matches:
temp = text.replace(reHighlight, function(s) {
return exoticStartChar + s + exoticEndChar;
});
}
// now we can escape the title...
node.titleWithHighlight = escapeHtml(temp)
// ... and finally insert the desired `<mark>` tags
.replace(/\uFFF7/g, "<mark>")
.replace(/\uFFF8/g, "</mark>");
.replace(reExoticStartChar, "<mark>")
.replace(reExoticEndChar, "</mark>");
} else {
node.titleWithHighlight = text.replace(
reHighlight,
function(s) {
return "<mark>" + s + "</mark>";
}
);
if (opts.fuzzy) {
node.titleWithHighlight = _markFuzzyMatchedChars(
text,
res
);
} else {
node.titleWithHighlight = text.replace(
reHighlight,
function(s) {
return "<mark>" + s + "</mark>";
}
);
}
}
// node.debug("filter", escapeTitles, text, node.titleWithHighlight);
}
return res;
return !!res;
};
}
@@ -11264,6 +11359,26 @@ var uniqueId = $.fn.extend( {
return this._applyFilterImpl(filter, true, opts);
};
/**
* [ext-filter] Re-apply current filter.
*
* @returns {integer} count
* @alias Fancytree#updateFilter
* @requires jquery.fancytree.filter.js
* @since 2.38
*/
$.ui.fancytree._FancytreeClass.prototype.updateFilter = function() {
if (
this.enableFilter &&
this.lastFilterArgs &&
this.options.filter.autoApply
) {
this._applyFilterImpl.apply(this, this.lastFilterArgs);
} else {
this.warn("updateFilter(): no filter active.");
}
};
/**
* [ext-filter] Reset the filter.
*
@@ -11355,7 +11470,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "filter",
version: "2.37.0",
version: "2.38.0",
// Default options for this extension.
options: {
autoApply: true, // Re-apply last filter if lazy data is loaded
@@ -11470,13 +11585,13 @@ var uniqueId = $.fn.extend( {
* Use glyph-fonts, ligature-fonts, or SVG icons instead of icon sprites.
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
(function(factory) {
@@ -11678,7 +11793,7 @@ var uniqueId = $.fn.extend( {
$.ui.fancytree.registerExtension({
name: "glyph",
version: "2.37.0",
version: "2.38.0",
// Default options for this extension.
options: {
preset: null, // 'awesome3', 'awesome4', 'bootstrap3', 'material'
@@ -11828,13 +11943,13 @@ var uniqueId = $.fn.extend( {
* Support keyboard navigation for trees with embedded input controls.
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
(function(factory) {
@@ -11951,7 +12066,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "gridnav",
version: "2.37.0",
version: "2.38.0",
// Default options for this extension.
options: {
autofocusInput: false, // Focus first embedded input if node gets activated
@@ -12053,13 +12168,13 @@ var uniqueId = $.fn.extend( {
* Allow multiple selection of nodes by mouse or keyboard.
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
(function(factory) {
@@ -12088,7 +12203,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "multi",
version: "2.37.0",
version: "2.38.0",
// Default options for this extension.
options: {
allowNoSelect: false, //
@@ -12185,13 +12300,13 @@ var uniqueId = $.fn.extend( {
*
* @depends: js-cookie or jquery-cookie
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
(function(factory) {
@@ -12402,7 +12517,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "persist",
version: "2.37.0",
version: "2.38.0",
// Default options for this extension.
options: {
cookieDelimiter: "~",
@@ -12690,13 +12805,13 @@ var uniqueId = $.fn.extend( {
* Render tree as table (aka 'tree grid', 'table tree').
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
(function(factory) {
@@ -12717,12 +12832,7 @@ var uniqueId = $.fn.extend( {
/******************************************************************************
* Private functions and variables
*/
function _assert(cond, msg) {
msg = msg || "";
if (!cond) {
$.error("Assertion failed " + msg);
}
}
var _assert = $.ui.fancytree.assert;
function insertFirstChild(referenceNode, newNode) {
referenceNode.insertBefore(newNode, referenceNode.firstChild);
@@ -12779,7 +12889,7 @@ var uniqueId = $.fn.extend( {
$.ui.fancytree.registerExtension({
name: "table",
version: "2.37.0",
version: "2.38.0",
// Default options for this extension.
options: {
checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx)
@@ -13240,13 +13350,13 @@ var uniqueId = $.fn.extend( {
*
* @see http://jqueryui.com/themeroller/
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
(function(factory) {
@@ -13269,7 +13379,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "themeroller",
version: "2.37.0",
version: "2.38.0",
// Default options for this extension.
options: {
activeClass: "ui-state-active", // Class added to active node
@@ -13360,13 +13470,13 @@ var uniqueId = $.fn.extend( {
* Support for 100% wide selection bars.
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
*/
(function(factory) {
@@ -13496,7 +13606,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "wide",
version: "2.37.0",
version: "2.38.0",
// Default options for this extension.
options: {
iconWidth: null, // Adjust this if @fancy-icon-width != "16px"
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-2020, Martin Wendt (https://wwWendt.de)
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.37.0
* @date 2020-09-11T18:58:08Z
* @version 2.38.0
* @date 2021-02-09T20:03:49Z
******************************************************************************/
/*------------------------------------------------------------------------------
* Helpers
+581 -545
View File
File diff suppressed because it is too large Load Diff
+27 -26
View File
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.45.9",
"version": "0.46.0-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -24,45 +24,46 @@
"test-all": "npm run test && npm run test-es6"
},
"dependencies": {
"async-mutex": "0.2.4",
"axios": "0.20.0",
"better-sqlite3": "7.1.1",
"async-mutex": "0.3.0",
"axios": "0.21.1",
"better-sqlite3": "7.1.2",
"body-parser": "1.19.0",
"cls-hooked": "4.2.2",
"commonmark": "0.29.2",
"commonmark": "0.29.3",
"cookie-parser": "1.4.5",
"csurf": "1.11.0",
"dayjs": "1.9.3",
"ejs": "3.1.5",
"electron-debug": "3.1.0",
"electron-dl": "3.0.2",
"dayjs": "1.10.4",
"ejs": "3.1.6",
"electron-debug": "3.2.0",
"electron-dl": "3.0.3",
"electron-find": "1.0.6",
"electron-window-state": "5.0.3",
"express": "4.17.1",
"express-session": "1.17.1",
"fs-extra": "9.0.1",
"helmet": "4.1.1",
"fs-extra": "9.1.0",
"helmet": "4.4.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",
"ini": "1.3.5",
"ini": "2.0.0",
"is-animated": "^2.0.1",
"is-svg": "4.2.1",
"jimp": "0.16.1",
"jsdom": "^16.4.0",
"mime-types": "2.1.27",
"mime-types": "2.1.28",
"multer": "1.4.2",
"node-abi": "2.19.1",
"open": "7.3.0",
"node-abi": "2.19.3",
"open": "7.4.1",
"portscanner": "2.2.0",
"rand-token": "1.0.1",
"request": "^2.88.2",
"rimraf": "3.0.2",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.1.1",
"sanitize-html": "2.3.2",
"sax": "1.2.4",
"semver": "7.3.2",
"semver": "7.3.4",
"serve-favicon": "2.5.0",
"session-file-store": "1.5.0",
"stream-throttle": "^0.1.3",
@@ -71,23 +72,23 @@
"turndown": "7.0.0",
"turndown-plugin-gfm": "1.0.2",
"unescape": "1.0.1",
"ws": "7.3.1",
"ws": "7.4.3",
"yauzl": "2.10.0",
"yazl": "2.5.1"
},
"devDependencies": {
"cross-env": "7.0.2",
"electron": "9.3.5",
"cross-env": "7.0.3",
"electron": "9.4.3",
"electron-builder": "22.9.1",
"electron-packager": "15.1.0",
"electron-rebuild": "2.3.2",
"electron-packager": "15.2.0",
"electron-rebuild": "2.3.5",
"esm": "3.2.25",
"jasmine": "3.6.2",
"jasmine": "3.6.4",
"jsdoc": "3.6.6",
"lorem-ipsum": "2.0.3",
"rcedit": "2.2.0",
"webpack": "5.1.3",
"webpack-cli": "4.1.0"
"rcedit": "3.0.0",
"webpack": "5.21.2",
"webpack-cli": "4.5.0"
},
"optionalDependencies": {
"electron-installer-debian": "2.0.1"
+2 -1
View File
@@ -46,7 +46,8 @@ class NoteBuilder {
branchId: id(),
noteId: childNoteBuilder.note.noteId,
parentNoteId: this.note.noteId,
prefix
prefix,
notePosition: 10
});
return this;
+15 -3
View File
@@ -12,8 +12,8 @@ describe("Search", () => {
beforeEach(() => {
noteCache.reset();
rootNote = new NoteBuilder(new Note(noteCache, {noteId: 'root', title: 'root'}));
new Branch(noteCache, {branchId: 'root', noteId: 'root', parentNoteId: 'none'});
rootNote = new NoteBuilder(new Note(noteCache, {noteId: 'root', title: 'root', type: 'text'}));
new Branch(noteCache, {branchId: 'root', noteId: 'root', parentNoteId: 'none', notePosition: 10});
});
it("simple path match", () => {
@@ -113,6 +113,15 @@ describe("Search", () => {
let searchResults = searchService.findNotesWithQuery('#capital=Vienna', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
// case sensitivity:
searchResults = searchService.findNotesWithQuery('#CAPITAL=VIENNA', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('#caPItal=vienNa', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("label comparison with full syntax", () => {
@@ -472,14 +481,17 @@ describe("Search", () => {
expect(searchResults.length).toEqual(expectedResultCount);
}
test("type", "text", 6);
test("type", "text", 7);
test("TYPE", "TEXT", 7);
test("type", "code", 0);
test("mime", "text/html", 6);
test("mime", "application/json", 0);
test("isProtected", "false", 7);
test("isProtected", "FALSE", 7);
test("isProtected", "true", 0);
test("isProtected", "TRUE", 0);
test("dateCreated", "'2020-05-14 12:11:42.001+0200'", 1);
test("dateCreated", "wrong", 0);
+4 -4
View File
@@ -24,12 +24,12 @@ describe("Value extractor", () => {
let valueExtractor = new ValueExtractor(["note", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria)).toEqual("vienna");
expect(valueExtractor.extract(austria)).toEqual("Vienna");
valueExtractor = new ValueExtractor(["#capital"]);
expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria)).toEqual("vienna");
expect(valueExtractor.extract(austria)).toEqual("Vienna");
});
it("parent/child property extraction", async () => {
@@ -59,12 +59,12 @@ describe("Value extractor", () => {
let valueExtractor = new ValueExtractor(["note", "relations", "neighbor", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria.note)).toEqual("prague");
expect(valueExtractor.extract(austria.note)).toEqual("Prague");
valueExtractor = new ValueExtractor(["~neighbor", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria.note)).toEqual("prague");
expect(valueExtractor.extract(austria.note)).toEqual("Prague");
});
});
+2 -1
View File
@@ -10,7 +10,6 @@ const FileStore = require('session-file-store')(session);
const sessionSecret = require('./services/session_secret');
const dataDir = require('./services/data_dir');
require('./services/handlers');
require('./services/hoisted_note_loader');
require('./services/note_cache/note_cache_loader');
const app = express();
@@ -24,6 +23,7 @@ app.use(helmet({
contentSecurityPolicy: false
}));
app.use(bodyParser.text({limit: '500mb'}));
app.use(bodyParser.json({limit: '500mb'}));
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
@@ -39,6 +39,7 @@ const sessionParser = session({
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // in milliseconds
},
name: 'trilium.sid',
store: new FileStore({
ttl: 30 * 24 * 3600,
path: dataDir.TRILIUM_DATA_DIR + '/sessions'
+2 -10
View File
@@ -18,7 +18,6 @@ const promotedAttributeDefinitionParser = require("../services/promoted_attribut
* @property {boolean} isInheritable - immutable
* @property {boolean} isDeleted
* @property {string|null} deleteId - ID identifying delete transaction
* @property {string} utcDateCreated
* @property {string} utcDateModified
*
* @extends Entity
@@ -26,7 +25,7 @@ const promotedAttributeDefinitionParser = require("../services/promoted_attribut
class Attribute extends Entity {
static get entityName() { return "attributes"; }
static get primaryKeyName() { return "attributeId"; }
static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable", "isDeleted", "utcDateCreated"]; }
static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable", "isDeleted"]; }
constructor(row) {
super(row);
@@ -103,15 +102,9 @@ class Attribute extends Entity {
this.isDeleted = false;
}
if (!this.utcDateCreated) {
this.utcDateCreated = dateUtils.utcNowDateTime();
}
super.beforeSaving();
if (this.isChanged) {
this.utcDateModified = dateUtils.utcNowDateTime();
}
this.utcDateModified = dateUtils.utcNowDateTime();
}
createClone(type, name, value, isInheritable) {
@@ -123,7 +116,6 @@ class Attribute extends Entity {
position: this.position,
isInheritable: isInheritable,
isDeleted: false,
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified
});
}
+1 -3
View File
@@ -57,9 +57,7 @@ class Branch extends Entity {
super.beforeSaving();
if (this.isChanged) {
this.utcDateModified = dateUtils.utcNowDateTime();
}
this.utcDateModified = dateUtils.utcNowDateTime();
}
createClone(parentNoteId, notePosition) {
+5 -6
View File
@@ -15,18 +15,13 @@ class Entity {
}
}
if ('isDeleted' in this) {
if ('isDeleted' in this && this.constructor.entityName !== 'recent_notes') {
this.isDeleted = !!this.isDeleted;
}
}
beforeSaving() {
this.generateIdIfNecessary();
const origHash = this.hash;
this.hash = this.generateHash();
this.isChanged = origHash !== this.hash;
}
generateIdIfNecessary() {
@@ -45,6 +40,10 @@ class Entity {
return utils.hash(contentToHash).substr(0, 10);
}
getUtcDateChanged() {
return this.utcDateModified;
}
get repository() {
if (!repo) {
repo = require('../services/repository');
+16 -12
View File
@@ -21,7 +21,6 @@ const RELATION = 'relation';
* @property {boolean} isProtected - true if note is protected
* @property {boolean} isDeleted - true if note is deleted
* @property {string|null} deleteId - ID identifying delete transaction
* @property {boolean} isErased - true if note's content is erased after it has been deleted
* @property {string} dateCreated - local date time (with offset)
* @property {string} dateModified - local date time (with offset)
* @property {string} utcDateCreated
@@ -70,9 +69,9 @@ class Note extends Entity {
/** @returns {*} */
getContent(silentNotFoundError = false) {
if (this.content === undefined) {
const res = sql.getRow(`SELECT content, hash FROM note_contents WHERE noteId = ?`, [this.noteId]);
const row = sql.getRow(`SELECT content FROM note_contents WHERE noteId = ?`, [this.noteId]);
if (!res) {
if (!row) {
if (silentNotFoundError) {
return undefined;
}
@@ -81,7 +80,7 @@ class Note extends Entity {
}
}
this.content = res.content;
this.content = row.content;
if (this.isProtected) {
if (this.isContentAvailable) {
@@ -143,8 +142,7 @@ class Note extends Entity {
noteId: this.noteId,
content: content,
dateModified: dateUtils.localNowDateTime(),
utcDateModified: dateUtils.utcNowDateTime(),
hash: utils.hash(this.noteId + "|" + content.toString())
utcDateModified: dateUtils.utcNowDateTime()
};
if (this.isProtected) {
@@ -158,7 +156,15 @@ class Note extends Entity {
sql.upsert("note_contents", "noteId", pojo);
entityChangesService.addNoteContentEntityChange(this.noteId);
const hash = utils.hash(this.noteId + "|" + content.toString());
entityChangesService.addEntityChange({
entityName: 'note_contents',
entityId: this.noteId,
hash: hash,
isErased: false,
utcDateChanged: this.getUtcDateChanged()
}, null);
}
setJsonContent(content) {
@@ -806,7 +812,7 @@ class Note extends Entity {
* @returns {boolean} - true if note has children
*/
hasChildren() {
return (this.getChildNotes()).length > 0;
return this.getChildNotes().length > 0;
}
/**
@@ -904,10 +910,8 @@ class Note extends Entity {
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.localNowDateTime();
this.utcDateModified = dateUtils.utcNowDateTime();
}
this.dateModified = dateUtils.localNowDateTime();
this.utcDateModified = dateUtils.utcNowDateTime();
}
// cannot be static!
+13 -11
View File
@@ -15,7 +15,6 @@ const entityChangesService = require('../services/entity_changes.js');
* @property {string} type
* @property {string} mime
* @property {string} title
* @property {boolean} isErased
* @property {boolean} isProtected
* @property {string} dateLastEdited
* @property {string} dateCreated
@@ -28,12 +27,11 @@ const entityChangesService = require('../services/entity_changes.js');
class NoteRevision extends Entity {
static get entityName() { return "note_revisions"; }
static get primaryKeyName() { return "noteRevisionId"; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "isErased", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }
constructor(row) {
super(row);
this.isErased = !!this.isErased;
this.isProtected = !!this.isProtected;
if (this.isProtected) {
@@ -67,7 +65,7 @@ class NoteRevision extends Entity {
/** @returns {*} */
getContent(silentNotFoundError = false) {
if (this.content === undefined) {
const res = sql.getRow(`SELECT content, hash FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]);
const res = sql.getRow(`SELECT content FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]);
if (!res) {
if (silentNotFoundError) {
@@ -79,7 +77,6 @@ class NoteRevision extends Entity {
}
this.content = res.content;
if (this.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
this.content = protectedSessionService.decrypt(this.content);
@@ -106,8 +103,7 @@ class NoteRevision extends Entity {
const pojo = {
noteRevisionId: this.noteRevisionId,
content: content,
utcDateModified: dateUtils.utcNowDateTime(),
hash: utils.hash(this.noteRevisionId + "|" + content)
utcDateModified: dateUtils.utcNowDateTime()
};
if (this.isProtected) {
@@ -121,15 +117,21 @@ class NoteRevision extends Entity {
sql.upsert("note_revision_contents", "noteRevisionId", pojo);
entityChangesService.addNoteRevisionContentEntityChange(this.noteRevisionId);
const hash = utils.hash(this.noteRevisionId + "|" + content);
entityChangesService.addEntityChange({
entityName: 'note_revision_contents',
entityId: this.noteRevisionId,
hash: hash,
isErased: false,
utcDateChanged: this.getUtcDateChanged()
}, null);
}
beforeSaving() {
super.beforeSaving();
if (this.isChanged) {
this.utcDateModified = dateUtils.utcNowDateTime();
}
this.utcDateModified = dateUtils.utcNowDateTime();
}
// cannot be static!
+2 -4
View File
@@ -32,10 +32,8 @@ class Option extends Entity {
super.beforeSaving();
if (this.isChanged) {
this.utcDateModified = dateUtils.utcNowDateTime();
}
this.utcDateModified = dateUtils.utcNowDateTime();
}
}
module.exports = Option;
module.exports = Option;
+2 -8
View File
@@ -8,21 +8,15 @@ const dateUtils = require('../services/date_utils');
*
* @property {string} noteId
* @property {string} notePath
* @property {boolean} isDeleted
* @property {string} utcDateModified
* @property {string} utcDateCreated
*
* @extends Entity
*/
class RecentNote extends Entity {
static get entityName() { return "recent_notes"; }
static get primaryKeyName() { return "noteId"; }
static get hashedProperties() { return ["noteId", "notePath", "utcDateCreated", "isDeleted"]; }
beforeSaving() {
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.utcDateCreated) {
this.utcDateCreated = dateUtils.utcNowDateTime();
}
@@ -31,4 +25,4 @@ class RecentNote extends Entity {
}
}
module.exports = RecentNote;
module.exports = RecentNote;
+54 -19
View File
@@ -7,17 +7,18 @@ const $form = $("#add-link-form");
const $autoComplete = $("#add-link-note-autocomplete");
const $linkTitle = $("#link-title");
const $addLinkTitleSettings = $("#add-link-title-settings");
const $addLinkTitleRadios = $(".add-link-title-radios");
const $addLinkTitleFormGroup = $("#add-link-title-form-group");
/** @var TextTypeWidget */
let textTypeWidget;
export async function showDialog(widget) {
export async function showDialog(widget, text = '') {
textTypeWidget = widget;
$addLinkTitleSettings.toggle(!textTypeWidget.hasSelection());
$addLinkTitleSettings.find('input[type=radio]').on('change', updateTitleFormGroupVisibility);
$addLinkTitleSettings.find('input[type=radio]').on('change', updateTitleSettingsVisibility);
// with selection hyper link is implied
if (textTypeWidget.hasSelection()) {
@@ -27,11 +28,11 @@ export async function showDialog(widget) {
$addLinkTitleSettings.find("input[value='reference-link']").prop("checked", true);
}
updateTitleFormGroupVisibility();
updateTitleSettingsVisibility();
utils.openDialog($dialog);
$autoComplete.val('').trigger('focus');
$autoComplete.val('');
$linkTitle.val('');
async function setDefaultLinkTitle(noteId) {
@@ -40,13 +41,15 @@ export async function showDialog(widget) {
$linkTitle.val(noteTitle);
}
noteAutocompleteService.initNoteAutocomplete($autoComplete);
noteAutocompleteService.initNoteAutocomplete($autoComplete, { allowExternalLinks: true });
$autoComplete.on('autocomplete:noteselected', function(event, suggestion, dataset) {
$autoComplete.on('autocomplete:noteselected', (event, suggestion, dataset) => {
if (!suggestion.notePath) {
return false;
}
updateTitleSettingsVisibility();
const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath);
if (noteId) {
@@ -54,39 +57,71 @@ export async function showDialog(widget) {
}
});
$autoComplete.on('autocomplete:cursorchanged', function(event, suggestion, dataset) {
const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath);
$autoComplete.on('autocomplete:externallinkselected', (event, suggestion, dataset) => {
if (!suggestion.externalLink) {
return false;
}
if (noteId) {
setDefaultLinkTitle(noteId);
updateTitleSettingsVisibility();
$linkTitle.val(suggestion.externalLink);
});
$autoComplete.on('autocomplete:cursorchanged', function(event, suggestion, dataset) {
if (suggestion.externalLink) {
$linkTitle.val(suggestion.externalLink)
}
else {
const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath);
if (noteId) {
setDefaultLinkTitle(noteId);
}
}
});
noteAutocompleteService.showRecentNotes($autoComplete);
if (text && text.trim()) {
noteAutocompleteService.setText($autoComplete, text);
}
else {
noteAutocompleteService.showRecentNotes($autoComplete);
}
$autoComplete
.trigger('focus')
.trigger('select'); // to be able to quickly remove entered text
}
function getLinkType() {
if ($autoComplete.getSelectedExternalLink()) {
return 'external-link';
}
return $addLinkTitleSettings.find('input[type=radio]:checked').val();
}
function updateTitleFormGroupVisibility() {
const visible = getLinkType() === 'hyper-link';
function updateTitleSettingsVisibility() {
const linkType = getLinkType();
$addLinkTitleFormGroup.toggle(visible);
$addLinkTitleFormGroup.toggle(linkType !== 'reference-link');
$addLinkTitleRadios.toggle(linkType !== 'external-link')
}
$form.on('submit', () => {
const notePath = $autoComplete.getSelectedNotePath();
if (notePath) {
if ($autoComplete.getSelectedNotePath()) {
$dialog.modal('hide');
const linkTitle = getLinkType() === 'reference-link' ? null : $linkTitle.val();
textTypeWidget.addLink(notePath, linkTitle);
textTypeWidget.addLink($autoComplete.getSelectedNotePath(), linkTitle);
}
else if ($autoComplete.getSelectedExternalLink()) {
$dialog.modal('hide');
textTypeWidget.addLink($autoComplete.getSelectedExternalLink(), $linkTitle.val());
}
else {
logError("No path to add link.");
logError("No link to add.");
}
return false;
+1
View File
@@ -13,6 +13,7 @@ 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");
const $csrf = $("#import-csrf");
let parentNoteId = null;
+4 -3
View File
@@ -13,6 +13,8 @@ export async function showDialog() {
utils.openDialog($dialog);
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true })
// clear any event listener added in previous invocation of this function
.off('autocomplete:noteselected')
.on('autocomplete:noteselected', function(event, suggestion, dataset) {
if (!suggestion.notePath) {
return false;
@@ -45,10 +47,9 @@ function showInFullText(e) {
e.preventDefault();
e.stopPropagation();
const searchText = $autoComplete.val();
const searchString = $autoComplete.val();
appContext.triggerCommand('showSearch', {searchText});
appContext.triggerCommand('searchForResults', {searchText});
appContext.triggerCommand('searchNotes', {searchString});
$dialog.modal('hide');
}
+8 -8
View File
@@ -42,14 +42,14 @@ const TPL = `
<div>
<h4>Note erasure timeout</h4>
<p>Deleted notes are at first only marked as deleted and it is possible to recover them
<p>Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them
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>
<div class="form-group">
<label for="erase-notes-after-time-in-seconds">Erase notes after X seconds</label>
<input class="form-control" id="erase-notes-after-time-in-seconds" type="number" min="0">
<label for="erase-entities-after-time-in-seconds">Erase notes after X seconds</label>
<input class="form-control" id="erase-entities-after-time-in-seconds" type="number" min="0">
</div>
<p>You can also trigger erasing manually:</p>
@@ -111,12 +111,12 @@ export default class ProtectedSessionOptions {
this.$availableLanguageCodes.text(webContents.session.availableSpellCheckerLanguages.join(', '));
}
this.$eraseNotesAfterTimeInSeconds = $("#erase-notes-after-time-in-seconds");
this.$eraseEntitiesAfterTimeInSeconds = $("#erase-entities-after-time-in-seconds");
this.$eraseNotesAfterTimeInSeconds.on('change', () => {
const eraseNotesAfterTimeInSeconds = this.$eraseNotesAfterTimeInSeconds.val();
this.$eraseEntitiesAfterTimeInSeconds.on('change', () => {
const eraseEntitiesAfterTimeInSeconds = this.$eraseEntitiesAfterTimeInSeconds.val();
server.put('options', { 'eraseNotesAfterTimeInSeconds': eraseNotesAfterTimeInSeconds }).then(() => {
server.put('options', { 'eraseEntitiesAfterTimeInSeconds': eraseEntitiesAfterTimeInSeconds }).then(() => {
toastService.showMessage("Options change have been saved.");
});
@@ -173,7 +173,7 @@ export default class ProtectedSessionOptions {
this.$spellCheckEnabled.prop("checked", options['spellCheckEnabled'] === 'true');
this.$spellCheckLanguageCode.val(options['spellCheckLanguageCode']);
this.$eraseNotesAfterTimeInSeconds.val(options['eraseNotesAfterTimeInSeconds']);
this.$eraseEntitiesAfterTimeInSeconds.val(options['eraseEntitiesAfterTimeInSeconds']);
this.$protectedSessionTimeout.val(options['protectedSessionTimeout']);
this.$noteRevisionsTimeInterval.val(options['noteRevisionSnapshotTimeInterval']);
+13
View File
@@ -22,6 +22,8 @@ class Attribute {
this.position = row.position;
/** @param {boolean} isInheritable */
this.isInheritable = !!row.isInheritable;
/** @param {boolean} */
this.isDeleted = !!row.isDeleted;
}
/** @returns {NoteShort} */
@@ -86,6 +88,17 @@ class Attribute {
getDefinition() {
return promotedAttributeDefinitionParser.parse(this.value);
}
isDefinitionFor(attr) {
return this.type === 'label' && this.name === `${attr.type}:${attr.name}`;
}
get dto() {
const dto = Object.assign({}, this);
delete dto.treeCache;
return dto;
}
}
export default Attribute;
+2 -2
View File
@@ -20,9 +20,9 @@ class Branch {
/** @param {boolean} */
this.isExpanded = !!row.isExpanded;
/** @param {boolean} */
this.isDeleted = !!row.isDeleted;
/** @param {boolean} */
this.fromSearchNote = !!row.fromSearchNote;
/** @param {boolean} */
this.isDeleted = !!row.isDeleted;
}
/** @returns {NoteShort} */
+76 -10
View File
@@ -1,6 +1,7 @@
import server from '../services/server.js';
import Attribute from './attribute.js';
import noteAttributeCache from "../services/note_attribute_cache.js";
import ws from "../services/ws.js";
import options from "../services/options.js";
const LABEL = 'label';
const RELATION = 'relation';
@@ -76,7 +77,7 @@ class NoteShort {
}
addChild(childNoteId, branchId, sort = true) {
if (!this.children.includes(childNoteId)) {
if (!(childNoteId in this.childToBranch)) {
this.children.push(childNoteId);
}
@@ -266,17 +267,21 @@ class NoteShort {
return this.getAttributes(LABEL, name);
}
getIcon(isFolder = false) {
const iconCassLabels = this.getLabels('iconClass');
getIcon() {
const iconClassLabels = this.getLabels('iconClass');
const workspaceIconClass = this.getWorkspaceIconClass();
if (iconCassLabels.length > 0) {
return iconCassLabels.map(l => l.value).join(' ');
if (iconClassLabels.length > 0) {
return iconClassLabels.map(l => l.value).join(' ');
}
else if (workspaceIconClass) {
return workspaceIconClass;
}
else if (this.noteId === 'root') {
return "bx bx-chevrons-right";
}
else if (this.type === 'text') {
if (isFolder) {
if (this.isFolder()) {
return "bx bx-folder";
}
else {
@@ -291,6 +296,34 @@ class NoteShort {
}
}
isFolder() {
return this.type === 'search'
|| this.getFilteredChildBranches().length > 0;
}
getFilteredChildBranches() {
let childBranches = this.getChildBranches();
if (!childBranches) {
ws.logError(`No children for ${parentNote}. This shouldn't happen.`);
return;
}
if (options.is("hideIncludedImages_main")) {
const imageLinks = this.getRelations('imageLink');
// image is already visible in the parent note so no need to display it separately in the book
childBranches = childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId));
}
// we're not checking hideArchivedNotes since that would mean we need to lazy load the child notes
// which would seriously slow down everything.
// we check this flag only once user chooses to expand the parent. This has the negative consequence that
// note may appear as folder but not contain any children when all of them are archived
return childBranches;
}
/**
* @param {string} [name] - relation name to filter
* @returns {Attribute[]} all note's relations (attributes with type relation), including inherited ones
@@ -475,19 +508,42 @@ class NoteShort {
return relations.map(rel => this.treeCache.notes[rel.value]);
}
hasAncestor(ancestorNote) {
getPromotedDefinitionAttributes() {
if (this.hasLabel('hidePromotedAttributes')) {
return [];
}
return this.getAttributes()
.filter(attr => attr.isDefinition())
.filter(attr => {
const def = attr.getDefinition();
return def && def.isPromoted;
});
}
hasAncestor(ancestorNote, visitedNoteIds) {
if (this.noteId === ancestorNote.noteId) {
return true;
}
if (!visitedNoteIds) {
visitedNoteIds = new Set();
} else if (visitedNoteIds.has(this.noteId)) {
// to avoid infinite cycle when template is descendent of the instance
return false;
}
visitedNoteIds.add(this.noteId);
for (const templateNote of this.getTemplateNotes()) {
if (templateNote.hasAncestor(ancestorNote)) {
if (templateNote.hasAncestor(ancestorNote, visitedNoteIds)) {
return true;
}
}
for (const parentNote of this.getParentNotes()) {
if (parentNote.hasAncestor(ancestorNote)) {
if (parentNote.hasAncestor(ancestorNote, visitedNoteIds)) {
return true;
}
}
@@ -548,6 +604,16 @@ class NoteShort {
const labels = this.getLabels('cssClass');
return labels.map(l => l.value).join(' ');
}
getWorkspaceIconClass() {
const labels = this.getLabels('workspaceIconClass');
return labels.length > 0 ? labels[0].value : "";
}
getWorkspaceTabBackgroundColor() {
const labels = this.getLabels('workspaceTabBackgroundColor');
return labels.length > 0 ? labels[0].value : "";
}
}
export default NoteShort;
@@ -5,11 +5,23 @@ import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import TabCachingWidget from "../widgets/tab_caching_widget.js";
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 NoteDetailWidget from "../widgets/note_detail.js";
import AttributeListWidget from "../widgets/attribute_list.js";
import OwnedAttributeListWidget from "../widgets/type_property_widgets/owned_attribute_list.js";
import CollapsibleSectionContainer from "../widgets/collapsible_section_container.js";
import SearchDefinitionWidget from "../widgets/type_property_widgets/search_definition.js";
import PromotedAttributesWidget from "../widgets/type_property_widgets/promoted_attributes.js";
import InheritedAttributesWidget from "../widgets/type_property_widgets/inherited_attribute_list.js";
import Container from "../widgets/container.js";
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
import NoteListWidget from "../widgets/note_list.js";
import SqlResultWidget from "../widgets/sql_result.js";
import FilePropertiesWidget from "../widgets/type_property_widgets/file_properties.js";
import ImagePropertiesWidget from "../widgets/type_property_widgets/image_properties.js";
import NotePropertiesWidget from "../widgets/type_property_widgets/note_properties.js";
import NoteIconWidget from "../widgets/note_icon.js";
import NotePathsWidget from "../widgets/note_paths.js";
export default class DesktopExtraWindowLayout {
constructor(customWidgets) {
@@ -32,15 +44,33 @@ export default class DesktopExtraWindowLayout {
.filling()
.child(new FlexContainer('column').id('center-pane').filling()
.child(new FlexContainer('row').class('title-row')
.css('align-items: center;')
.cssBlock('.title-row > * { margin: 5px; }')
.overflowing()
.cssBlock('.title-row > * { margin: 5px 5px 0 5px; }')
.child(new NoteIconWidget())
.child(new NoteTitleWidget())
.child(new RunScriptButtonsWidget().hideInZenMode())
.child(new NotePathsWidget().hideInZenMode())
.child(new NoteTypeWidget().hideInZenMode())
.child(new NoteActionsWidget().hideInZenMode())
)
.child(new TabCachingWidget(() => new AttributeListWidget()))
.child(new TabCachingWidget(() => new NoteDetailWidget()))
.child(
new TabCachingWidget(() => new CollapsibleSectionContainer()
.child(new SearchDefinitionWidget())
.child(new NotePropertiesWidget())
.child(new FilePropertiesWidget())
.child(new ImagePropertiesWidget())
.child(new PromotedAttributesWidget())
.child(new OwnedAttributeListWidget())
.child(new InheritedAttributesWidget())
)
)
.child(new Container()
.css('height: 100%; overflow: auto;')
.child(new TabCachingWidget(() => new SqlTableSchemasWidget()))
.child(new TabCachingWidget(() => new NoteDetailWidget()))
.child(new TabCachingWidget(() => new NoteListWidget()))
.child(new TabCachingWidget(() => new SqlResultWidget()))
)
.child(...this.customWidgets.get('center-pane'))
)
);
@@ -4,15 +4,11 @@ import TabRowWidget from "../widgets/tab_row.js";
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
import StandardTopWidget from "../widgets/standard_top_widget.js";
import SidePaneContainer from "../widgets/side_pane_container.js";
import GlobalButtonsWidget from "../widgets/global_buttons.js";
import SearchBoxWidget from "../widgets/search_box.js";
import SearchResultsWidget from "../widgets/search_results.js";
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 OwnedAttributeListWidget from "../widgets/type_property_widgets/owned_attribute_list.js";
import NoteTypeWidget from "../widgets/note_type.js";
import NoteActionsWidget from "../widgets/note_actions.js";
import NoteDetailWidget from "../widgets/note_detail.js";
@@ -24,6 +20,18 @@ 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";
import CollapsibleSectionContainer from "../widgets/collapsible_section_container.js";
import PromotedAttributesWidget from "../widgets/type_property_widgets/promoted_attributes.js";
import InheritedAttributesWidget from "../widgets/type_property_widgets/inherited_attribute_list.js";
import NoteListWidget from "../widgets/note_list.js";
import SearchDefinitionWidget from "../widgets/type_property_widgets/search_definition.js";
import Container from "../widgets/container.js";
import SqlResultWidget from "../widgets/sql_result.js";
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
import FilePropertiesWidget from "../widgets/type_property_widgets/file_properties.js";
import ImagePropertiesWidget from "../widgets/type_property_widgets/image_properties.js";
import NotePropertiesWidget from "../widgets/type_property_widgets/note_properties.js";
import NoteIconWidget from "../widgets/note_icon.js";
const RIGHT_PANE_CSS = `
<style>
@@ -40,7 +48,7 @@ const RIGHT_PANE_CSS = `
#right-pane .card-header {
background: inherit;
padding: 6px 10px 3px 10px;
padding: 6px 10px 3px 0;
width: 99%; /* to give minimal right margin */
background-color: var(--button-background-color);
border-color: var(--button-border-color);
@@ -85,15 +93,19 @@ const RIGHT_PANE_CSS = `
#right-pane .widget-toggle-button {
cursor: pointer;
color: var(--main-text-color) !important;
position: relative;
top: 2px;
font-size: large;
}
#right-pane .widget-toggle-button:hover {
text-decoration: none !important;
}
#right-pane .widget-toggle-icon {
position: relative;
top: 2px;
font-size: large;
padding-left: 5px;
}
#right-pane .body-wrapper {
overflow: auto;
}
@@ -137,24 +149,38 @@ export default class DesktopMainWindowLayout {
.filling()
.child(new SidePaneContainer('left')
.hideInZenMode()
.child(new GlobalButtonsWidget())
.child(new SearchBoxWidget())
.child(new SearchResultsWidget())
.child(new TabCachingWidget(() => new NotePathsWidget()))
.child(appContext.mainTreeWidget)
.child(...this.customWidgets.get('left-pane'))
)
.child(new FlexContainer('column').id('center-pane')
.child(new FlexContainer('row').class('title-row')
.cssBlock('.title-row > * { margin: 5px 5px 0 5px; }')
.css('align-items: center;')
.cssBlock('.title-row > * { margin: 5px; }')
.overflowing()
.child(new NoteIconWidget())
.child(new NoteTitleWidget())
.child(new RunScriptButtonsWidget().hideInZenMode())
.child(new NotePathsWidget().hideInZenMode())
.child(new NoteTypeWidget().hideInZenMode())
.child(new NoteActionsWidget().hideInZenMode())
)
.child(new TabCachingWidget(() => new AttributeListWidget()))
.child(new TabCachingWidget(() => new NoteDetailWidget()))
.child(
new TabCachingWidget(() => new CollapsibleSectionContainer()
.child(new SearchDefinitionWidget())
.child(new NotePropertiesWidget())
.child(new FilePropertiesWidget())
.child(new ImagePropertiesWidget())
.child(new PromotedAttributesWidget())
.child(new OwnedAttributeListWidget())
.child(new InheritedAttributesWidget())
)
)
.child(new Container()
.css('height: 100%; overflow: auto;')
.child(new TabCachingWidget(() => new SqlTableSchemasWidget()))
.child(new TabCachingWidget(() => new NoteDetailWidget()))
.child(new TabCachingWidget(() => new NoteListWidget()))
.child(new TabCachingWidget(() => new SqlResultWidget()))
)
.child(new TabCachingWidget(() => new SimilarNotesWidget()))
.child(...this.customWidgets.get('center-pane'))
)
@@ -40,44 +40,49 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) {
}
async function initLabelValueAutocomplete({ $el, open, nameCallback }) {
if (!$el.hasClass("aa-input")) {
const attributeName = nameCallback();
if (attributeName.trim() === "") {
return;
}
const attributeValues = (await server.get('attributes/values/' + encodeURIComponent(attributeName)))
.map(attribute => ({ value: attribute }));
if (attributeValues.length === 0) {
return;
}
$el.autocomplete({
appendTo: document.querySelector('body'),
hint: false,
openOnFocus: true,
minLength: 0,
tabAutocomplete: false
}, [{
displayKey: 'value',
source: function (term, cb) {
term = term.toLowerCase();
const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term));
cb(filtered);
}
}]);
$el.on('autocomplete:opened', () => {
if ($el.attr("readonly")) {
$el.autocomplete('close');
}
})
if ($el.hasClass("aa-input")) {
// we reinit everytime because autocomplete seems to have a bug where it retains state from last
// open even though the value was resetted
$el.autocomplete('destroy');
}
const attributeName = nameCallback();
if (attributeName.trim() === "") {
return;
}
const attributeValues = (await server.get('attributes/values/' + encodeURIComponent(attributeName)))
.map(attribute => ({ value: attribute }));
if (attributeValues.length === 0) {
return;
}
$el.autocomplete({
appendTo: document.querySelector('body'),
hint: false,
openOnFocus: false, // handled manually
minLength: 0,
tabAutocomplete: false
}, [{
displayKey: 'value',
cache: false,
source: async function (term, cb) {
term = term.toLowerCase();
const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term));
cb(filtered);
}
}]);
$el.on('autocomplete:opened', () => {
if ($el.attr("readonly")) {
$el.autocomplete('close');
}
});
if (open) {
$el.autocomplete("open");
}
+26 -2
View File
@@ -63,7 +63,7 @@ async function createNoteLink(noteId) {
}
async function renderAttributes(attributes, renderIsInheritable) {
const $container = $("<span>");
const $container = $('<span class="rendered-note-attributes">');
for (let i = 0; i < attributes.length; i++) {
const attribute = attributes[i];
@@ -79,7 +79,31 @@ async function renderAttributes(attributes, renderIsInheritable) {
return $container;
}
async function renderNormalAttributes(note) {
const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();
let attrs = note.getAttributes();
if (promotedDefinitionAttributes.length > 0) {
attrs = attrs.filter(attr => !!promotedDefinitionAttributes.find(promAttr => promAttr.isDefinitionFor(attr)));
}
else {
attrs = attrs.filter(
attr => !attr.isDefinition()
&& !attr.isAutoLink
&& attr.noteId === note.noteId
);
}
const $renderedAttributes = await renderAttributes(attrs, false);
return {
count: attrs.length,
$renderedAttributes
}
}
export default {
renderAttribute,
renderAttributes
renderAttributes,
renderNormalAttributes
}
+27
View File
@@ -0,0 +1,27 @@
import server from './server.js';
async function addLabel(noteId, name, value = "") {
await server.put(`notes/${noteId}/attribute`, {
type: 'label',
name: name,
value: value
});
}
async function setLabel(noteId, name, value = "") {
await server.put(`notes/${noteId}/set-attribute`, {
type: 'label',
name: name,
value: value
});
}
async function removeAttributeById(noteId, attributeId) {
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
}
export default {
addLabel,
setLabel,
removeAttributeById
}
+30 -2
View File
@@ -1,5 +1,13 @@
import treeCache from "./tree_cache.js";
import server from "./server.js";
import ws from "./ws.js";
/** @return {NoteShort} */
async function getInboxNote() {
const note = await server.get('date-notes/inbox/' + dayjs().format("YYYY-MM-DD"), "date-note");
return await treeCache.getNote(note.noteId);
}
/** @return {NoteShort} */
async function getTodayNote() {
@@ -34,10 +42,30 @@ async function createSqlConsole() {
return await treeCache.getNote(note.noteId);
}
/** @return {NoteShort} */
async function createSearchNote(opts = {}) {
const note = await server.post('search-note');
const attrsToUpdate = [
opts.ancestorNoteId ? { type: 'relation', name: 'ancestor', value: opts.ancestorNoteId } : undefined,
{ type: 'label', name: 'searchString', value: opts.searchString }
].filter(attr => !!attr);
if (attrsToUpdate.length > 0) {
await server.put(`notes/${note.noteId}/attributes`, attrsToUpdate);
}
await ws.waitForMaxKnownEntityChangeId();
return await treeCache.getNote(note.noteId);
}
export default {
getInboxNote,
getTodayNote,
getDateNote,
getMonthNote,
getYearNote,
createSqlConsole
}
createSqlConsole,
createSearchNote
}
@@ -1,7 +1,7 @@
import Component from "../widgets/component.js";
import appContext from "./app_context.js";
import dateNoteService from "../services/date_notes.js";
import noteCreateService from "../services/note_create.js";
import treeService from "../services/tree.js";
export default class DialogCommandExecutor extends Component {
jumpToNoteCommand() {
@@ -64,7 +64,21 @@ export default class DialogCommandExecutor extends Component {
const tabContext = await appContext.tabManager.openTabWithNote(sqlConsoleNote.noteId, true);
appContext.triggerCommand('focusOnDetail', {tabId: tabContext.tabId});
appContext.triggerEvent('focusOnDetail', {tabId: tabContext.tabId});
}
async searchNotesCommand({searchString, ancestorNoteId}) {
const searchNote = await dateNoteService.createSearchNote({searchString, ancestorNoteId});
const tabContext = await appContext.tabManager.openTabWithNote(searchNote.noteId, true);
appContext.triggerCommand('focusOnSearchDefinition', {tabId: tabContext.tabId});
}
async searchInSubtreeCommand({notePath}) {
const noteId = treeService.getNoteIdFromNotePath(notePath);
this.searchNotesCommand({ancestorNoteId: noteId});
}
showBackendLogCommand() {
+37 -34
View File
@@ -1,6 +1,6 @@
import utils from "./utils.js";
import dateNoteService from "./date_notes.js";
import hoistedNoteService from "./hoisted_note.js";
import protectedSessionHolder from './protected_session_holder.js';
import server from "./server.js";
import appContext from "./app_context.js";
import Component from "../widgets/component.js";
@@ -62,14 +62,14 @@ export default class Entrypoints extends Component {
findInPage.openFindWindow();
}
async createNoteIntoDayNoteCommand() {
const todayNote = await dateNoteService.getTodayNote();
async createNoteIntoInboxCommand() {
const inboxNote = await dateNoteService.getInboxNote();
const {note} = await server.post(`notes/${todayNote.noteId}/children?target=into`, {
const {note} = await server.post(`notes/${inboxNote.noteId}/children?target=into`, {
title: 'new note',
content: '',
type: 'text',
isProtected: todayNote.isProtected
isProtected: inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable()
});
await ws.waitForMaxKnownEntityChangeId();
@@ -80,19 +80,30 @@ export default class Entrypoints extends Component {
}
async toggleNoteHoistingCommand() {
const note = appContext.tabManager.getActiveTabNote();
const tabContext = appContext.tabManager.getActiveTabContext();
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
if (note.noteId === hoistedNoteId) {
hoistedNoteService.unhoist();
if (tabContext.note.noteId === tabContext.hoistedNoteId) {
await tabContext.unhoist();
}
else if (note.type !== 'search') {
hoistedNoteService.setHoistedNoteId(note.noteId);
else if (tabContext.note.type !== 'search') {
await tabContext.setHoistedNoteId(tabContext.note.noteId);
}
}
async hoistNoteCommand({noteId}) {
const tabContext = appContext.tabManager.getActiveTabContext();
if (tabContext.hoistedNoteId !== noteId) {
await tabContext.setHoistedNoteId(noteId);
}
}
async unhoistCommand() {
hoistedNoteService.unhoist();
const activeTabContext = appContext.tabManager.getActiveTabContext();
if (activeTabContext) {
activeTabContext.unhoist();
}
}
copyWithoutFormattingCommand() {
@@ -165,21 +176,6 @@ export default class Entrypoints extends Component {
}
}
async searchForResultsCommand({searchText}) {
const response = await server.get('search/' + encodeURIComponent(searchText) + '?includeNoteContent=true&excludeArchived=true&fuzzyAttributeSearch=false');
if (!response.success) {
toastService.showError("Search failed: " + response.message, 10000);
// even in this case we'll show the results
}
this.triggerEvent('searchResults', {results: response.results});
// have at least some feedback which is good especially in situations
// when the result list does not change with a query
toastService.showMessage("Search finished successfully.");
}
async switchToDesktopVersionCommand() {
utils.setCookie('trilium-device', 'desktop');
@@ -188,11 +184,15 @@ export default class Entrypoints extends Component {
createTopLevelNoteCommand() { noteCreateService.createNewTopLevelNote(); }
async openInWindowCommand({notePath}) {
async openInWindowCommand({notePath, hoistedNoteId}) {
if (!hoistedNoteId) {
hoistedNoteId = 'root';
}
if (utils.isElectron()) {
const {ipcRenderer} = utils.dynamicRequire('electron');
ipcRenderer.send('create-extra-window', {notePath});
ipcRenderer.send('create-extra-window', {notePath, hoistedNoteId});
}
else {
const url = window.location.protocol + '//' + window.location.host + window.location.pathname + '?extra=1#' + notePath;
@@ -202,11 +202,12 @@ export default class Entrypoints extends Component {
}
async openNewWindowCommand() {
this.openInWindowCommand({notePath: ''});
this.openInWindowCommand({notePath: '', hoistedNoteId: 'root'});
}
async runActiveNoteCommand() {
const note = appContext.tabManager.getActiveTabNote();
const tabContext = appContext.tabManager.getActiveTabContext();
const note = tabContext.note;
// ctrl+enter is also used elsewhere so make sure we're running only when appropriate
if (!note || note.type !== 'code') {
@@ -215,10 +216,12 @@ export default class Entrypoints extends Component {
if (note.mime.endsWith("env=frontend")) {
await bundleService.getAndExecuteBundle(note.noteId);
}
if (note.mime.endsWith("env=backend")) {
} else if (note.mime.endsWith("env=backend")) {
await server.post('script/run/' + note.noteId);
} else if (note.mime === 'text/x-sqlite;schema=trilium') {
const result = await server.post("sql/execute/" + note.noteId);
this.triggerEvent('sqlQueryResults', {tabId: tabContext.tabId, results: result.results});
}
toastService.showMessage("Note executed");
@@ -1,4 +1,3 @@
import treeService from './tree.js';
import server from './server.js';
import utils from './utils.js';
import toastService from './toast.js';
@@ -9,7 +8,6 @@ import protectedSessionService from './protected_session.js';
import dateNotesService from './date_notes.js';
import CollapsibleWidget from '../widgets/collapsible_widget.js';
import ws from "./ws.js";
import hoistedNoteService from "./hoisted_note.js";
import appContext from "./app_context.js";
import TabAwareWidget from "../widgets/tab_aware_widget.js";
import TabCachingWidget from "../widgets/tab_caching_widget.js";
@@ -89,7 +87,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
this.addButtonToToolbar = opts => {
const buttonId = "toolbar-button-" + opts.title.replace(/\s/g, "-");
const button = $('<button>')
const button = $('<button class="noborder">')
.addClass("btn btn-sm")
.on('click', opts.action);
@@ -379,13 +377,19 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
this.getYearNote = dateNotesService.getYearNote;
/**
* Hoist note. See https://github.com/zadam/trilium/wiki/Note-hoisting
* Hoist note in the current tab. See https://github.com/zadam/trilium/wiki/Note-hoisting
*
* @method
* @param {string} noteId - set hoisted note. 'root' will effectively unhoist
* @return {Promise}
*/
this.setHoistedNoteId = hoistedNoteService.setHoistedNoteId;
this.setHoistedNoteId = (noteId) => {
const activeTabContext = appContext.tabManager.getActiveTabContext();
if (activeTabContext) {
activeTabContext.setHoistedNoteId(noteId);
}
};
/**
* @method
+2 -6
View File
@@ -58,7 +58,7 @@ function setupGlobs() {
'Line: ' + lineNo,
'Column: ' + columnNo,
'Error object: ' + JSON.stringify(error),
'Stack: ' + error && error.stack
'Stack: ' + (error && error.stack)
].join(', ');
}
@@ -73,11 +73,7 @@ function setupGlobs() {
libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`);
}
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/";
$(document).on("click", "*[data-help-page]", e => {
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
});
utils.initHelpButtons($(window));
$("body").on("click", "a.external", function () {
window.open($(this).attr("href"), '_blank');
+9 -19
View File
@@ -1,27 +1,18 @@
import options from './options.js';
import appContext from "./app_context.js";
import treeService from "./tree.js";
import treeCache from "./tree_cache.js";
function getHoistedNoteId() {
return options.get('hoistedNoteId');
}
const activeTabContext = appContext.tabManager.getActiveTabContext();
async function setHoistedNoteId(noteId) {
if (getHoistedNoteId() === noteId) {
return;
}
await options.save('hoistedNoteId', noteId);
await treeCache.loadInitialTree();
// FIXME - just use option load event
appContext.triggerEvent('hoistedNoteChanged', {noteId});
return activeTabContext ? activeTabContext.hoistedNoteId : 'root';
}
async function unhoist() {
await setHoistedNoteId('root');
const activeTabContext = appContext.tabManager.getActiveTabContext();
if (activeTabContext) {
await activeTabContext.unhoist();
}
}
function isTopLevelNode(node) {
@@ -34,7 +25,7 @@ function isRootNode(node) {
|| node.data.noteId === getHoistedNoteId();
}
async function checkNoteAccess(notePath) {
async function checkNoteAccess(notePath, tabContext) {
// notePath argument can contain only noteId which is not good when hoisted since
// then we need to check the whole note path
const resolvedNotePath = await treeService.resolveNotePath(notePath);
@@ -44,7 +35,7 @@ async function checkNoteAccess(notePath) {
return false;
}
const hoistedNoteId = getHoistedNoteId();
const hoistedNoteId = tabContext.hoistedNoteId;
if (hoistedNoteId !== 'root' && !resolvedNotePath.includes(hoistedNoteId)) {
const confirmDialog = await import('../dialogs/confirm.js');
@@ -62,7 +53,6 @@ async function checkNoteAccess(notePath) {
export default {
getHoistedNoteId,
setHoistedNoteId,
unhoist,
isTopLevelNode,
isRootNode,
+2 -2
View File
@@ -27,7 +27,7 @@ export async function uploadFiles(parentNoteId, files, options) {
({noteId} = await $.ajax({
url: baseApiUrl + 'notes/' + parentNoteId + '/import',
headers: server.getHeaders(),
headers: await server.getHeaders(),
data: formData,
dataType: 'json',
type: 'POST',
@@ -71,4 +71,4 @@ ws.subscribeToMessages(async message => {
export default {
uploadFiles
}
}
+5 -8
View File
@@ -78,7 +78,7 @@ function goToLink(e) {
if (notePath) {
if ((e.which === 1 && e.ctrlKey) || e.which === 2) {
appContext.tabManager.openTabWithNote(notePath);
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
}
else if (e.which === 1) {
const activeTabContext = appContext.tabManager.getActiveTabContext();
@@ -90,7 +90,7 @@ function goToLink(e) {
|| $link.hasClass("ck-link-actions__preview") // within edit link dialog single click suffices
) {
const address = $link.attr('href');
console.log("address", address);
if (address) {
if (address.toLowerCase().startsWith('http')) {
window.open(address, '_blank');
@@ -127,10 +127,10 @@ function linkContextMenu(e) {
],
selectMenuItemHandler: ({command}) => {
if (command === 'openNoteInNewTab') {
appContext.tabManager.openTabWithNote(notePath);
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
}
else if (command === 'openNoteInNewWindow') {
appContext.triggerCommand('openInWindow', {notePath});
appContext.triggerCommand('openInWindow', {notePath, hoistedNoteId: 'root'});
}
}
});
@@ -144,11 +144,8 @@ async function loadReferenceLinkTitle(noteId, $el) {
if (!note) {
title = '[missing]';
}
else if (!note.isDeleted) {
title = note.title;
}
else {
title = note.isErased ? '[erased]' : `${note.title} (deleted)`;
title = note.isDeleted ? `${note.title} (deleted)` : note.title;
}
$el.text(title);
+1
View File
@@ -54,6 +54,7 @@ export default class LoadResults {
this.attributes.push({attributeId, sourceId});
}
/** @return {Attribute[]} */
getAttributes(sourceId = 'none') {
return this.attributes
.filter(row => row.sourceId !== sourceId)
+69 -3
View File
@@ -3,10 +3,13 @@ import appContext from "./app_context.js";
import utils from './utils.js';
import noteCreateService from './note_create.js';
import treeService from './tree.js';
import treeCache from "./tree_cache.js";
// this key needs to have this value so it's hit by the tooltip
const SELECTED_NOTE_PATH_KEY = "data-note-path";
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link";
async function autocompleteSourceForCKEditor(queryText) {
return await new Promise((res, rej) => {
autocompleteSource(queryText, rows => {
@@ -21,18 +24,20 @@ async function autocompleteSourceForCKEditor(queryText) {
highlightedNotePathTitle: row.highlightedNotePathTitle
}
}));
}, {
allowCreatingNotes: true
});
});
}
async function autocompleteSource(term, cb) {
async function autocompleteSource(term, cb, options = {}) {
const activeNoteId = appContext.tabManager.getActiveTabNoteId();
let results = await server.get('autocomplete'
+ '?query=' + encodeURIComponent(term)
+ '&activeNoteId=' + activeNoteId);
if (term.trim().length >= 1) {
if (term.trim().length >= 1 && options.allowCreatingNotes) {
results = [
{
action: 'create-note',
@@ -43,6 +48,16 @@ async function autocompleteSource(term, cb) {
].concat(results);
}
if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
results = [
{
action: 'external-link',
externalLink: term,
highlightedNotePathTitle: `Insert external link to "${term}"`
}
].concat(results);
}
cb(results);
}
@@ -55,6 +70,17 @@ function clearText($el) {
$el.autocomplete("val", "").trigger('change');
}
function setText($el, text) {
if (utils.isMobile()) {
return;
}
$el.setSelectedNotePath("");
$el
.autocomplete("val", text.trim())
.autocomplete("open");
}
function showRecentNotes($el) {
if (utils.isMobile()) {
return;
@@ -119,7 +145,7 @@ function initNoteAutocomplete($el, options) {
tabAutocomplete: false
}, [
{
source: autocompleteSource,
source: (term, cb) => autocompleteSource(term, cb, options),
displayKey: 'notePathTitle',
templates: {
suggestion: suggestion => suggestion.highlightedNotePathTitle
@@ -130,6 +156,19 @@ function initNoteAutocomplete($el, options) {
]);
$el.on('autocomplete:selected', async (event, suggestion) => {
if (suggestion.action === 'external-link') {
$el.setSelectedNotePath(null);
$el.setSelectedExternalLink(suggestion.externalLink);
$el.autocomplete("val", suggestion.externalLink);
$el.autocomplete("close");
$el.trigger('autocomplete:externallinkselected', [suggestion]);
return;
}
if (suggestion.action === 'create-note') {
const {note} = await noteCreateService.createNote(suggestion.parentNoteId, {
title: suggestion.noteTitle,
@@ -140,6 +179,7 @@ function initNoteAutocomplete($el, options) {
}
$el.setSelectedNotePath(suggestion.notePath);
$el.setSelectedExternalLink(null);
$el.autocomplete("val", suggestion.noteTitle);
@@ -193,6 +233,31 @@ function init() {
.toggleClass("disabled", !notePath.trim())
.attr(SELECTED_NOTE_PATH_KEY, notePath); // we also set attr here so tooltip can be displayed
};
$.fn.getSelectedExternalLink = function () {
if (!$(this).val().trim()) {
return "";
} else {
return $(this).attr(SELECTED_EXTERNAL_LINK_KEY);
}
};
$.fn.setSelectedExternalLink = function (externalLink) {
$(this).attr(SELECTED_EXTERNAL_LINK_KEY, externalLink);
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
.toggleClass("disabled", true);
}
$.fn.setNote = async function (noteId) {
const note = noteId ? await treeCache.getNote(noteId, true) : null;
$(this)
.val(note ? note.title : "")
.setSelectedNotePath(noteId);
}
}
export default {
@@ -200,5 +265,6 @@ export default {
autocompleteSourceForCKEditor,
initNoteAutocomplete,
showRecentNotes,
setText,
init
}
@@ -4,34 +4,42 @@ import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
import libraryLoader from "./library_loader.js";
import openService from "./open.js";
import treeCache from "./tree_cache.js";
async function getRenderedContent(note, options = {}) {
options = Object.assign({
trim: false,
tooltip: false
}, options);
async function getRenderedContent(note) {
const type = getRenderingType(note);
let $rendered;
const $renderedContent = $('<div class="rendered-note-content">');
if (type === 'text') {
const fullNote = await server.get('notes/' + note.noteId);
const noteComplement = await treeCache.getNoteComplement(note.noteId);
$rendered = $('<div class="ck-content">').html(fullNote.content);
$renderedContent.append($('<div class="ck-content">').html(trim(noteComplement.content, options.trim)));
if ($rendered.find('span.math-tex').length > 0) {
if ($renderedContent.find('span.math-tex').length > 0) {
await libraryLoader.requireLibrary(libraryLoader.KATEX);
renderMathInElement($rendered[0], {});
renderMathInElement($renderedContent[0], {});
}
}
else if (type === 'code') {
const fullNote = await server.get('notes/' + note.noteId);
$rendered = $("<pre>").text(fullNote.content);
$renderedContent.append($("<pre>").text(trim(fullNote.content, options.trim)));
}
else if (type === 'image') {
$rendered = $("<img>")
.attr("src", `api/images/${note.noteId}/${note.title}`)
.css("max-width", "100%");
$renderedContent.append(
$("<img>")
.attr("src", `api/images/${note.noteId}/${note.title}`)
.css("max-width", "100%")
);
}
else if (type === 'file' || type === 'pdf') {
else if (!options.tooltip && (type === 'file' || type === 'pdf')) {
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
@@ -41,48 +49,62 @@ async function getRenderedContent(note) {
// open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected);
$rendered = $('<div style="display: flex; flex-direction: column; height: 100%;">');
const $content = $('<div style="display: flex; flex-direction: column; height: 100%;">');
if (type === 'pdf') {
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
$pdfPreview.attr("src", openService.getUrlForDownload("api/notes/" + note.noteId + "/open"));
$rendered.append($pdfPreview);
$content.append($pdfPreview);
}
$rendered.append(
$("<div>")
$content.append(
$('<div style="display: flex; justify-content: space-evenly; margin-top: 5px;">')
.append($downloadButton)
.append(' &nbsp; ')
.append($openButton)
);
$renderedContent.append($content);
}
else if (type === 'render') {
$rendered = $('<div>');
const $content = $('<div>');
await renderService.render(note, $rendered, this.ctx);
await renderService.render(note, $content, this.ctx);
$renderedContent.append($content);
}
else if (type === 'protected-session') {
else if (!options.tooltip && type === 'protected-session') {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
.on('click', protectedSessionService.enterProtectedSession);
$rendered = $("<div>")
.append("<div>This note is protected and to access it you need to enter password.</div>")
.append("<br/>")
.append($button);
$renderedContent.append(
$("<div>")
.append("<div>This note is protected and to access it you need to enter password.</div>")
.append("<br/>")
.append($button)
);
}
else {
$rendered = $("<em>Content of this note cannot be displayed in the book format</em>");
$renderedContent.append($("<em>Content of this note cannot be displayed in the book format</em>"));
}
$rendered.addClass(note.getCssClass());
$renderedContent.addClass(note.getCssClass());
return {
renderedContent: $rendered,
$renderedContent,
type
};
}
function trim(text, doTrim) {
if (!doTrim) {
return text;
}
else {
return text.substr(0, Math.min(text.length, 1000));
}
}
function getRenderingType(note) {
let type = note.type;
+2 -2
View File
@@ -56,10 +56,10 @@ async function createNote(parentNoteId, options = {}) {
await activeTabContext.setNote(note.noteId);
if (options.focus === 'title') {
appContext.triggerCommand('focusAndSelectTitle');
appContext.triggerEvent('focusAndSelectTitle');
}
else if (options.focus === 'content') {
appContext.triggerCommand('focusOnDetail', {tabId: this.tabId});
appContext.triggerEvent('focusOnDetail', {tabId: activeTabContext.tabId});
}
}
@@ -0,0 +1,364 @@
import linkService from "./link.js";
import noteContentRenderer from "./note_content_renderer.js";
import treeCache from "./tree_cache.js";
import attributeService from "./attributes.js";
import attributeRenderer from "./attribute_renderer.js";
const TPL = `
<div class="note-list">
<style>
.note-list {
overflow: hidden;
position: relative;
height: 100%;
}
.note-list.grid-view .note-list-container {
display: flex;
flex-wrap: wrap;
}
.note-list.grid-view .note-book-card {
flex-basis: 300px;
}
.note-list.grid-view .note-expander {
display: none;
}
.note-list.grid-view .note-book-card {
max-height: 300px;
}
.note-book-card {
border-radius: 10px;
background-color: var(--accented-background-color);
padding: 10px 15px 15px 8px;
margin: 5px 5px 5px 0;
overflow: hidden;
display: flex;
flex-direction: column;
flex-shrink: 0;
flex-grow: 1;
}
.note-book-card:not(.expanded) .note-book-content {
display: none !important;
padding: 10px
}
.note-book-card.expanded .note-book-content {
display: block;
min-height: 0;
height: 100%;
padding-top: 10px;
}
.note-book-title {
margin-bottom: 0;
}
.note-book-title .rendered-note-attributes {
font-size: medium;
}
.note-book-title .rendered-note-attributes:before {
content: "\\00a0\\00a0";
}
.note-book-card .note-book-card {
border: 1px solid var(--main-border-color);
}
.note-book-content.type-image, .note-book-content.type-file, .note-book-content.type-protected-session {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 10px;
}
.note-book-content.type-image img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.note-book-card.type-image .note-book-content img, .note-book-card.type-text .note-book-content img {
max-width: 100%;
max-height: 100%;
}
.note-book-title {
flex-grow: 0;
}
.note-list-wrapper {
height: 100%;
overflow: auto;
}
.note-expander {
font-size: x-large;
position: relative;
top: 3px;
padding-right: 3px;
cursor: pointer;
}
.note-list-pager {
text-align: center;
}
</style>
<div class="btn-group floating-button" style="right: 20px; top: 10px;">
<button type="button"
class="collapse-all-button btn icon-button bx bx-layer-minus"
title="Collapse all notes"></button>
&nbsp;
<button type="button"
class="expand-children-button btn icon-button bx bx-move-vertical"
title="Expand all children"></button>
&nbsp;
<button type="button"
class="list-view-button btn icon-button bx bx-menu"
title="List view"></button>
&nbsp;
<button type="button"
class="grid-view-button btn icon-button bx bx-grid-alt"
title="Grid view"></button>
</div>
<div class="note-list-wrapper">
<div class="note-list-pager"></div>
<div class="note-list-container"></div>
<div class="note-list-pager"></div>
</div>
</div>`;
class NoteListRenderer {
/*
* We're using noteIds so that it's not necessary to load all notes at once when paging
*/
constructor($parent, parentNote, noteIds) {
this.$noteList = $(TPL);
// note list must be added to the DOM immediatelly, otherwise some functionality scripting (canvas) won't work
$parent.empty();
this.parentNote = parentNote;
const includedNoteIds = this.getIncludedNoteIds();
this.noteIds = noteIds.filter(noteId => !includedNoteIds.has(noteId));
if (this.noteIds.length === 0) {
return;
}
$parent.append(this.$noteList);
this.page = 1;
this.pageSize = parseInt(parentNote.getLabelValue('pageSize'));
if (!this.pageSize || this.pageSize < 1) {
this.pageSize = 20;
}
this.viewType = parentNote.getLabelValue('viewType');
if (!['list', 'grid'].includes(this.viewType)) {
// when not explicitly set decide based on note type
this.viewType = parentNote.type === 'search' ? 'list' : 'grid';
}
this.$noteList.addClass(this.viewType + '-view');
this.$noteList.find('.list-view-button').on('click', () => this.toggleViewType('list'));
this.$noteList.find('.grid-view-button').on('click', () => this.toggleViewType('grid'));
this.$noteList.find('.expand-children-button').on('click', async () => {
if (!this.parentNote.hasLabel('expanded')) {
await attributeService.addLabel(this.parentNote.noteId, 'expanded');
}
await this.renderList();
});
this.$noteList.find('.collapse-all-button').on('click', async () => {
// owned is important - we shouldn't remove inherited expanded labels
for (const expandedAttr of this.parentNote.getOwnedLabels('expanded')) {
await attributeService.removeAttributeById(this.parentNote.noteId, expandedAttr.attributeId);
}
await this.renderList();
});
}
/** @return {Set<string>} list of noteIds included (images, included notes) into a parent note and which
* don't have to be shown in the note list. */
getIncludedNoteIds() {
const includedLinks = this.parentNote
? this.parentNote.getRelations().filter(rel => rel.name === 'imageLink' || rel.name === 'includeNoteLink')
: [];
return new Set(includedLinks.map(rel => rel.value));
}
async toggleViewType(type) {
if (type !== 'list' && type !== 'grid') {
throw new Error(`Invalid view type ${type}`);
}
this.viewType = type;
this.$noteList
.removeClass('grid-view')
.removeClass('list-view')
.addClass(this.viewType + '-view');
await attributeService.setLabel(this.parentNote.noteId, 'viewType', type);
await this.renderList();
}
async renderList() {
if (this.noteIds.length === 0) {
this.$noteList.hide();
return;
}
this.$noteList.show();
const $container = this.$noteList.find('.note-list-container').empty();
const startIdx = (this.page - 1) * this.pageSize;
const endIdx = startIdx + this.pageSize;
const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length));
const pageNotes = await treeCache.getNotes(pageNoteIds);
for (const note of pageNotes) {
const $card = await this.renderNote(note, this.parentNote.hasLabel('expanded'));
$container.append($card);
}
this.renderPager();
return this.$noteList;
}
renderPager() {
const $pager = this.$noteList.find('.note-list-pager').empty();
const pageCount = Math.ceil(this.noteIds.length / this.pageSize);
$pager.toggle(pageCount > 1);
let lastPrinted;
for (let i = 1; i <= pageCount; i++) {
if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(this.page - i) <= 2) {
lastPrinted = true;
$pager.append(
i === this.page
? $('<span>').text(i).css('text-decoration', 'underline').css('font-weight', "bold")
: $('<a href="javascript:">')
.text(i)
.on('click', () => {
this.page = i;
this.renderList();
}),
" &nbsp; "
);
}
else if (lastPrinted) {
$pager.append("... &nbsp; ");
lastPrinted = false;
}
}
}
async renderNote(note, expand = false) {
const $expander = $('<span class="note-expander bx bx-chevron-right"></span>');
const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
const $card = $('<div class="note-book-card">')
.attr('data-note-id', note.noteId)
.append(
$('<h5 class="note-book-title">')
.append($expander)
.append(await linkService.createNoteLink(note.noteId, {showTooltip: false}))
.append($renderedAttributes)
);
$expander.on('click', () => this.toggleContent($card, note, !$card.hasClass("expanded")));
await this.toggleContent($card, note, expand);
return $card;
}
async toggleContent($card, note, expand) {
if (this.viewType === 'list' && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) {
return;
}
const $expander = $card.find('> .note-book-title .note-expander');
if (expand || this.viewType === 'grid') {
$card.addClass("expanded");
$expander.addClass("bx-chevron-down").removeClass("bx-chevron-right");
}
else {
$card.removeClass("expanded");
$expander.addClass("bx-chevron-right").removeClass("bx-chevron-down");
}
if ((expand || this.viewType === 'grid') && $card.find('.note-book-content').length === 0) {
$card.append(await this.renderNoteContent(note));
}
}
async renderNoteContent(note) {
const $content = $('<div class="note-book-content">');
try {
const {$renderedContent, type} = await noteContentRenderer.getRenderedContent(note, {
trim: this.viewType === 'grid' // for grid only short content is needed
});
$content.append($renderedContent);
$content.addClass("type-" + type);
} catch (e) {
console.log(`Caught error while rendering note ${note.noteId} of type ${note.type}: ${e.message}, stack: ${e.stack}`);
$content.append("rendering error");
}
if (this.viewType === 'list') {
const imageLinks = note.getRelations('imageLink');
const childNotes = (await note.getChildNotes())
.filter(childNote => !imageLinks.find(rel => rel.value === childNote.noteId));
for (const childNote of childNotes) {
$content.append(await this.renderNote(childNote));
}
}
return $content;
}
}
export default NoteListRenderer;
+12 -35
View File
@@ -3,7 +3,7 @@ import linkService from "./link.js";
import treeCache from "./tree_cache.js";
import utils from "./utils.js";
import attributeRenderer from "./attribute_renderer.js";
import libraryLoader from "./library_loader.js";
import noteContentRenderer from "./note_content_renderer.js";
function setupGlobalTooltip() {
$(document).on("mouseenter", "a", mouseEnterHandler);
@@ -45,9 +45,7 @@ async function mouseEnterHandler() {
const noteId = treeService.getNoteIdFromNotePath(notePath);
const note = await treeCache.getNote(noteId);
const noteComplement = await treeCache.getNoteComplement(noteId);
const content = await renderTooltip(note, noteComplement);
const content = await renderTooltip(note);
if (utils.isHtmlEmpty(content)) {
return;
@@ -79,7 +77,7 @@ function mouseLeaveHandler() {
$(this).tooltip('dispose');
}
async function renderTooltip(note, noteComplement) {
async function renderTooltip(note) {
if (note.isDeleted) {
return '<div>Note has been deleted.</div>';
}
@@ -90,39 +88,18 @@ async function renderTooltip(note, noteComplement) {
return;
}
let content = $("<h5>").text(await treeService.getNotePathTitle(someNotePath)).prop('outerHTML');
let content = '<h5 class="note-tooltip-title">' + (await treeService.getNoteTitleWithPathAsSuffix(someNotePath)).prop('outerHTML') + '</h5>';
const attributes = note.getAttributes()
.filter(attr => !attr.isDefinition());
const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
if (attributes.length > 0) {
content += '<div class="tooltip-attributes"><strong>Attributes: </strong> '
+ (await attributeRenderer.renderAttributes(attributes)).html()
+ '</div>'
}
const {$renderedContent} = await noteContentRenderer.getRenderedContent(note, {
tooltip: true,
trim: true
});
if (note.type === 'text' && !utils.isHtmlEmpty(noteComplement.content)) {
const $content = $('<div class="ck-content">').append(noteComplement.content);
if ($content.find('span.math-tex').length > 0) {
await libraryLoader.requireLibrary(libraryLoader.KATEX);
renderMathInElement($content[0], {});
}
content += $content[0].outerHTML;
}
else if (note.type === 'code' && noteComplement.content && noteComplement.content.trim()) {
content += $("<pre>")
.text(noteComplement.content)
.prop('outerHTML');
}
else if (note.type === 'image') {
content += $("<img>")
.prop("src", `api/images/${note.noteId}/${note.title}`)
.prop('outerHTML');
}
// other types of notes don't have tooltip preview
content = content
+ '<div class="note-tooltip-attributes">' + $renderedAttributes[0].outerHTML + '</div>'
+ $renderedContent[0].outerHTML;
return content;
}
+34 -14
View File
@@ -2,13 +2,16 @@ import utils from './utils.js';
const REQUEST_LOGGING_ENABLED = false;
function getHeaders(headers) {
async function getHeaders(headers) {
const appContext = (await import('./app_context.js')).default;
const activeTabContext = appContext.tabManager ? appContext.tabManager.getActiveTabContext() : null;
// headers need to be lowercase because node.js automatically converts them to lower case
// so hypothetical protectedSessionId becomes protectedsessionid on the backend
// also avoiding using underscores instead of dashes since nginx filters them out by default
const allHeaders = {
'trilium-source-id': glob.sourceId,
'trilium-local-now-datetime': utils.localNowDateTime(),
'trilium-hoisted-note-id': activeTabContext ? activeTabContext.hoistedNoteId : null,
'x-csrf-token': glob.csrfToken
};
@@ -44,6 +47,7 @@ async function remove(url, sourceId) {
let i = 1;
const reqResolves = {};
const reqRejects = {};
let maxKnownEntityChangeId = 0;
@@ -52,12 +56,15 @@ async function call(method, url, data, headers = {}) {
const start = Date.now();
headers = await getHeaders(headers);
if (utils.isElectron()) {
const ipc = utils.dynamicRequire('electron').ipcRenderer;
const requestId = i++;
resp = await new Promise((resolve, reject) => {
reqResolves[requestId] = resolve;
reqRejects[requestId] = reject;
if (REQUEST_LOGGING_ENABLED) {
console.log(utils.now(), "Request #" + requestId + " to " + method + " " + url);
@@ -65,7 +72,7 @@ async function call(method, url, data, headers = {}) {
ipc.send('server-request', {
requestId: requestId,
headers: getHeaders(headers),
headers: headers,
method: method,
url: "/" + baseApiUrl + url,
data: data
@@ -91,12 +98,20 @@ async function call(method, url, data, headers = {}) {
return resp.body;
}
async function reportError(method, url, status, error) {
const message = "Error when calling " + method + " " + url + ": " + status + " - " + error;
const toastService = (await import("./toast.js")).default;
toastService.showError(message);
toastService.throwError(message);
}
function ajax(url, method, data, headers) {
return new Promise((res, rej) => {
const options = {
url: baseApiUrl + url,
type: method,
headers: getHeaders(headers),
headers: headers,
timeout: 60000,
success: (body, textStatus, jqXhr) => {
const respHeaders = {};
@@ -112,11 +127,8 @@ function ajax(url, method, data, headers) {
headers: respHeaders
});
},
error: async (jqXhr, textStatus, error) => {
const message = "Error when calling " + method + " " + url + ": " + textStatus + " - " + error;
const toastService = (await import("./toast.js")).default;
toastService.showError(message);
toastService.throwError(message);
error: async (jqXhr, status, error) => {
await reportError(method, url, status, error);
rej(error);
}
@@ -138,17 +150,25 @@ function ajax(url, method, data, headers) {
if (utils.isElectron()) {
const ipc = utils.dynamicRequire('electron').ipcRenderer;
ipc.on('server-response', (event, arg) => {
ipc.on('server-response', async (event, arg) => {
if (REQUEST_LOGGING_ENABLED) {
console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode);
}
reqResolves[arg.requestId]({
body: arg.body,
headers: arg.headers
});
if (arg.statusCode >= 200 && arg.statusCode < 300) {
reqResolves[arg.requestId]({
body: arg.body,
headers: arg.headers
});
}
else {
await reportError(arg.method, arg.url, arg.statusCode, arg.body);
reqRejects[arg.requestId]();
}
delete reqResolves[arg.requestId];
delete reqRejects[arg.requestId];
});
}
+10 -3
View File
@@ -14,8 +14,15 @@ function setupSplit(left, right) {
return;
}
const leftPaneWidth = options.getInt('leftPaneWidth');
const rightPaneWidth = options.getInt('rightPaneWidth');
let leftPaneWidth = options.getInt('leftPaneWidth');
if (!leftPaneWidth || leftPaneWidth < 5) {
leftPaneWidth = 5;
}
let rightPaneWidth = options.getInt('rightPaneWidth');
if (!rightPaneWidth || rightPaneWidth < 5) {
rightPaneWidth = 5;
}
if (left && right) {
instance = Split(['#left-pane', '#center-pane', '#right-pane'], {
@@ -49,4 +56,4 @@ function setupSplit(left, right) {
export default {
setupSplit
};
};
+66 -36
View File
@@ -11,10 +11,11 @@ class TabContext extends Component {
/**
* @param {string|null} tabId
*/
constructor(tabId = null) {
constructor(tabId = null, hoistedNoteId = 'root') {
super();
this.tabId = tabId || utils.randomString(4);
this.hoistedNoteId = hoistedNoteId;
}
setEmpty() {
@@ -25,31 +26,10 @@ class TabContext extends Component {
}
async setNote(inputNotePath, triggerSwitchEvent = true) {
const noteId = treeService.getNoteIdFromNotePath(inputNotePath);
let resolvedNotePath;
const resolvedNotePath = await this.getResolvedNotePath(inputNotePath);
if ((await treeCache.getNote(noteId)).isDeleted) {
// no point in trying to resolve canonical notePath
resolvedNotePath = inputNotePath;
}
else {
resolvedNotePath = await treeService.resolveNotePath(inputNotePath);
if (!resolvedNotePath) {
logError(`Cannot resolve note path ${inputNotePath}`);
return;
}
if (resolvedNotePath === this.notePath) {
return;
}
if (await hoistedNoteService.checkNoteAccess(resolvedNotePath) === false) {
return; // note is outside of hoisted subtree and user chose not to unhoist
}
// if user choise to unhoist, cache was reloaded, but might not contain this note (since it's on unexpanded path)
await treeCache.getNote(noteId);
if (!resolvedNotePath) {
return;
}
await this.triggerEvent('beforeNoteSwitch', {tabContext: this});
@@ -57,21 +37,12 @@ class TabContext extends Component {
utils.closeActiveDialog();
this.notePath = resolvedNotePath;
this.noteId = noteId;
this.noteId = treeService.getNoteIdFromNotePath(resolvedNotePath);
this.autoBookDisabled = false;
this.textPreviewDisabled = false;
this.codePreviewDisabled = false;
setTimeout(async () => {
// we include the note into recent list only if the user stayed on the note at least 5 seconds
if (resolvedNotePath && resolvedNotePath === this.notePath) {
await server.post('recent-notes', {
noteId: this.note.noteId,
notePath: this.notePath
});
}
}, 5000);
this.saveToRecentNotes(resolvedNotePath);
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
@@ -88,6 +59,47 @@ class TabContext extends Component {
}
}
saveToRecentNotes(resolvedNotePath) {
setTimeout(async () => {
// we include the note into recent list only if the user stayed on the note at least 5 seconds
if (resolvedNotePath && resolvedNotePath === this.notePath) {
await server.post('recent-notes', {
noteId: this.note.noteId,
notePath: this.notePath
});
}
}, 5000);
}
async getResolvedNotePath(inputNotePath) {
const noteId = treeService.getNoteIdFromNotePath(inputNotePath);
if ((await treeCache.getNote(noteId)).isDeleted) {
// no point in trying to resolve canonical notePath
return inputNotePath;
}
const resolvedNotePath = await treeService.resolveNotePath(inputNotePath);
if (!resolvedNotePath) {
logError(`Cannot resolve note path ${inputNotePath}`);
return;
}
if (resolvedNotePath === this.notePath) {
return;
}
if (await hoistedNoteService.checkNoteAccess(resolvedNotePath, this) === false) {
return; // note is outside of hoisted subtree and user chose not to unhoist
}
// if user choise to unhoist, cache was reloaded, but might not contain this note (since it's on unexpanded path)
await treeCache.getNote(noteId);
return resolvedNotePath;
}
/** @property {NoteShort} */
get note() {
if (this.noteId && !(this.noteId in treeCache.notes)) {
@@ -123,10 +135,28 @@ class TabContext extends Component {
return {
tabId: this.tabId,
notePath: this.notePath,
hoistedNoteId: this.hoistedNoteId,
active: this.isActive()
}
}
async unhoist() {
await this.setHoistedNoteId('root');
}
async setHoistedNoteId(noteIdToHoist) {
if (this.notePathArray && !this.notePathArray.includes(noteIdToHoist)) {
await this.setNote(noteIdToHoist);
}
this.hoistedNoteId = noteIdToHoist;
await this.triggerEvent('hoistedNoteChanged', {
noteId: noteIdToHoist,
tabId: this.tabId
});
}
async entitiesReloadedEvent({loadResults}) {
if (loadResults.isNoteReloaded(this.noteId)) {
const note = await treeCache.getNote(this.noteId);
+31 -19
View File
@@ -58,7 +58,8 @@ export default class TabManager extends Component {
else {
tabsToOpen.push({
notePath: notePath,
active: true
active: true,
hoistedNoteId: glob.extraHoistedNoteId || 'root'
});
}
}
@@ -83,7 +84,8 @@ export default class TabManager extends Component {
if (filteredTabs.length === 0) {
filteredTabs.push({
notePath: this.isMainWindow ? 'root' : '',
active: true
active: true,
extraHoistedNoteId: glob.extraHoistedNoteId || 'root'
});
}
@@ -93,7 +95,7 @@ export default class TabManager extends Component {
await this.tabsUpdate.allowUpdateWithoutChange(async () => {
for (const tab of filteredTabs) {
await this.openTabWithNote(tab.notePath, tab.active, tab.tabId);
await this.openTabWithNote(tab.notePath, tab.active, tab.tabId, tab.hoistedNoteId);
}
});
}
@@ -184,8 +186,8 @@ export default class TabManager extends Component {
await tabContext.setEmpty();
}
async openEmptyTab(tabId) {
const tabContext = new TabContext(tabId);
async openEmptyTab(tabId, hoistedNoteId) {
const tabContext = new TabContext(tabId, hoistedNoteId);
this.child(tabContext);
await this.triggerEvent('newTabOpened', {tabContext});
@@ -193,8 +195,26 @@ export default class TabManager extends Component {
return tabContext;
}
async openTabWithNote(notePath, activate, tabId = null) {
const tabContext = await this.openEmptyTab(tabId);
/**
* If the requested notePath is within current note hoisting scope then keep the note hoisting also for the new tab.
*/
async openTabWithNoteWithHoisting(notePath) {
const tabContext = this.getActiveTabContext();
let hoistedNoteId = 'root';
if (tabContext) {
const resolvedNotePath = await treeService.resolveNotePath(notePath);
if (resolvedNotePath.includes(tabContext.hoistedNoteId)) {
hoistedNoteId = tabContext.hoistedNoteId;
}
}
return this.openTabWithNote(notePath, false, null, hoistedNoteId);
}
async openTabWithNote(notePath, activate, tabId, hoistedNoteId) {
const tabContext = await this.openEmptyTab(tabId, hoistedNoteId);
if (notePath) {
await tabContext.setNote(notePath, !activate); // if activate is false then send normal noteSwitched event
@@ -330,22 +350,14 @@ export default class TabManager extends Component {
}
moveTabToNewWindowCommand({tabId}) {
const notePath = this.getTabContextById(tabId).notePath;
const {notePath, hoistedNoteId} = this.getTabContextById(tabId);
this.removeTab(tabId);
this.triggerCommand('openInWindow', {notePath});
this.triggerCommand('openInWindow', {notePath, hoistedNoteId});
}
async hoistedNoteChangedEvent({hoistedNoteId}) {
if (hoistedNoteId === 'root') {
return;
}
for (const tc of this.tabContexts.splice()) {
if (tc.notePath && !tc.notePath.split("/").includes(hoistedNoteId)) {
await this.removeTab(tc.tabId);
}
}
hoistedNoteChangedEvent() {
this.tabsUpdate.scheduleUpdate();
}
}
+41 -17
View File
@@ -37,8 +37,6 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
path.push('root');
}
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
const effectivePath = [];
let childNoteId = null;
let i = 0;
@@ -91,10 +89,6 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
effectivePath.push(parentNoteId);
childNoteId = parentNoteId;
if (parentNoteId === hoistedNoteId) {
break;
}
}
return effectivePath.reverse();
@@ -110,7 +104,7 @@ function getSomeNotePath(note) {
while (cur.noteId !== 'root') {
path.push(cur.noteId);
const parents = cur.getParentNotes();
const parents = cur.getParentNotes().filter(note => note.type !== 'search');
if (!parents.length) {
logError(`Can't find parents for note ${cur.noteId}`);
@@ -242,10 +236,8 @@ async function getNoteTitle(noteId, parentNoteId = null) {
return title;
}
async function getNotePathTitle(notePath) {
utils.assertArguments(notePath);
const titlePath = [];
async function getNotePathTitleComponents(notePath) {
const titleComponents = [];
if (notePath.startsWith('root/')) {
notePath = notePath.substr(5);
@@ -253,20 +245,51 @@ async function getNotePathTitle(notePath) {
// special case when we want just root's title
if (notePath === 'root') {
return await getNoteTitle(notePath);
titleComponents.push(await getNoteTitle(notePath));
} else {
let parentNoteId = 'root';
for (const noteId of notePath.split('/')) {
titleComponents.push(await getNoteTitle(noteId, parentNoteId));
parentNoteId = noteId;
}
}
let parentNoteId = 'root';
return titleComponents;
}
for (const noteId of notePath.split('/')) {
titlePath.push(await getNoteTitle(noteId, parentNoteId));
async function getNotePathTitle(notePath) {
utils.assertArguments(notePath);
parentNoteId = noteId;
}
const titlePath = await getNotePathTitleComponents(notePath);
return titlePath.join(' / ');
}
async function getNoteTitleWithPathAsSuffix(notePath) {
utils.assertArguments(notePath);
const titleComponents = await getNotePathTitleComponents(notePath);
if (!titleComponents || titleComponents.length === 0) {
return "";
}
const title = titleComponents[titleComponents.length - 1];
const path = titleComponents.slice(0, titleComponents.length - 1);
const $titleWithPath = $('<span class="note-title-with-path">')
.append($('<span class="note-title">').text(title));
if (path.length > 0) {
$titleWithPath
.append($('<span class="note-path">').text(' (' + path.join(' / ') + ')'));
}
return $titleWithPath;
}
function getHashValueFromAddress() {
const str = document.location.hash ? document.location.hash.substr(1) : ""; // strip initial #
@@ -295,6 +318,7 @@ export default {
getBranchIdFromNotePath,
getNoteTitle,
getNotePathTitle,
getNoteTitleWithPathAsSuffix,
getHashValueFromAddress,
parseNotePath
};
+11 -45
View File
@@ -2,6 +2,7 @@ import Branch from "../entities/branch.js";
import NoteShort from "../entities/note_short.js";
import Attribute from "../entities/attribute.js";
import server from "./server.js";
import appContext from "./app_context.js";
import NoteComplement from "../entities/note_complement.js";
/**
@@ -20,9 +21,6 @@ class TreeCache {
async loadInitialTree() {
const resp = await server.get('tree');
// FIXME: we need to do this to cover for ascendants of template notes which are not loaded
await this.loadParents(resp, false);
// clear the cache only directly before adding new content which is important for e.g. switching to protected session
/** @type {Object.<string, NoteShort>} */
@@ -43,45 +41,11 @@ class TreeCache {
async loadSubTree(subTreeNoteId) {
const resp = await server.get('tree?subTreeNoteId=' + subTreeNoteId);
await this.loadParents(resp, true);
this.addResp(resp);
return this.notes[subTreeNoteId];
}
async loadParents(resp, additiveLoad) {
const noteIds = new Set(resp.notes.map(note => note.noteId));
const missingNoteIds = [];
const existingNotes = additiveLoad ? this.notes : {};
for (const branch of resp.branches) {
if (!(branch.parentNoteId in existingNotes) && !noteIds.has(branch.parentNoteId) && branch.parentNoteId !== 'none') {
missingNoteIds.push(branch.parentNoteId);
}
}
for (const attr of resp.attributes) {
if (attr.type === 'relation' && attr.name === 'template' && !(attr.value in existingNotes) && !noteIds.has(attr.value)) {
missingNoteIds.push(attr.value);
}
if (!(attr.noteId in existingNotes) && !noteIds.has(attr.noteId)) {
missingNoteIds.push(attr.noteId);
}
}
if (missingNoteIds.length > 0) {
const newResp = await server.post('tree/load', { noteIds: missingNoteIds });
resp.notes = resp.notes.concat(newResp.notes);
resp.branches = resp.branches.concat(newResp.branches);
resp.attributes = resp.attributes.concat(newResp.attributes);
await this.loadParents(resp, additiveLoad);
}
}
addResp(resp) {
const noteRows = resp.notes;
const branchRows = resp.branches;
@@ -111,7 +75,7 @@ class TreeCache {
}
note.children = [];
note.childToBranch = [];
note.childToBranch = {};
}
// we want to remove all "real" branches (represented in the database) since those will be created
@@ -198,7 +162,6 @@ class TreeCache {
const resp = await server.post('tree/load', { noteIds });
await this.loadParents(resp, true);
this.addResp(resp);
for (const note of resp.notes) {
@@ -209,19 +172,17 @@ class TreeCache {
throw new Error(`Search note ${note.noteId} failed: ${searchResultNoteIds}`);
}
// force to load all the notes at once instead of one by one
await this.getNotes(searchResultNoteIds);
// reset all the virtual branches from old search results
if (note.noteId in treeCache.notes) {
treeCache.notes[note.noteId].children = [];
treeCache.notes[note.noteId].childToBranch = {};
}
const branches = resp.branches.filter(b => b.noteId === note.noteId || b.parentNoteId === note.noteId);
searchResultNoteIds.forEach((resultNoteId, index) => branches.push({
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree
branchId: "virt" + resultNoteId + '-' + note.noteId,
branchId: "virt-" + note.noteId + '-' + resultNoteId,
noteId: resultNoteId,
parentNoteId: note.noteId,
notePosition: (index + 1) * 10,
@@ -236,6 +197,8 @@ class TreeCache {
});
}
}
appContext.triggerEvent('notesReloaded', {noteIds});
}
/** @return {NoteShort[]} */
@@ -263,8 +226,7 @@ class TreeCache {
console.trace(`Can't find note "${noteId}"`);
return null;
}
else {
} else {
return this.notes[noteId];
}
}).filter(note => !!note);
@@ -292,6 +254,10 @@ class TreeCache {
}
getNoteFromCache(noteId) {
if (!noteId) {
throw new Error("Empty noteId");
}
return this.notes[noteId];
}
+5 -4
View File
@@ -1,6 +1,5 @@
import treeService from './tree.js';
import treeCache from "./tree_cache.js";
import hoistedNoteService from './hoisted_note.js';
import clipboard from './clipboard.js';
import noteCreateService from "./note_create.js";
import contextMenu from "./context_menu.js";
@@ -40,7 +39,7 @@ class TreeContextMenu {
const note = await treeCache.getNote(this.node.data.noteId);
const branch = treeCache.getBranch(this.node.data.branchId);
const isNotRoot = note.noteId !== 'root';
const isHoisted = note.noteId === hoistedNoteService.getHoistedNoteId();
const isHoisted = note.noteId === appContext.tabManager.getActiveTabContext().hoistedNoteId;
const parentNote = isNotRoot ? await treeCache.getNote(branch.parentNoteId) : null;
// some actions don't support multi-note so they are disabled when notes are selected
@@ -69,7 +68,7 @@ class TreeContextMenu {
{ title: 'Search in subtree <kbd data-command="searchInSubtree"></kbd>', command: "searchInSubtree", uiIcon: "search",
enabled: notSearch && noSelectedNotes },
isHoisted ? null : { title: 'Hoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="ToggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "arrow-from-bottom" },
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "door-open" },
{ title: 'Edit branch prefix <kbd data-command="editBranchPrefix"></kbd>', command: "editBranchPrefix", uiIcon: "empty",
enabled: isNotRoot && parentNotSearch && noSelectedNotes},
{ title: "Advanced", uiIcon: "empty", enabled: true, items: [
@@ -110,7 +109,7 @@ class TreeContextMenu {
const notePath = treeService.getNotePath(this.node);
if (command === 'openInTab') {
appContext.tabManager.openTabWithNote(notePath);
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
}
else if (command === "insertNoteAfter") {
const parentNoteId = this.node.data.parentNoteId;
@@ -130,6 +129,8 @@ class TreeContextMenu {
});
}
else {
console.log("Triggering", command, notePath);
this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath});
}
}
+20 -1
View File
@@ -317,6 +317,23 @@ function timeLimit(promise, limitMs) {
});
}
function initHelpDropdown($el) {
// stop inside clicks from closing the menu
const $dropdownMenu = $el.find('.help-dropdown .dropdown-menu');
$dropdownMenu.on('click', e => e.stopPropagation());
// previous propagation stop will also block help buttons from being opened so we need to re-init for this element
initHelpButtons($dropdownMenu);
}
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/"
function initHelpButtons($el) {
$el.on("click", "*[data-help-page]", e => {
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
});
}
export default {
reloadApp,
parseDate,
@@ -355,5 +372,7 @@ export default {
copySelectionToClipboard,
isCKEditorInitialized,
dynamicRequire,
timeLimit
timeLimit,
initHelpDropdown,
initHelpButtons
};
+57 -57
View File
@@ -40,8 +40,8 @@ let consumeQueuePromise = null;
// to make sure each change event is processed only once. Not clear if this is still necessary
const processedEntityChangeIds = new Set();
function logRows(syncRows) {
const filteredRows = syncRows.filter(row =>
function logRows(entityChanges) {
const filteredRows = entityChanges.filter(row =>
!processedEntityChangeIds.has(row.id)
&& row.entityName !== 'recent_notes'
&& (row.entityName !== 'options' || row.entityId !== 'openTabs'));
@@ -59,16 +59,16 @@ async function handleMessage(event) {
}
if (message.type === 'sync') {
let syncRows = message.data;
let entityChanges = message.data;
lastPingTs = Date.now();
if (syncRows.length > 0) {
logRows(syncRows);
if (entityChanges.length > 0) {
logRows(entityChanges);
syncDataQueue.push(...syncRows);
syncDataQueue.push(...entityChanges);
// we set lastAcceptedEntityChangeId even before sync processing and send ping so that backend can start sending more updates
lastAcceptedEntityChangeId = Math.max(lastAcceptedEntityChangeId, syncRows[syncRows.length - 1].id);
lastAcceptedEntityChangeId = Math.max(lastAcceptedEntityChangeId, entityChanges[entityChanges.length - 1].id);
sendPing();
// first wait for all the preceding consumers to finish
@@ -141,13 +141,13 @@ async function runSafely(syncHandler, syncData) {
async function consumeSyncData() {
if (syncDataQueue.length > 0) {
const allSyncRows = syncDataQueue;
const allEntityChanges = syncDataQueue;
syncDataQueue = [];
const nonProcessedSyncRows = allSyncRows.filter(sync => !processedEntityChangeIds.has(sync.id));
const nonProcessedEntityChanges = allEntityChanges.filter(ec => !processedEntityChangeIds.has(ec.id));
try {
await utils.timeLimit(processSyncRows(nonProcessedSyncRows), 30000);
await utils.timeLimit(processEntityChanges(nonProcessedEntityChanges), 30000);
}
catch (e) {
logError(`Encountered error ${e.message}: ${e.stack}, reloading frontend.`);
@@ -158,17 +158,17 @@ async function consumeSyncData() {
utils.reloadApp();
}
else {
console.log("nonProcessedSyncRows causing the timeout", nonProcessedSyncRows);
console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges);
alert(`Encountered error "${e.message}", check out the console.`);
}
}
for (const syncRow of nonProcessedSyncRows) {
processedEntityChangeIds.add(syncRow.id);
for (const entityChange of nonProcessedEntityChanges) {
processedEntityChangeIds.add(entityChange.id);
}
lastProcessedEntityChangeId = Math.max(lastProcessedEntityChangeId, allSyncRows[allSyncRows.length - 1].id);
lastProcessedEntityChangeId = Math.max(lastProcessedEntityChangeId, allEntityChanges[allEntityChanges.length - 1].id);
}
checkEntityChangeIdListeners();
@@ -229,10 +229,10 @@ subscribeToMessages(message => {
}
});
async function processSyncRows(syncRows) {
async function processEntityChanges(entityChanges) {
const missingNoteIds = [];
for (const {entityName, entity} of syncRows) {
for (const {entityName, entity} of entityChanges) {
if (entityName === 'branches' && !(entity.parentNoteId in treeCache.notes)) {
missingNoteIds.push(entity.parentNoteId);
}
@@ -251,33 +251,33 @@ async function processSyncRows(syncRows) {
const loadResults = new LoadResults(treeCache);
for (const sync of syncRows.filter(sync => sync.entityName === 'notes')) {
const note = treeCache.notes[sync.entityId];
for (const ec of entityChanges.filter(ec => ec.entityName === 'notes')) {
const note = treeCache.notes[ec.entityId];
if (note) {
note.update(sync.entity);
loadResults.addNote(sync.entityId, sync.sourceId);
note.update(ec.entity);
loadResults.addNote(ec.entityId, ec.sourceId);
}
}
for (const sync of syncRows.filter(sync => sync.entityName === 'branches')) {
let branch = treeCache.branches[sync.entityId];
const childNote = treeCache.notes[sync.entity.noteId];
const parentNote = treeCache.notes[sync.entity.parentNoteId];
for (const ec of entityChanges.filter(ec => ec.entityName === 'branches')) {
let branch = treeCache.branches[ec.entityId];
const childNote = treeCache.notes[ec.entity.noteId];
const parentNote = treeCache.notes[ec.entity.parentNoteId];
if (branch) {
branch.update(sync.entity);
loadResults.addBranch(sync.entityId, sync.sourceId);
branch.update(ec.entity);
loadResults.addBranch(ec.entityId, ec.sourceId);
if (sync.entity.isDeleted) {
if (ec.entity.isDeleted) {
if (childNote) {
childNote.parents = childNote.parents.filter(parentNoteId => parentNoteId !== sync.entity.parentNoteId);
delete childNote.parentToBranch[sync.entity.parentNoteId];
childNote.parents = childNote.parents.filter(parentNoteId => parentNoteId !== ec.entity.parentNoteId);
delete childNote.parentToBranch[ec.entity.parentNoteId];
}
if (parentNote) {
parentNote.children = parentNote.children.filter(childNoteId => childNoteId !== sync.entity.noteId);
delete parentNote.childToBranch[sync.entity.noteId];
parentNote.children = parentNote.children.filter(childNoteId => childNoteId !== ec.entity.noteId);
delete parentNote.childToBranch[ec.entity.noteId];
}
}
else {
@@ -290,12 +290,12 @@ async function processSyncRows(syncRows) {
}
}
}
else if (!sync.entity.isDeleted) {
else if (!ec.entity.isDeleted) {
if (childNote || parentNote) {
branch = new Branch(treeCache, sync.entity);
branch = new Branch(treeCache, ec.entity);
treeCache.branches[branch.branchId] = branch;
loadResults.addBranch(sync.entityId, sync.sourceId);
loadResults.addBranch(ec.entityId, ec.sourceId);
if (childNote) {
childNote.addParent(branch.parentNoteId, branch.branchId);
@@ -308,14 +308,14 @@ async function processSyncRows(syncRows) {
}
}
for (const sync of syncRows.filter(sync => sync.entityName === 'note_reordering')) {
for (const ec of entityChanges.filter(ec => ec.entityName === 'note_reordering')) {
const parentNoteIdsToSort = new Set();
for (const branchId in sync.positions) {
for (const branchId in ec.positions) {
const branch = treeCache.branches[branchId];
if (branch) {
branch.notePosition = sync.positions[branchId];
branch.notePosition = ec.positions[branchId];
parentNoteIdsToSort.add(branch.parentNoteId);
}
@@ -329,20 +329,20 @@ async function processSyncRows(syncRows) {
}
}
loadResults.addNoteReordering(sync.entityId, sync.sourceId);
loadResults.addNoteReordering(ec.entityId, ec.sourceId);
}
// missing reloading the relation target note
for (const sync of syncRows.filter(sync => sync.entityName === 'attributes')) {
let attribute = treeCache.attributes[sync.entityId];
const sourceNote = treeCache.notes[sync.entity.noteId];
const targetNote = sync.entity.type === 'relation' && treeCache.notes[sync.entity.value];
for (const ec of entityChanges.filter(ec => ec.entityName === 'attributes')) {
let attribute = treeCache.attributes[ec.entityId];
const sourceNote = treeCache.notes[ec.entity.noteId];
const targetNote = ec.entity.type === 'relation' && treeCache.notes[ec.entity.value];
if (attribute) {
attribute.update(sync.entity);
loadResults.addAttribute(sync.entityId, sync.sourceId);
attribute.update(ec.entity);
loadResults.addAttribute(ec.entityId, ec.sourceId);
if (sync.entity.isDeleted) {
if (ec.entity.isDeleted) {
if (sourceNote) {
sourceNote.attributes = sourceNote.attributes.filter(attributeId => attributeId !== attribute.attributeId);
}
@@ -352,13 +352,13 @@ async function processSyncRows(syncRows) {
}
}
}
else if (!sync.entity.isDeleted) {
else if (!ec.entity.isDeleted) {
if (sourceNote || targetNote) {
attribute = new Attribute(treeCache, sync.entity);
attribute = new Attribute(treeCache, ec.entity);
treeCache.attributes[attribute.attributeId] = attribute;
loadResults.addAttribute(sync.entityId, sync.sourceId);
loadResults.addAttribute(ec.entityId, ec.sourceId);
if (sourceNote && !sourceNote.attributes.includes(attribute.attributeId)) {
sourceNote.attributes.push(attribute.attributeId);
@@ -371,24 +371,24 @@ async function processSyncRows(syncRows) {
}
}
for (const sync of syncRows.filter(sync => sync.entityName === 'note_contents')) {
delete treeCache.noteComplementPromises[sync.entityId];
for (const ec of entityChanges.filter(ec => ec.entityName === 'note_contents')) {
delete treeCache.noteComplementPromises[ec.entityId];
loadResults.addNoteContent(sync.entityId, sync.sourceId);
loadResults.addNoteContent(ec.entityId, ec.sourceId);
}
for (const sync of syncRows.filter(sync => sync.entityName === 'note_revisions')) {
loadResults.addNoteRevision(sync.entityId, sync.noteId, sync.sourceId);
for (const ec of entityChanges.filter(ec => ec.entityName === 'note_revisions')) {
loadResults.addNoteRevision(ec.entityId, ec.noteId, ec.sourceId);
}
for (const sync of syncRows.filter(sync => sync.entityName === 'options')) {
if (sync.entity.name === 'openTabs') {
for (const ec of entityChanges.filter(ec => ec.entityName === 'options')) {
if (ec.entity.name === 'openTabs') {
continue; // only noise
}
options.set(sync.entity.name, sync.entity.value);
options.set(ec.entity.name, ec.entity.value);
loadResults.addOption(sync.entity.name);
loadResults.addOption(ec.entity.name);
}
if (!loadResults.isEmpty()) {
-272
View File
@@ -1,272 +0,0 @@
import TabAwareWidget from "./tab_aware_widget.js";
import AttributeDetailWidget from "./attribute_detail.js";
import attributeRenderer from "../services/attribute_renderer.js";
import AttributeEditorWidget from "./attribute_editor.js";
import options from '../services/options.js';
import PromotedAttributesWidget from "./promoted_attributes.js";
const TPL = `
<div class="attribute-list">
<style>
.attribute-list {
margin-left: 7px;
margin-right: 7px;
position: relative;
}
.inherited-attributes-wrapper {
color: var(--muted-text-color);
max-height: 200px;
overflow: auto;
padding-bottom: 5px;
padding-left: 7px;
}
.attribute-list-editor p {
margin: 0 !important;
}
.attribute-list.error .attribute-list-editor {
border-color: red !important;
}
.attr-expander {
display: flex;
flex-direction: row;
color: var(--muted-text-color);
font-size: 90%;
margin: 3px 0 3px 0;
}
.attribute-list hr {
height: 1px;
border-color: var(--main-border-color);
position: relative;
top: 4px;
margin-top: 5px;
margin-bottom: 0;
}
.attr-expander-text {
padding-left: 20px;
padding-right: 20px;
white-space: nowrap;
}
.attr-expander:hover {
cursor: pointer;
}
.attr-expander:hover hr {
border-color: var(--main-text-color);
}
.attr-expander:hover .attr-expander-text {
color: var(--main-text-color);
}
</style>
<div class="attr-expander attr-promoted-expander">
<hr class="w-100">
<div class="attr-expander-text">Promoted attributes</div>
<hr class="w-100">
</div>
<div class="all-attr-wrapper">
<div class="promoted-attributes-placeholder"></div>
<div class="attr-expander attr-owned-and-inherited-expander">
<hr class="w-100">
<div class="attr-expander-text"></div>
<hr class="w-100">
</div>
<div class="owned-and-inherited-wrapper">
<div class="attr-editor-placeholder"></div>
<hr class="w-100 attr-inherited-empty-expander" style="margin-bottom: 10px;">
<div class="attr-expander attr-inherited-expander">
<hr class="w-100">
<div class="attr-expander-text"></div>
<hr class="w-100">
</div>
<div class="inherited-attributes-wrapper"></div>
</div>
</div>
</div>
`;
export default class AttributeListWidget extends TabAwareWidget {
constructor() {
super();
this.promotedAttributesWidget = new PromotedAttributesWidget().setParent(this);
this.attributeDetailWidget = new AttributeDetailWidget().setParent(this);
this.attributeEditorWidget = new AttributeEditorWidget(this.attributeDetailWidget).setParent(this);
this.child(this.promotedAttributesWidget, this.attributeEditorWidget, this.attributeDetailWidget);
}
doRender() {
this.$widget = $(TPL);
this.overflowing();
this.$promotedExpander = this.$widget.find('.attr-promoted-expander');
this.$allAttrWrapper = this.$widget.find('.all-attr-wrapper');
this.$promotedExpander.on('click', async () => {
const collapse = this.$allAttrWrapper.is(":visible");
await options.save('promotedAttributesExpanded', !collapse);
this.triggerEvent(`promotedAttributesCollapsedStateChanged`, {collapse});
});
this.$ownedAndInheritedWrapper = this.$widget.find('.owned-and-inherited-wrapper');
this.$ownedExpander = this.$widget.find('.attr-owned-and-inherited-expander');
this.$ownedExpander.on('click', async () => {
const collapse = this.$ownedAndInheritedWrapper.is(":visible");
await options.save('attributeListExpanded', !collapse);
this.triggerEvent(`attributeListCollapsedStateChanged`, {collapse});
});
this.$ownedExpanderText = this.$ownedExpander.find('.attr-expander-text');
this.$inheritedAttributesWrapper = this.$widget.find('.inherited-attributes-wrapper');
this.$inheritedExpander = this.$widget.find('.attr-inherited-expander');
this.$inheritedExpander.on('click', () => {
if (this.$inheritedAttributesWrapper.is(":visible")) {
this.$inheritedAttributesWrapper.slideUp(200);
}
else {
this.$inheritedAttributesWrapper.slideDown(200);
}
});
this.$inheritedExpanderText = this.$inheritedExpander.find('.attr-expander-text');
this.$inheritedEmptyExpander = this.$widget.find('.attr-inherited-empty-expander');
this.$widget.find('.promoted-attributes-placeholder').replaceWith(this.promotedAttributesWidget.render());
this.$widget.find('.attr-editor-placeholder').replaceWith(this.attributeEditorWidget.render());
this.$widget.append(this.attributeDetailWidget.render());
}
async refreshWithNote(note, updateOnly = false) {
if (!updateOnly) {
const hasPromotedAttrs = this.promotedAttributesWidget.getPromotedDefinitionAttributes().length > 0;
if (hasPromotedAttrs) {
this.$promotedExpander.show();
this.$allAttrWrapper.toggle(options.is('promotedAttributesExpanded'));
this.$ownedAndInheritedWrapper.hide();
this.$inheritedAttributesWrapper.hide();
} else {
this.$promotedExpander.hide();
this.$allAttrWrapper.show();
this.$ownedAndInheritedWrapper.toggle(options.is('attributeListExpanded'));
}
}
const ownedAttributes = note.getOwnedAttributes().filter(attr => !attr.isAutoLink);
this.$ownedExpanderText.text(ownedAttributes.length + ' owned ' + this.attrPlural(ownedAttributes.length));
const inheritedAttributes = note.getAttributes().filter(attr => attr.noteId !== this.noteId);
if (inheritedAttributes.length === 0) {
this.$inheritedExpander.hide();
this.$inheritedEmptyExpander.show();
}
else {
this.$inheritedExpander.show();
this.$inheritedEmptyExpander.hide();
}
this.$inheritedExpanderText.text(inheritedAttributes.length + ' inherited ' + this.attrPlural(inheritedAttributes.length));
this.$inheritedAttributesWrapper.empty();
await this.renderInheritedAttributes(inheritedAttributes, this.$inheritedAttributesWrapper);
}
attrPlural(number) {
return 'attribute' + (number === 1 ? '' : 's');
}
async renderInheritedAttributes(attributes, $container) {
for (const attribute of attributes) {
const $attr = (await attributeRenderer.renderAttribute(attribute, false))
.on('click', e => this.attributeDetailWidget.showAttributeDetail({
attribute: {
noteId: attribute.noteId,
type: attribute.type,
name: attribute.name,
value: attribute.value,
isInheritable: attribute.isInheritable
},
isOwned: false,
x: e.pageX,
y: e.pageY
}));
$container
.append($attr)
.append(" ");
}
}
async saveAttributesCommand() {
await this.attributeEditorWidget.save();
}
async reloadAttributesCommand() {
await this.attributeEditorWidget.refresh();
}
async updateAttributeListCommand({attributes}) {
await this.attributeEditorWidget.updateAttributeList(attributes);
}
/**
* This event is used to synchronize collapsed state of all the tab-cached widgets since they are all rendered
* separately but should behave uniformly for the user.
*/
attributeListCollapsedStateChangedEvent({collapse}) {
if (collapse) {
this.$ownedAndInheritedWrapper.slideUp(200);
} else {
this.$ownedAndInheritedWrapper.slideDown(200);
}
}
/**
* This event is used to synchronize collapsed state of all the tab-cached widgets since they are all rendered
* separately but should behave uniformly for the user.
*/
promotedAttributesCollapsedStateChangedEvent({collapse}) {
if (collapse) {
this.$allAttrWrapper.slideUp(200);
} else {
this.$allAttrWrapper.slideDown(200);
}
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
this.refreshWithNote(this.note, true);
}
}
}
@@ -1,13 +1,13 @@
import server from "../services/server.js";
import treeCache from "../services/tree_cache.js";
import treeService from "../services/tree.js";
import linkService from "../services/link.js";
import attributeAutocompleteService from "../services/attribute_autocomplete.js";
import noteAutocompleteService from "../services/note_autocomplete.js";
import promotedAttributeDefinitionParser from '../services/promoted_attribute_definition_parser.js';
import TabAwareWidget from "./tab_aware_widget.js";
import SpacedUpdate from "../services/spaced_update.js";
import utils from "../services/utils.js";
import server from "../../services/server.js";
import treeCache from "../../services/tree_cache.js";
import treeService from "../../services/tree.js";
import linkService from "../../services/link.js";
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import promotedAttributeDefinitionParser from '../../services/promoted_attribute_definition_parser.js';
import TabAwareWidget from "../tab_aware_widget.js";
import SpacedUpdate from "../../services/spaced_update.js";
import utils from "../../services/utils.js";
const TPL = `
<div class="attr-detail">
@@ -248,6 +248,8 @@ export default class AttributeDetailWidget extends TabAwareWidget {
this.$rowValue = this.$widget.find('.attr-row-value');
this.$inputValue = this.$widget.find('.attr-input-value');
this.$inputValue.on('keyup', () => this.userEditedAttribute());
this.$inputValue.on('change', () => this.userEditedAttribute());
this.$inputValue.on('autocomplete:closed', () => this.userEditedAttribute());
this.$inputValue.on('focus', () => {
attributeAutocompleteService.initLabelValueAutocomplete({
$el: this.$inputValue,
@@ -1,13 +1,13 @@
import TabAwareWidget from "./tab_aware_widget.js";
import noteAutocompleteService from "../services/note_autocomplete.js";
import server from "../services/server.js";
import contextMenuService from "../services/context_menu.js";
import attributesParser from "../services/attribute_parser.js";
import libraryLoader from "../services/library_loader.js";
import treeCache from "../services/tree_cache.js";
import attributeRenderer from "../services/attribute_renderer.js";
import noteCreateService from "../services/note_create.js";
import treeService from "../services/tree.js";
import TabAwareWidget from "../tab_aware_widget.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import server from "../../services/server.js";
import contextMenuService from "../../services/context_menu.js";
import attributesParser from "../../services/attribute_parser.js";
import libraryLoader from "../../services/library_loader.js";
import treeCache from "../../services/tree_cache.js";
import attributeRenderer from "../../services/attribute_renderer.js";
import noteCreateService from "../../services/note_create.js";
import treeService from "../../services/tree.js";
const HELP_TEXT = `
<p>To add label, just type e.g. <code>#rock</code> or if you want to add also value then e.g. <code>#year = 2020</code></p>
@@ -17,7 +17,7 @@ const HELP_TEXT = `
<p>Alternatively you can add label and relation using the <code>+</code> button on the right side.</p>`;
const TPL = `
<div style="position: relative">
<div style="position: relative; padding-top: 10px; padding-bottom: 10px">
<style>
.attribute-list-editor {
border: 0 !important;
@@ -34,7 +34,7 @@ const TPL = `
.save-attributes-button {
color: var(--muted-text-color);
position: absolute;
bottom: 3px;
bottom: 14px;
right: 25px;
cursor: pointer;
border: 1px solid transparent;
@@ -44,7 +44,7 @@ const TPL = `
.add-new-attribute-button {
color: var(--muted-text-color);
position: absolute;
bottom: 2px;
bottom: 13px;
right: 0;
cursor: pointer;
border: 1px solid transparent;
@@ -446,11 +446,8 @@ export default class AttributeEditorWidget extends TabAwareWidget {
if (!note) {
title = '[missing]';
}
else if (!note.isDeleted) {
title = note.title;
}
else {
title = note.isErased ? '[erased]' : `${note.title} (deleted)`;
title = note.isDeleted ? `${note.title} (deleted)` : note.title;
}
$el.text(title);
@@ -488,7 +485,7 @@ export default class AttributeEditorWidget extends TabAwareWidget {
});
}
else {
this.triggerCommand('focusOnDetail', {tabId: this.tabContext.tabId});
this.triggerEvent('focusOnDetail', {tabId: this.tabContext.tabId});
}
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ class BasicWidget extends Component {
}
overflowing() {
this.css('contain', 'none');
this.css('contain', 'none !important');
return this;
}
@@ -0,0 +1,164 @@
import TabAwareWidget from "./tab_aware_widget.js";
const TPL = `
<div class="section-container">
<style>
.section-container {
margin-bottom: 5px;
}
.section-title-container {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 7px;
margin-left: 10px;
margin-right: 10px;
}
.section-title {
padding-right: 10px;
padding-left: 10px;
color: var(--muted-text-color);
border-bottom: 1px solid var(--main-border-color);
}
.section-title.active {
color: var(--main-text-color);
border-bottom: 1px solid var(--main-text-color);
}
.section-title:hover {
cursor: pointer;
}
.section-title:hover {
color: var(--main-text-color);
}
.section-title-empty {
flex-shrink: 1;
flex-grow: 1;
}
.section-body {
display: none;
border-bottom: 1px solid var(--main-border-color);
margin-left: 10px;
margin-right: 10px;
}
.section-body.active {
display: block;
}
</style>
<div class="section-title-container"></div>
<div class="section-body-container"></div>
</div>`;
export default class CollapsibleSectionContainer extends TabAwareWidget {
constructor() {
super();
this.children = [];
this.positionCounter = 10;
}
child(...components) {
if (!components) {
return this;
}
super.child(...components);
for (const component of components) {
if (!component.position) {
component.position = this.positionCounter;
this.positionCounter += 10;
}
}
this.children.sort((a, b) => a.position - b.position < 0 ? -1 : 1);
return this;
}
doRender() {
this.$widget = $(TPL);
this.overflowing();
this.$titleContainer = this.$widget.find('.section-title-container');
this.$bodyContainer = this.$widget.find('.section-body-container');
for (const widget of this.children) {
this.$bodyContainer.append(
$('<div class="section-body">')
.attr('data-section-component-id', widget.componentId)
.append(widget.render())
);
}
this.$titleContainer.on('click', '.section-title-real', e => {
const $sectionTitle = $(e.target).closest('.section-title-real');
const activate = !$sectionTitle.hasClass("active");
this.$titleContainer.find('.section-title-real').removeClass("active");
this.$bodyContainer.find('.section-body').removeClass("active");
if (activate) {
const sectionComponentId = $sectionTitle.attr('data-section-component-id');
this.lastActiveComponentId = sectionComponentId;
this.$titleContainer.find(`.section-title-real[data-section-component-id="${sectionComponentId}"]`).addClass("active");
this.$bodyContainer.find(`.section-body[data-section-component-id="${sectionComponentId}"]`).addClass("active");
}
else {
this.lastActiveComponentId = null;
}
});
}
async refreshWithNote(note) {
let $sectionToActivate, $lastActiveSection;
this.$titleContainer.empty().append('<div class="section-title section-title-empty">');
for (const widget of this.children) {
const ret = widget.renderTitle(note);
if (!ret.show) {
continue;
}
const $sectionTitle = $('<div class="section-title section-title-real">')
.attr('data-section-component-id', widget.componentId)
.append(ret.$title);
this.$titleContainer.append($sectionTitle);
this.$titleContainer.append('<div class="section-title section-title-empty">');
if (ret.activate && !$sectionToActivate) {
$sectionToActivate = $sectionTitle;
}
if (this.lastActiveComponentId === widget.componentId) {
$lastActiveSection = $sectionTitle;
}
}
if (!$sectionToActivate) {
$sectionToActivate = $lastActiveSection;
}
if ($sectionToActivate) {
$sectionToActivate.trigger('click');
}
else {
this.$bodyContainer.find('.section-body').removeClass("active");
}
}
}
+21 -17
View File
@@ -3,21 +3,24 @@ import options from "../services/options.js";
const WIDGET_TPL = `
<div class="card widget">
<div class="card-header">
<div class="card-header">
<div>
<span class="widget-title">
Collapsible Group Item
</span>
<a class="widget-toggle-button no-arrow"
title="Minimize/maximize widget"
data-toggle="collapse" data-target="#[to be set]">
<span class="widget-toggle-icon bx"></span>
<span class="widget-title">
Collapsible Group Item
</span>
</a>
<span class="widget-header-actions"></span>
</div>
<div>
<a class="widget-help external no-arrow bx bx-info-circle"></a>
&nbsp;
<a class="widget-toggle-button no-arrow bx bx-minus"
title="Minimize/maximize widget"
data-toggle="collapse" data-target="#[to be set]"></a>
</div>
</div>
@@ -45,13 +48,14 @@ export default class CollapsibleWidget extends TabAwareWidget {
this.widgetName = this.widgetTitle.replace(/[^[a-zA-Z0-9]/g, "_");
this.$toggleButton = this.$widget.find('.widget-toggle-button');
this.$toggleIcon = this.$widget.find('.widget-toggle-icon');
const collapsed = options.is(this.widgetName + 'Collapsed');
if (!collapsed) {
this.$bodyWrapper.collapse("show");
}
this.updateToggleButton(collapsed);
this.updateToggleIcon(collapsed);
// using immediate variants of the event so that the previous collapse is not caught
this.$bodyWrapper.on('hide.bs.collapse', () => this.toggleCollapsed(true));
@@ -85,23 +89,23 @@ export default class CollapsibleWidget extends TabAwareWidget {
}
toggleCollapsed(collapse) {
this.updateToggleButton(collapse);
this.updateToggleIcon(collapse);
options.save(this.widgetName + 'Collapsed', collapse.toString());
this.triggerEvent(`widgetCollapsedStateChanged`, {widgetName: this.widgetName, collapse});
}
updateToggleButton(collapse) {
updateToggleIcon(collapse) {
if (collapse) {
this.$toggleButton
.addClass("bx-window")
.removeClass("bx-minus")
this.$toggleIcon
.addClass("bx-chevron-right")
.removeClass("bx-chevron-down")
.attr("title", "Show");
} else {
this.$toggleButton
.addClass("bx-minus")
.removeClass("bx-window")
this.$toggleIcon
.addClass("bx-chevron-down")
.removeClass("bx-chevron-right")
.attr("title", "Hide");
}
}
@@ -3,6 +3,18 @@ import linkService from "../../services/link.js";
import server from "../../services/server.js";
import treeCache from "../../services/tree_cache.js";
const TPL = `
<div class="edited-notes-widget">
<style>
.edited-notes-widget .edited-note-line {
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
}
</style>
</div>
`;
export default class EditedNotesWidget extends CollapsibleWidget {
get widgetTitle() { return "Edited notes on this day"; }
@@ -17,6 +29,11 @@ export default class EditedNotesWidget extends CollapsibleWidget {
&& this.note.hasOwnedLabel("dateNote");
}
async doRenderBody() {
this.$body.html(TPL);
this.$editedNotes = this.$body.find('.edited-notes-widget');
}
async refreshWithNote(note) {
// remember which title was when we found the similar notes
this.title = note.title;
@@ -33,13 +50,18 @@ export default class EditedNotesWidget extends CollapsibleWidget {
await treeCache.getNotes(noteIds, true); // preload all at once
const $list = $('<ul>');
const $list = $('<div>'); // not using <ul> because it's difficult to style correctly with text-overflow
for (const editedNote of editedNotes) {
const $item = $("<li>");
const $item = $('<div class="edited-note-line">');
if (editedNote.isDeleted) {
$item.append($("<i>").text(editedNote.title + " (deleted)"));
const title = editedNote.title + " (deleted)";
$item.append(
$("<i>")
.text(title)
.attr("title", title)
);
}
else {
$item.append(editedNote.notePath ? await linkService.createNoteLink(editedNote.notePath.join("/"), {showNotePath: true}) : editedNote.title);
@@ -48,6 +70,6 @@ export default class EditedNotesWidget extends CollapsibleWidget {
$list.append($item);
}
this.$body.empty().append($list);
this.$editedNotes.empty().append($list);
}
}
}
@@ -25,7 +25,7 @@ export default class LinkMapWidget extends CollapsibleWidget {
get headerActions() {
const $showFullButton = $("<a>")
.addClass("bx bx-map-alt")
.addClass("bx bx-network-chart")
.addClass('widget-header-action')
.attr('title', 'Show full link map');
@@ -1,4 +1,5 @@
import CollapsibleWidget from "../collapsible_widget.js";
import server from "../../services/server.js";
const TPL = `
<table class="note-info-widget-table">
@@ -38,6 +39,21 @@ const TPL = `
<th>Modified:</th>
<td class="note-info-date-modified"></td>
</tr>
<tr title="Note size provides rough estimate of storage requirements for this note. It takes into account note's content and content of its note revisions.">
<th>Note size:</th>
<td colspan="3">
<button class="btn btn-sm calculate-button" style="padding: 0px 10px 0px 10px;">
<span class="bx bx-calculator"></span> calculate
</button>
<span class="note-sizes-wrapper">
<span class="note-size"></span>
(subtree size: <span class="subtree-size"></span>)
</span>
</td>
</tr>
</table>
`;
@@ -56,6 +72,25 @@ export default class NoteInfoWidget extends CollapsibleWidget {
this.$dateModified = this.$body.find(".note-info-date-modified");
this.$type = this.$body.find(".note-info-type");
this.$mime = this.$body.find(".note-info-mime");
this.$noteSizesWrapper = this.$body.find('.note-sizes-wrapper');
this.$noteSize = this.$body.find(".note-size");
this.$subTreeSize = this.$body.find(".subtree-size");
this.$calculateButton = this.$body.find(".calculate-button");
this.$calculateButton.on('click', async () => {
this.$noteSizesWrapper.show();
this.$calculateButton.hide();
this.$noteSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
this.$subTreeSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
const noteSizeResp = await server.get(`stats/note-size/${this.noteId}`);
this.$noteSize.text(this.formatSize(noteSizeResp.noteSize));
const subTreeSizeResp = await server.get(`stats/subtree-size/${this.noteId}`);
this.$subTreeSize.text(this.formatSize(subTreeSizeResp.subTreeSize));
});
}
async refreshWithNote(note) {
@@ -78,6 +113,20 @@ export default class NoteInfoWidget extends CollapsibleWidget {
else {
this.$mime.empty();
}
this.$calculateButton.show();
this.$noteSizesWrapper.hide();
}
formatSize(size) {
size = Math.max(Math.round(size / 1024), 1);
if (size < 1024) {
return `${size} KiB`;
}
else {
return `${Math.round(size / 102.4) / 10} MiB`;
}
}
entitiesReloadedEvent({loadResults}) {
@@ -16,7 +16,7 @@ export default class WhatLinksHereWidget extends CollapsibleWidget {
get headerActions() {
const $showFullButton = $("<a>")
.addClass("bx bx-map-alt")
.addClass("bx bx-network-chart")
.addClass('widget-header-action')
.attr('title', 'Show full link map');
+38
View File
@@ -0,0 +1,38 @@
import BasicWidget from "./basic_widget.js";
export default class Container extends BasicWidget {
constructor() {
super();
this.children = [];
this.positionCounter = 10;
}
child(...components) {
if (!components) {
return this;
}
super.child(...components);
for (const component of components) {
if (!component.position) {
component.position = this.positionCounter;
this.positionCounter += 10;
}
}
this.children.sort((a, b) => a.position - b.position < 0 ? -1 : 1);
return this;
}
doRender() {
this.$widget = $(`<div>`);
for (const widget of this.children) {
this.$widget.append(widget.render());
}
}
}
+2 -33
View File
@@ -1,6 +1,6 @@
import BasicWidget from "./basic_widget.js";
import Container from "./container.js";
export default class FlexContainer extends BasicWidget {
export default class FlexContainer extends Container {
constructor(direction) {
super();
@@ -9,36 +9,5 @@ export default class FlexContainer extends BasicWidget {
}
this.attrs.style = `display: flex; flex-direction: ${direction};`;
this.children = [];
this.positionCounter = 10;
}
child(...components) {
if (!components) {
return this;
}
super.child(...components);
for (const component of components) {
if (!component.position) {
component.position = this.positionCounter;
this.positionCounter += 10;
}
}
this.children.sort((a, b) => a.position - b.position < 0 ? -1 : 1);
return this;
}
doRender() {
this.$widget = $(`<div>`);
for (const widget of this.children) {
this.$widget.append(widget.render());
}
}
}
-40
View File
@@ -1,40 +0,0 @@
import BasicWidget from "./basic_widget.js";
const WIDGET_TPL = `
<div class="global-buttons">
<style>
.global-buttons {
display: flex;
justify-content: space-around;
border: 1px solid var(--main-border-color);
border-radius: 7px;
margin: 3px 5px 5px 5px;
}
</style>
<a data-trigger-command="createTopLevelNote"
title="Create new top level note"
class="icon-action bx bx-folder-plus"></a>
<a data-trigger-command="collapseTree"
title="Collapse note tree"
class="icon-action bx bx-layer-minus"></a>
<a data-trigger-command="scrollToActiveNote"
title="Scroll to active note"
class="icon-action bx bx-crosshair"></a>
<a data-trigger-command="searchNotes"
title="Search in notes"
class="icon-action bx bx-search"></a>
</div>
`;
class GlobalButtonsWidget extends BasicWidget {
doRender() {
this.$widget = $(WIDGET_TPL);
this.overflowing();
}
}
export default GlobalButtonsWidget;
+11 -5
View File
@@ -17,9 +17,17 @@ const TPL = `
}
.global-menu button {
margin-right: 10px;
margin-right: 5px;
margin-left: 5px;
height: 34px;
border: none;
font-size: 130%;
padding-left: 10px;
padding-right: 10px;
}
.global-menu button:hover {
background-color: var(--hover-item-background-color);
}
.global-menu .dropdown-menu {
@@ -28,10 +36,8 @@ const TPL = `
</style>
<div class="dropdown global-menu">
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm" title="Menu">
<span class="bx bx-menu"></span>
Menu
<span class="caret"></span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item options-button" data-trigger-command="showOptions">
@@ -122,6 +128,6 @@ export default class GlobalMenuWidget extends BasicWidget {
this.$widget.find(".open-dev-tools-button").toggle(utils.isElectron());
this.$widget.on('click', '.dropdown-item',
() => this.$widget.find('.dropdown-toggle').dropdown('toggle'));
() => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -59,7 +59,7 @@ const TPL = `
</style>
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
Note actions
Actions
<span class="caret"></span>
</button>
<div class="dropdown-menu dropdown-menu-right">
+7 -24
View File
@@ -9,7 +9,6 @@ import EditableTextTypeWidget from "./type_widgets/editable_text.js";
import EditableCodeTypeWidget from "./type_widgets/editable_code.js";
import FileTypeWidget from "./type_widgets/file.js";
import ImageTypeWidget from "./type_widgets/image.js";
import SearchTypeWidget from "./type_widgets/search.js";
import RenderTypeWidget from "./type_widgets/render.js";
import RelationMapTypeWidget from "./type_widgets/relation_map.js";
import ProtectedSessionTypeWidget from "./type_widgets/protected_session.js";
@@ -20,13 +19,12 @@ import noteCreateService from "../services/note_create.js";
import DeletedTypeWidget from "./type_widgets/deleted.js";
import ReadOnlyTextTypeWidget from "./type_widgets/read_only_text.js";
import ReadOnlyCodeTypeWidget from "./type_widgets/read_only_code.js";
import NoneTypeWidget from "./type_widgets/none.js";
const TPL = `
<div class="note-detail">
<style>
.note-detail {
height: 100%;
min-height: 0;
font-family: var(--detail-font-family);
font-size: var(--detail-font-size);
}
@@ -43,7 +41,7 @@ const typeWidgetClasses = {
'read-only-code': ReadOnlyCodeTypeWidget,
'file': FileTypeWidget,
'image': ImageTypeWidget,
'search': SearchTypeWidget,
'search': NoneTypeWidget,
'render': RenderTypeWidget,
'relation-map': RelationMapTypeWidget,
'protected-session': ProtectedSessionTypeWidget,
@@ -75,6 +73,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$widget.on("dragover", e => e.preventDefault());
@@ -170,17 +169,6 @@ export default class NoteDetailWidget extends TabAwareWidget {
let type = note.type;
if (type === 'text' && !this.tabContext.autoBookDisabled
&& note.hasChildren()
&& utils.isDesktop()) {
const noteComplement = await this.tabContext.getNoteComplement();
if (utils.isHtmlEmpty(noteComplement.content)) {
type = 'book';
}
}
if (type === 'text' && !this.tabContext.textPreviewDisabled) {
const noteComplement = await this.tabContext.getNoteComplement();
@@ -261,7 +249,6 @@ export default class NoteDetailWidget extends TabAwareWidget {
loadCSS: [
"libraries/codemirror/codemirror.css",
"libraries/ckeditor/ckeditor-content.css",
"libraries/ckeditor/ckeditor-content.css",
"libraries/bootstrap/css/bootstrap.min.css",
"libraries/katex/katex.min.css",
"stylesheets/print.css",
@@ -272,8 +259,10 @@ export default class NoteDetailWidget extends TabAwareWidget {
});
}
hoistedNoteChangedEvent() {
this.refresh();
hoistedNoteChangedEvent({tabId}) {
if (this.isTab(tabId)) {
this.refresh();
}
}
async entitiesReloadedEvent({loadResults}) {
@@ -307,12 +296,6 @@ export default class NoteDetailWidget extends TabAwareWidget {
this.spacedUpdate.updateNowIfNecessary();
}
autoBookDisabledEvent({tabContext}) {
if (this.isTab(tabContext.tabId)) {
this.refresh();
}
}
textPreviewDisabledEvent({tabContext}) {
if (this.isTab(tabContext.tabId)) {
this.refresh();
+178
View File
@@ -0,0 +1,178 @@
import TabAwareWidget from "./tab_aware_widget.js";
import server from "../services/server.js";
import attributeService from "../services/attributes.js";
const TPL = `
<div class="note-icon-container dropdown">
<style>
.note-icon-container {
padding-top: 3px;
padding-left: 7px;
margin-right: 0;
}
.note-icon-container button {
font-size: 180%;
background: transparent;
border: 1px solid transparent;
cursor: pointer;
}
.note-icon-container button:hover {
border: 1px solid var(--main-border-color);
}
.note-icon-container .dropdown-menu {
border-radius: 10px;
border-width: 2px;
box-shadow: 10px 10px 93px -25px black;
padding: 10px 15px 10px 15px !important;
}
.note-icon-container .filter-row {
padding-top: 10px;
padding-bottom: 10px;
padding-right: 20px;
display: flex;
align-items: baseline;
}
.note-icon-container .filter-row span {
display: block;
padding-left: 15px;
padding-right: 15px;
font-weight: bold;
}
.note-icon-container .icon-list {
height: 500px;
overflow: auto;
font-size: 180%;
}
.note-icon-container .icon-list span {
display: inline-block;
padding: 10px;
cursor: pointer;
border: 1px solid transparent;
}
.note-icon-container .icon-list span:hover {
border: 1px solid var(--main-border-color);
}
</style>
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Change note icon"></button>
<div class="dropdown-menu" aria-labelledby="note-path-list-button" style="width: 610px;">
<div class="filter-row">
<span>Category:</span> <select name="icon-category" class="form-control"></select>
<span>Search:</span> <input type="text" name="icon-search" class="form-control" />
</div>
<div class="icon-list"></div>
</div>
</div>`;
let icons = [];
let categories = [];
export default class NoteIconWidget extends TabAwareWidget {
doRender() {
this.$widget = $(TPL);
this.overflowing();
this.$icon = this.$widget.find('button');
this.$iconList = this.$widget.find('.icon-list');
this.$iconList.on('click', 'span', async e => {
const clazz = $(e.target).attr('class');
await attributeService.setLabel(this.noteId,
this.note.hasOwnedLabel('workspace') ? 'workspaceIconClass' : 'iconClass',
clazz
);
});
this.$iconCategory = this.$widget.find("select[name='icon-category']");
this.$iconCategory.on('change', () => this.renderFilteredDropdown());
this.$iconCategory.on('click', e => e.stopPropagation());
this.$iconSearch = this.$widget.find("input[name='icon-search']");
this.$iconSearch.on('input', () => this.renderFilteredDropdown());
this.$notePathList = this.$widget.find(".note-path-list");
this.$widget.on('show.bs.dropdown', async () => {
const {categories} = (await import('./icon_list.js')).default;
this.$iconCategory.empty();
for (const category of categories) {
this.$iconCategory.append(
$("<option>")
.text(category.name)
.attr("value", category.id)
);
}
this.$iconSearch.val('');
this.renderDropdown();
});
}
async refreshWithNote(note) {
this.$icon.removeClass().addClass(note.getIcon());
}
async entitiesReloadedEvent({loadResults}) {
for (const attr of loadResults.getAttributes()) {
if (attr.type === 'label'
&& ['iconClass', 'workspaceIconClass'].includes(attr.name)
&& attr.isAffecting(this.note)) {
this.refresh();
break;
}
}
}
renderFilteredDropdown() {
const categoryId = parseInt(this.$iconCategory.find('option:selected').val());
const search = this.$iconSearch.val();
this.renderDropdown(categoryId, search);
}
async renderDropdown(categoryId, search) {
this.$iconList.empty();
const {icons} = (await import('./icon_list.js')).default;
for (const icon of icons) {
if (categoryId && icon.category_id !== categoryId) {
continue;
}
if (search && search.trim() && !icon.name.includes(search.trim().toLowerCase())) {
continue;
}
this.$iconList.append(
$('<span>')
.addClass(this.getIconClass(icon))
.attr("title", icon.name)
);
}
}
getIconClass(icon) {
if (icon.type_of_icon === 'LOGO') {
return "bx bxl-" + icon.name;
}
else if (icon.type_of_icon === 'SOLID') {
return "bx bxs-" + icon.name;
}
else {
return "bx bx-" + icon.name;
}
}
}
+108
View File
@@ -0,0 +1,108 @@
import TabAwareWidget from "./tab_aware_widget.js";
import NoteListRenderer from "../services/note_list_renderer.js";
const TPL = `
<div class="note-list-widget">
<style>
.note-list-widget {
flex-grow: 100000;
flex-shrink: 100000;
min-height: 0;
overflow: auto;
}
.note-list-widget .note-list {
padding: 10px;
}
</style>
<div class="note-list-widget-content">
</div>
</div>`;
export default class NoteListWidget extends TabAwareWidget {
isEnabled() {
return super.isEnabled()
&& this.note.mime !== 'text/x-sqlite;schema=trilium'
&& (
['book', 'search', 'code'].includes(this.note.type)
|| (this.note.type === 'text' && this.note.hasChildren())
)
&& !this.note.hasLabel('hideChildrenOverview');
}
doRender() {
this.$widget = $(TPL);
this.$content = this.$widget.find('.note-list-widget-content');
this.contentSized();
const observer = new IntersectionObserver(entries => {
this.isIntersecting = entries[0].isIntersecting;
this.checkRenderStatus();
}, {
rootMargin: '50px',
threshold: 0.1
});
observer.observe(this.$widget[0]);
}
checkRenderStatus() {
// console.log("this.isIntersecting", this.isIntersecting);
// console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId);
// console.log("this.shownNoteId !== this.noteId", this.shownNoteId !== this.noteId);
if (this.isIntersecting
&& this.noteIdRefreshed === this.noteId
&& this.shownNoteId !== this.noteId) {
this.shownNoteId = this.noteId;
this.renderNoteList(this.note);
}
}
async renderNoteList(note) {
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
await noteListRenderer.renderList();
}
async refresh() {
this.$content.empty();
this.shownNoteId = null;
await super.refresh();
}
/**
* We have this event so that we evaluate intersection only after note detail is loaded.
* If it's evaluated before note detail then it's clearly intersected (visible) although after note detail load
* it is not intersected (visible) anymore.
*/
noteDetailRefreshedEvent({tabId}) {
if (!this.isTab(tabId)) {
return;
}
this.noteIdRefreshed = this.noteId;
setTimeout(() => this.checkRenderStatus(), 100);
}
searchRefreshedEvent({tabId}) {
if (!this.isTab(tabId)) {
return;
}
this.noteIdRefreshed = this.noteId;
this.shownNoteId = null;
this.checkRenderStatus();
}
notesReloadedEvent({noteIds}) {
if (noteIds.includes(this.noteId)) {
this.refresh();
}
}
}
+8 -79
View File
@@ -4,52 +4,25 @@ import linkService from "../services/link.js";
import hoistedNoteService from "../services/hoisted_note.js";
const TPL = `
<div class="note-paths-widget">
<div class="dropdown note-paths-widget">
<style>
.note-paths-widget {
display: flex;
flex-direction: row;
border-bottom: 1px solid var(--main-border-color);
padding: 5px 10px 5px 10px;
}
.note-path-list a.current {
font-weight: bold;
}
.note-path-list-button {
padding: 0;
width: 24px;
height: 24px;
margin-left: 5px;
position: relative;
top: -2px;
font-size: 120% !important;
}
.note-path-list-button::after {
display: none !important; // disabling the standard caret
}
.current-path {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-path-list {
max-height: 600px;
overflow-y: auto;
}
</style>
<div class="current-path"></div>
<div class="dropdown hide-in-zen-mode">
<button class="btn btn-sm dropdown-toggle note-path-list-button bx bx-caret-down" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<ul class="note-path-list dropdown-menu dropdown-menu-right" aria-labelledby="note-path-list-button">
</ul>
</div>
<button class="btn dropdown-toggle note-path-list-button bx bx-collection" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Note paths"></button>
<ul class="note-path-list dropdown-menu dropdown-menu-right" aria-labelledby="note-path-list-button">
</ul>
</div>`;
export default class NotePathsWidget extends TabAwareWidget {
@@ -57,52 +30,8 @@ export default class NotePathsWidget extends TabAwareWidget {
this.$widget = $(TPL);
this.overflowing();
this.$currentPath = this.$widget.find('.current-path');
this.$dropdown = this.$widget.find(".dropdown");
this.$dropdownToggle = this.$widget.find('.dropdown-toggle');
this.$notePathList = this.$dropdown.find(".note-path-list");
this.$dropdown.on('show.bs.dropdown', () => this.renderDropdown());
}
async refreshWithNote(note) {
const noteIdsPath = treeService.parseNotePath(this.notePath);
this.$currentPath.empty();
let parentNoteId = 'root';
let curPath = '';
let passedHoistedNote = false;
for (let i = 0; i < noteIdsPath.length; i++) {
const noteId = noteIdsPath[i];
curPath += (curPath ? '/' : '') + noteId;
if (noteId === hoistedNoteService.getHoistedNoteId()) {
passedHoistedNote = true;
}
if (passedHoistedNote && (noteId !== hoistedNoteService.getHoistedNoteId() || noteIdsPath.length - i < 3)) {
this.$currentPath.append(
$("<a>")
.attr('href', '#' + curPath)
.attr('data-note-path', curPath)
.addClass('no-tooltip-preview')
.text(await treeService.getNoteTitle(noteId, parentNoteId))
);
if (i !== noteIdsPath.length - 1) {
this.$currentPath.append(' / ');
}
}
parentNoteId = noteId;
}
this.$dropdownToggle.dropdown('hide');
this.$notePathList = this.$widget.find(".note-path-list");
this.$widget.on('show.bs.dropdown', () => this.renderDropdown());
}
async renderDropdown() {
+7 -3
View File
@@ -9,11 +9,10 @@ const TPL = `
<style>
.note-title-container {
flex-grow: 100;
height: 34px;
}
.note-title-container input.note-title {
font-size: 150%;
font-size: 180%;
border: 0;
min-width: 5em;
width: 100%;
@@ -24,7 +23,7 @@ const TPL = `
}
</style>
<input autocomplete="off" value="" class="note-title" tabindex="100">
<input autocomplete="off" value="" placeholder="type note's title here..." class="note-title" tabindex="100">
</div>`;
export default class NoteTitleWidget extends TabAwareWidget {
@@ -42,6 +41,7 @@ export default class NoteTitleWidget extends TabAwareWidget {
doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$noteTitle = this.$widget.find(".note-title");
this.$noteTitle.on('input', () => this.spacedUpdate.scheduleUpdate());
@@ -94,6 +94,10 @@ export default class NoteTitleWidget extends TabAwareWidget {
// not updating the title specifically since the synced title might be older than what the user is currently typing
this.setProtectedStatus(this.note);
}
if (loadResults.isNoteReloaded(this.noteId, this.componentId)) {
this.refresh();
}
}
beforeUnloadEvent() {
+166 -116
View File
@@ -34,24 +34,26 @@ const TPL = `
height: 100%;
overflow: auto;
padding-bottom: 35px;
padding-top: 5px;
}
.collapse-tree-button {
position: absolute;
bottom: 10px;
right: 80px;
z-index: 100;
}
.refresh-search-button {
cursor: pointer;
position: relative;
top: -1px;
border: 1px solid transparent;
padding: 2px;
border-radius: 2px;
}
.refresh-search-button:hover {
border-color: var(--button-border-color);
.scroll-to-active-note-button {
position: absolute;
bottom: 10px;
right: 45px;
z-index: 100;
}
.tree-settings-button {
position: absolute;
top: 10px;
bottom: 10px;
right: 10px;
z-index: 100;
}
@@ -64,7 +66,7 @@ const TPL = `
padding: 20px;
z-index: 1000;
width: 320px;
border-radius: 10px 0 10px 10px;
border-radius: 10px;
}
ul.fancytree-container {
@@ -128,8 +130,33 @@ const TPL = `
span.fancytree-node.archived {
opacity: 0.6;
}
.tree-item-button {
font-size: 120%;
cursor: pointer;
border-radius: 3px;
border: 1px solid var(--main-background-color);
margin-left: 5px;
margin-top: 2px;
}
.tree-item-button:hover {
border-color: var(--main-border-color);
}
.add-note-button {
display: none;
}
span.fancytree-node:hover .add-note-button {
display: inline-block;
}
</style>
<button class="btn btn-sm icon-button bx bx-layer-minus collapse-tree-button" title="Collapse note tree" data-trigger-command="collapseTree"></button>
<button class="btn btn-sm icon-button bx bx-crosshair scroll-to-active-note-button" title="Scroll to active note" data-trigger-command="scrollToActiveNote"></button>
<button class="btn btn-sm icon-button bx bx-cog tree-settings-button" title="Tree settings"></button>
<div class="tree-settings-popup">
@@ -159,6 +186,8 @@ const TPL = `
</div>
`;
const MAX_SEARCH_RESULTS_IN_TREE = 100;
export default class NoteTreeWidget extends TabAwareWidget {
constructor(treeName) {
super();
@@ -170,8 +199,21 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.$widget = $(TPL);
this.$tree = this.$widget.find('.tree');
this.$tree.on("click", ".unhoist-button", hoistedNoteService.unhoist);
this.$tree.on("click", ".refresh-search-button", () => this.refreshSearch());
this.$tree.on("mousedown", ".unhoist-button", () => hoistedNoteService.unhoist());
this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e));
this.$tree.on("mousedown", ".add-note-button", e => {
const node = $.ui.fancytree.getNode(e);
noteCreateService.createNote(node.data.noteId, {
isProtected: node.data.isProtected
});
});
this.$tree.on("mousedown", ".enter-workspace-button", e => {
const node = $.ui.fancytree.getNode(e);
this.triggerCommand('hoistNote', {noteId: node.data.noteId});
});
// fancytree doesn't support middle click so this is a way to support it
this.$tree.on('mousedown', '.fancytree-title', e => {
@@ -181,7 +223,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const notePath = treeService.getNotePath(node);
if (notePath) {
appContext.tabManager.openTabWithNote(notePath);
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
}
e.stopPropagation();
@@ -205,7 +247,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
let top = this.$treeSettingsButton[0].offsetTop;
let left = this.$treeSettingsButton[0].offsetLeft;
top += this.$treeSettingsButton.outerHeight();
top -= this.$treeSettingsPopup.outerHeight() + 10;
left += this.$treeSettingsButton.outerWidth() - this.$treeSettingsPopup.outerWidth();
if (left < 0) {
@@ -290,7 +332,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.$tree.fancytree({
titlesTabbable: true,
keyboard: true,
extensions: ["dnd5", "clones"],
extensions: ["dnd5", "clones", "filter"],
source: treeData,
scrollOfs: {
top: 100,
@@ -311,7 +353,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
else if (event.ctrlKey) {
const notePath = treeService.getNotePath(node);
appContext.tabManager.openTabWithNote(notePath);
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
}
else if (data.node.isActive()) {
// this is important for single column mobile view, otherwise it's not possible to see again previously displayed note
@@ -336,13 +378,19 @@ export default class NoteTreeWidget extends TabAwareWidget {
await activeTabContext.setNote(notePath);
if (utils.isMobile()) {
this.triggerCommand('setActiveScreen', {screen:'detail'});
this.triggerCommand('setActiveScreen', {screen: 'detail'});
}
},
expand: (event, data) => this.setExpanded(data.node.data.branchId, true),
collapse: (event, data) => this.setExpanded(data.node.data.branchId, false),
filter: {
counter: false,
mode: "hide",
autoExpand: true
},
dnd5: {
autoExpandMS: 600,
preventLazyParents: false,
dragStart: (node, data) => {
// don't allow dragging root node
if (node.data.noteId === hoistedNoteService.getHoistedNoteId()
@@ -430,9 +478,19 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
data.result = treeCache.reloadNotes([noteId]).then(() => {
const note = treeCache.getNoteFromCache(noteId);
const note = treeCache.getNoteFromCache(noteId);
return this.prepareChildren(note);
let childNoteIds = note.getChildNoteIds();
if (childNoteIds.length > MAX_SEARCH_RESULTS_IN_TREE) {
childNoteIds = childNoteIds.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
}
return treeCache.getNotes(childNoteIds);
}).then(() => {
const note = treeCache.getNoteFromCache(noteId);
return this.prepareChildren(note);
});
}
else {
@@ -444,29 +502,46 @@ export default class NoteTreeWidget extends TabAwareWidget {
},
enhanceTitle: async function (event, data) {
const node = data.node;
const $span = $(node.span);
if (node.data.noteId !== 'root'
&& node.data.noteId === hoistedNoteService.getHoistedNoteId()
&& $span.find('.unhoist-button').length === 0) {
const action = await keyboardActionsService.getAction('unhoist');
let shortcuts = action.effectiveShortcuts.join(',');
shortcuts = shortcuts ? `(${shortcuts})` : '';
const unhoistButton = $(`<span class="unhoist-button-wrapper" title="Unhoist current note to show the whole note tree ${shortcuts}">[<a class="unhoist-button">unhoist</a>]</span>`);
// prepending since appending could push out (when note title is too long)
// the button too much to the right so that it's not visible
$span.prepend(unhoistButton);
if (!node.data.noteId) {
// if there's "non-note" node, then don't enhance
// this can happen for e.g. "Load error!" node
return;
}
const note = await treeCache.getNote(node.data.noteId);
const activeTabContext = appContext.tabManager.getActiveTabContext();
if (note.type === 'search' && $span.find('.refresh-search-button').length === 0) {
const refreshSearchButton = $('<span>&nbsp; <span class="refresh-search-button bx bx-refresh" title="Refresh saved search results"></span></span>');
const $span = $(node.span);
$span.append(refreshSearchButton);
$span.find('.tree-item-button').remove();
const isHoistedNote = activeTabContext && activeTabContext.hoistedNoteId === note.noteId && note.noteId !== 'root';
if (isHoistedNote) {
const $unhoistButton = $('<span class="tree-item-button unhoist-button bx bx-door-open" title="Unhoist"></span>');
$unhoistButton.on('click', () => alert("bebe"));
$span.append($unhoistButton);
}
if (note.hasLabel('workspace') && !isHoistedNote) {
const $enterWorkspaceButton = $('<span class="tree-item-button enter-workspace-button bx bx-door-open" title="Hoist this note (workspace)"></span>');
$span.append($enterWorkspaceButton);
}
if (note.type === 'search') {
const $refreshSearchButton = $('<span class="tree-item-button refresh-search-button bx bx-refresh" title="Refresh saved search results"></span>');
$span.append($refreshSearchButton);
}
if (note.type !== 'search') {
const $createChildNoteButton = $('<span class="tree-item-button add-note-button bx bx-plus" title="Create child note"></span>');
$span.append($createChildNoteButton);
}
},
// this is done to automatically lazy load all expanded notes after tree load
@@ -510,19 +585,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
prepareRootNode() {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
let hoistedBranch;
if (hoistedNoteId === 'root') {
hoistedBranch = treeCache.getBranch('root');
}
else {
const hoistedNote = treeCache.getNoteFromCache(hoistedNoteId);
hoistedBranch = hoistedNote.getBranches()[0];
}
return this.prepareNode(hoistedBranch);
return this.prepareNode(treeCache.getBranch('root'));
}
/**
@@ -535,7 +598,13 @@ export default class NoteTreeWidget extends TabAwareWidget {
const hideArchivedNotes = this.hideArchivedNotes;
for (const branch of this.getChildBranches(parentNote)) {
let childBranches = parentNote.getFilteredChildBranches();
if (childBranches.length > MAX_SEARCH_RESULTS_IN_TREE) {
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
}
for (const branch of childBranches) {
if (hideArchivedNotes) {
const note = branch.getNoteFromCache();
@@ -552,32 +621,20 @@ export default class NoteTreeWidget extends TabAwareWidget {
return noteList;
}
getIcon(note, isFolder) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
if (note.noteId !== 'root' && note.noteId === hoistedNoteId) {
return "bx bxs-arrow-from-bottom";
}
return note.getIcon(isFolder);
}
updateNode(node) {
const note = treeCache.getNoteFromCache(node.data.noteId);
const branch = treeCache.getBranch(node.data.branchId);
const isFolder = this.isFolder(note);
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
node.data.isProtected = note.isProtected;
node.data.noteType = note.type;
node.folder = isFolder;
node.icon = this.getIcon(note, isFolder);
node.folder = note.isFolder();
node.icon = note.getIcon();
node.extraClasses = this.getExtraClasses(note);
node.title = utils.escapeHtml(title);
if (node.isExpanded() !== branch.isExpanded) {
node.setExpanded(branch.isExpanded, {noEvents: true});
node.setExpanded(branch.isExpanded, {noEvents: true, noAnimation: true});
}
node.renderTitle();
@@ -594,9 +651,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
const isFolder = this.isFolder(note);
const isFolder = note.isFolder();
const node = {
noteId: note.noteId,
@@ -606,11 +662,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
noteType: note.type,
title: utils.escapeHtml(title),
extraClasses: this.getExtraClasses(note),
icon: this.getIcon(note, isFolder),
icon: note.getIcon(isFolder),
refKey: note.noteId,
lazy: true,
folder: isFolder,
expanded: (branch.isExpanded || hoistedNoteId === note.noteId) && note.type !== 'search',
expanded: branch.isExpanded && note.type !== 'search',
key: utils.randomString(12) // this should prevent some "duplicate key" errors
};
@@ -621,34 +677,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
return node;
}
isFolder(note) {
return note.type === 'search'
|| this.getChildBranches(note).length > 0;
}
getChildBranches(parentNote) {
let childBranches = parentNote.getChildBranches();
if (!childBranches) {
ws.logError(`No children for ${parentNote}. This shouldn't happen.`);
return;
}
if (this.hideIncludedImages) {
const imageLinks = parentNote.getRelations('imageLink');
// image is already visible in the parent note so no need to display it separately in the book
childBranches = childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId));
}
// we're not checking hideArchivedNotes since that would mean we need to lazy load the child notes
// which would seriously slow down everything.
// we check this flag only once user chooses to expand the parent. This has the negative consequence that
// note may appear as folder but not contain any children when all of them are archived
return childBranches;
}
getExtraClasses(note) {
utils.assertArguments(note);
@@ -711,13 +739,14 @@ export default class NoteTreeWidget extends TabAwareWidget {
const {branchIds} = await server.put(`branches/${node.data.branchId}/expanded-subtree/${isExpanded ? 1 : 0}`);
treeCache.getBranches(branchIds, true).forEach(branch => branch.isExpanded = isExpanded);
treeCache.getBranches(branchIds, true)
.forEach(branch => branch.isExpanded = !!isExpanded);
await this.batchUpdate(async () => {
await node.load(true);
if (node.data.noteId !== 'root') { // root is always expanded
await node.setExpanded(isExpanded, {noEvents: true});
await node.setExpanded(isExpanded, {noEvents: true, noAnimation: true});
}
});
}
@@ -730,6 +759,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
await this.setExpandedStatusForSubtree(node, false);
}
collapseTreeCommand() { this.collapseTree(); }
/**
* @return {FancytreeNode|null}
*/
@@ -752,7 +783,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
}
async scrollToActiveNoteEvent() {
async scrollToActiveNoteCommand() {
const activeContext = appContext.tabManager.getActiveTabContext();
if (activeContext && activeContext.notePath) {
@@ -798,7 +829,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
if (expand) {
await parentNode.setExpanded(true);
await parentNode.setExpanded(true, {noAnimation: true});
// although previous line should set the expanded status, it seems to happen asynchronously
// so we need to make sure it is set properly before calling updateNode which uses this flag
@@ -862,9 +893,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null
}
// must be event since it's triggered from outside the tree
collapseTreeEvent() { this.collapseTree(); }
isEnabled() {
return !!this.tabContext;
}
@@ -897,13 +925,15 @@ export default class NoteTreeWidget extends TabAwareWidget {
newActiveNode.makeVisible({scrollIntoView: true});
}
}
this.filterHoistedBranch();
}
async refreshSearch() {
const activeNode = this.getActiveNode();
async refreshSearch(e) {
const activeNode = $.ui.fancytree.getNode(e);
activeNode.load(true);
activeNode.setExpanded(true);
activeNode.setExpanded(true, {noAnimation: true});
toastService.showMessage("Saved search note refreshed.");
}
@@ -971,7 +1001,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const noteIdsToReload = new Set();
for (const attr of loadResults.getAttributes()) {
if (attr.type === 'label' && ['iconClass', 'cssClass'].includes(attr.name)) {
if (attr.type === 'label' && ['iconClass', 'cssClass', 'workspace', 'workspaceIconClass', 'archived'].includes(attr.name)) {
if (attr.isInheritable) {
noteIdsToReload.add(attr.noteId);
}
@@ -1102,6 +1132,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
await newActiveNode.setFocus(true);
}
}
if (noteIdsToReload.size > 0 || noteIdsToUpdate.size > 0) {
// workaround for https://github.com/mar10/fancytree/issues/1054
this.filterHoistedBranch();
}
}
sortChildren(node) {
@@ -1117,7 +1152,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
});
}
async setExpanded(branchId, isExpanded) {
setExpanded(branchId, isExpanded) {
utils.assertArguments(branchId);
const branch = treeCache.getBranch(branchId, true);
@@ -1135,7 +1170,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
branch.isExpanded = isExpanded;
await server.put(`branches/${branchId}/expanded/${isExpanded ? 1 : 0}`);
server.put(`branches/${branchId}/expanded/${isExpanded ? 1 : 0}`);
}
async reloadTreeFromCache() {
@@ -1156,8 +1191,21 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
}
hoistedNoteChangedEvent() {
this.reloadTreeFromCache();
async hoistedNoteChangedEvent({tabId}) {
if (this.isTab(tabId)) {
this.filterHoistedBranch();
}
}
filterHoistedBranch() {
if (this.tabContext) {
if (this.tabContext.hoistedNoteId === 'root') {
this.tree.clearFilter();
}
else {
this.tree.filterBranches(node => node.data.noteId === this.tabContext.hoistedNoteId);
}
}
}
treeCacheReloadedEvent() {
@@ -1171,7 +1219,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts) {
hotKeyMap[utils.normalizeShortcut(shortcut)] = node => {
this.triggerCommand(action.actionName, {node});
const notePath = treeService.getNotePath(node);
this.triggerCommand(action.actionName, {node, notePath});
return false;
}

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