Compare commits

..

218 Commits

Author SHA1 Message Date
zadam 631a75deec release 0.39.2-beta 2019-12-30 19:48:54 +01:00
zadam 3f1d0e5872 small template refactoring 2019-12-30 19:46:48 +01:00
zadam 0fe91d0184 include note feature 2019-12-30 19:32:45 +01:00
zadam 2f711a12f8 added "include note" widget to text notes, WIP 2019-12-29 23:46:40 +01:00
zadam 64f32ba38f fix zen mode with new layout, closes #794 2019-12-29 20:48:24 +01:00
zadam 7db4859fb9 Merge remote-tracking branch 'origin/master' 2019-12-29 10:19:11 +01:00
zadam eee9fcae5c set timeout for the initial sync seed request 2019-12-29 10:19:00 +01:00
zadam 9c4a976342 added some statistic scripts to demo document 2019-12-28 21:52:35 +01:00
zadam e4a09c6207 fix left pane scrolling, closes #788 2019-12-28 21:37:13 +01:00
zadam d467db2227 unify API for creating note links 2019-12-28 21:10:02 +01:00
zadam b8d6ff0542 reset file inputs to allow reuploading the same file again 2019-12-28 19:29:52 +01:00
zadam a9b8e65c9b force scrolling when width of the content overflows 2019-12-28 19:18:44 +01:00
zadam bb8b563ece relaunch app after successful sync 2019-12-28 12:55:53 +01:00
zadam 05a8ffb944 small fixes 2019-12-28 10:28:12 +01:00
zadam 2502646a64 release 0.39.1-beta 2019-12-27 21:21:57 +01:00
zadam 3d95d69f80 styling hack for the design to look correct in both FF and chrome 2019-12-27 21:18:14 +01:00
zadam df751f5d67 fix icon 2019-12-27 21:11:56 +01:00
zadam 4f06b6de78 fix registration of global shortcuts, fixes #786 2019-12-27 20:28:27 +01:00
zadam d2177cd517 fix detection of desktop build in setup page, closes #787 2019-12-27 20:22:46 +01:00
zadam 0affcf5ad2 move electron-installer-debian to optional dependencies since it can't be installed on windows, #783 2019-12-26 10:02:55 +01:00
zadam 7a416b107b fix electron dep issue in the server version, closes #784 2019-12-25 19:38:28 +01:00
zadam 1ff124dab7 release 0.39.0-beta 2019-12-25 12:18:52 +01:00
zadam 4cb511bad0 fix import with style.css 2019-12-25 12:09:49 +01:00
zadam 73c8d145fa tar export fixes + added code block example to the demo document 2019-12-25 11:56:55 +01:00
zadam ab79f24729 added content style to tar html export as well 2019-12-25 11:34:45 +01:00
zadam cec71f65b3 load ckeditor content style for printing to have more similar look to the in-editor, #782 2019-12-25 10:59:45 +01:00
zadam f75c008154 more sync recovery fixes 2019-12-24 17:49:44 +01:00
zadam 474baa7d95 sync recovery fixes 2019-12-24 16:00:31 +01:00
zadam a155b6e8d5 create separate window for setup and then main window 2019-12-24 14:42:03 +01:00
zadam 229974e543 added option to enable native title bar (disabled by default) 2019-12-24 12:10:32 +01:00
zadam 6fc19bfb93 Merge branch 'stable' 2019-12-24 11:55:27 +01:00
zadam ccaa108faa release 0.38.3 2019-12-24 10:51:51 +01:00
zadam 0a72383495 make opening new links from ckeditor more consistent for internal and external links, closes #779 2019-12-24 10:49:16 +01:00
zadam d389100611 properly cleanup sidebar content after closing tab 2019-12-23 21:46:37 +01:00
zadam ea7257a5b2 cleaned up experimental attribute pane 2019-12-23 21:13:56 +01:00
zadam 0ebc947fbd visual tweaks 2019-12-23 21:05:47 +01:00
zadam c89514f9bb saving size and visibility state of the panes 2019-12-23 20:34:29 +01:00
zadam e0368e395c removed left and right sidebar sizing options 2019-12-23 19:45:59 +01:00
zadam 6986c201dd removed hideTabRowForOneTab option 2019-12-23 19:39:48 +01:00
zadam bcf163f8a1 css alignment 2019-12-23 17:12:17 +01:00
zadam 15aaead7b9 show/hide switcher for the sidebar 2019-12-23 16:48:34 +01:00
zadam d29c5c4758 sidebar pulled outside of the tab content and added splitter 2019-12-23 15:50:24 +01:00
zadam 81e2baeee5 blur title buttons after clicking 2019-12-23 13:46:26 +01:00
zadam 4cececafc9 added title bar buttons 2019-12-23 13:34:54 +01:00
zadam 7c8e7a3f4b frameless design with tabs on top, split for left panel 2019-12-23 11:52:45 +01:00
zadam 613d5f93e8 convert css grid design to flex based one 2019-12-23 08:52:57 +01:00
zadam 4f5b23fbf8 tar file export now sets mtime to tar records based on utcDateCreated of a note, closes #487 2019-12-22 10:57:55 +01:00
zadam a37b9cfc7b steel blue tweaks in demo document 2019-12-21 15:48:36 +01:00
zadam 2bc18bc214 use main border color for tab border 2019-12-21 15:37:51 +01:00
zadam f31a998c5d path list contains a button to add a new path, closes #611 2019-12-21 13:55:13 +01:00
zadam 5552917533 Merge branch 'stable' 2019-12-21 13:39:12 +01:00
zadam a9702aa6a2 fix empty checkbox visibility, closes #775 2019-12-21 12:37:24 +01:00
zadam d1941cc650 Merge branch 'stable' 2019-12-20 21:00:30 +01:00
zadam f98fa4098f clearer WS error message 2019-12-20 20:17:58 +01:00
zadam 5350496ed4 fix creating note from global ctrl+alt+p shortcut, closes #773 2019-12-20 20:13:21 +01:00
zadam b62d79044a fix creating note from relation map, closes #771 2019-12-20 19:02:52 +01:00
zadam 0db3722ec2 package updates 2019-12-20 18:04:05 +01:00
zadam d47403c0e7 implemented sync hash check recovery process 2019-12-18 22:58:30 +01:00
zadam 77311954a1 added sectors for contect check computation 2019-12-18 22:24:13 +01:00
zadam b7cf4fe96b more general filter parsing 2019-12-18 20:51:48 +01:00
zadam 6d9b702d4c Merge branch 'master' into stable 2019-12-18 20:49:24 +01:00
zadam 6e4c30571c release 0.38.2 2019-12-18 20:21:06 +01:00
zadam 5988776b7e styling of active button 2019-12-18 20:16:11 +01:00
zadam 384da60953 fix regex for parsing out the filters 2019-12-18 19:56:53 +01:00
zadam 21fab412cb sync error mitigation 2019-12-17 22:17:03 +01:00
zadam eb4dfbad92 sync fixes 2019-12-16 22:47:07 +01:00
zadam aff9ce97ee small sync fixes 2019-12-16 22:00:44 +01:00
zadam b0a3f828fb release 0.38.1-beta 2019-12-10 23:09:02 +01:00
zadam 76f5736255 update demo document 2019-12-10 23:08:50 +01:00
zadam a82066d899 ability to define a keyboard shortcut for paste markdown, closes #761 2019-12-10 23:04:18 +01:00
zadam 45c5287d53 protection against note switching race conditions 2019-12-10 22:35:24 +01:00
zadam dce54c7af3 run consistency checks on demand 2019-12-10 22:03:00 +01:00
zadam ee15db0ae1 for title/content search does not make sense to search for protected notes 2019-12-10 21:40:53 +01:00
zadam c48dbb0913 more debugging info for problems after sync 2019-12-10 21:31:24 +01:00
zadam 882ebdbd8f revert unicode regex since it's still not supported by ff 2019-12-09 23:08:32 +01:00
zadam 6f32d6fabe new mechanism to wait for sync after interaction with backend in Script API using api.waitForMaxKnownSyncId() 2019-12-09 23:07:45 +01:00
zadam 1e123f2390 sql console tweaks 2019-12-09 21:31:38 +01:00
zadam b29155775e Merge branch 'stable'
# Conflicts:
#	package-lock.json
2019-12-09 20:23:22 +01:00
zadam b821ed28fc refactor mention setup 2019-12-09 20:09:55 +01:00
zadam fdb8959aa1 move mime types loading to avoid race conditions 2019-12-09 20:06:36 +01:00
zadam 7554cb057b support unicode characters in filters without quotes, fixes #757 2019-12-09 19:38:48 +01:00
zadam fab959539a ability to run multiple queries (and get multiple result sets) from SQL console 2019-12-08 11:20:44 +01:00
zadam afe44a6fe8 Merge branch 'stable' 2019-12-08 10:37:19 +01:00
zadam 7ed526beb7 fix clipper, closes #752 2019-12-08 09:41:31 +01:00
zadam af695802e3 codeNotesMimeTypes option has not been created for new documents, fixes #755 2019-12-08 09:12:42 +01:00
zadam 156f040880 added "backend log" dialog 2019-12-05 21:25:36 +01:00
zadam cf6f04defb better port error logging 2019-12-04 22:59:26 +01:00
zadam a890b91079 release 0.38.0-beta 2019-12-04 22:40:31 +01:00
zadam e0aabe4f9c fix docs 2019-12-04 22:40:20 +01:00
zadam 01cd9d8fb3 ckeditor 16 with code blocks plugin 2019-12-04 21:21:07 +01:00
zadam af10f0f52a API docs updates 2019-12-03 22:53:17 +01:00
zadam aa5ede5039 Merge branch 'stable' 2019-12-03 22:52:37 +01:00
zadam 21e77c83fc release 0.37.8 2019-12-03 22:31:20 +01:00
zadam b0310e34e2 fix CSS loading from subdomain, #741 2019-12-03 22:13:02 +01:00
zadam 9812b9c272 (mostly) backwards compatible .createNote() backend API 2019-12-03 21:31:46 +01:00
zadam 92d5f91aa6 Merge branch 'stable' 2019-12-03 20:33:42 +01:00
zadam b0368c7f17 Merge remote-tracking branch 'origin/master' 2019-12-03 20:32:01 +01:00
zadam 8e1f8c869b new @parentCount virtual search attribute, closes #738 2019-12-03 20:31:34 +01:00
nil0x42 4688cda493 [DOC] createNewNote(): Add missing render type (#740) 2019-12-03 19:49:27 +01:00
zadam 761c51069a use max-height for promoted attributes, #739 2019-12-03 19:47:53 +01:00
zadam 4dc285d84f serve favicon from relative path 2019-12-03 19:31:58 +01:00
zadam 0e2f8b5734 don't set date modified on erasing 2019-12-03 19:10:40 +01:00
zadam a1402c7c66 release 0.37.7 2019-12-02 23:09:42 +01:00
zadam 6ba3e5ab7f backport fix from master to avoid doubled attributes inherited from multiple paths 2019-12-02 23:07:19 +01:00
zadam f740e52ebf correctly respect label @disableVersioning
(cherry picked from commit dc063983ea)
2019-12-02 23:06:06 +01:00
zadam e9454e4db7 fix SQL console scrolling
(cherry picked from commit 749bb90713)
2019-12-02 23:05:05 +01:00
zadam 749bb90713 fix SQL console scrolling 2019-12-02 23:04:22 +01:00
zadam eb8c296e62 attempt to make updating clients via websocket faster 2019-12-02 22:27:06 +01:00
zadam dc063983ea correctly respect label @disableVersioning 2019-12-02 20:21:52 +01:00
zadam 2595c3ac31 fix attribute loading CTE + don't duplicate attributes in case of cloning, fixes #735 2019-12-02 20:10:10 +01:00
zadam 9cb8bc5dd8 generate document now creates note revisions as well 2019-12-02 19:52:58 +01:00
zadam 3b690f5456 mini optimizations 2019-12-01 15:01:09 +01:00
zadam 7ef2e7769f added index to source_ids 2019-12-01 14:39:24 +01:00
zadam 4c07ac4c4c optimized edited notes on day query 2019-12-01 14:30:59 +01:00
zadam 35cd7f3261 optimization of recursive CTE attribute query 2019-12-01 13:29:39 +01:00
zadam 8c3e2e5eb7 better placement of slow query detection 2019-12-01 12:51:47 +01:00
zadam d57caee0d3 drop unnecessary indexes 2019-12-01 12:38:07 +01:00
zadam 6e83980784 aligning frontend attributes API with the backend one 2019-12-01 12:22:22 +01:00
zadam 295af1f43e adding file length limit to tar export 2019-12-01 11:49:14 +01:00
zadam ed2afe5c20 Merge branch 'stable' 2019-12-01 11:32:33 +01:00
zadam bfc7570e14 don't convert MD to HTML if "import markdown as text" is not selected, closes #733 2019-12-01 11:27:22 +01:00
zadam d9b9d730bb moving from inherited attribute queries to owned one where it makes sense 2019-12-01 11:10:04 +01:00
zadam ba8a8dca7b adding more "owned" attribute methods to Note entity 2019-12-01 10:57:28 +01:00
zadam 29eb5a8435 Merge branch 'stable'
# Conflicts:
#	src/services/notes.js
#	src/tools/generate_document.js
2019-12-01 10:32:30 +01:00
zadam 5de92171a7 use owned attributes where it's a better fit 2019-12-01 10:28:05 +01:00
zadam 29c5e394ab generate document now creates also labels and relations 2019-12-01 10:20:18 +01:00
zadam 07b3d11fe5 fix generate new document script 2019-12-01 09:19:16 +01:00
zadam 67663fba50 fixes 2019-11-30 11:36:36 +01:00
zadam 995ebbf577 removed foreign keys PRAGMAs since foreign key constraints are not used anymore 2019-11-30 10:41:53 +01:00
zadam d0e6be3e0c entity stat as part of consistency checks 2019-11-30 09:15:08 +01:00
zadam 01370a5968 fix anonymization according to latest schema 2019-11-29 21:42:24 +01:00
zadam 6c561b5764 rename API .createNote() to .createNewNote() to allow future backwards compatibility 2019-11-28 22:35:55 +01:00
zadam 2953f1bdb8 adding some standard classes for tree items 2019-11-28 22:05:27 +01:00
zadam 1c057cac75 small script API additions 2019-11-27 23:07:10 +01:00
zadam 0415efd33b create year/month/date labels/relation only when the note is created 2019-11-27 19:42:10 +01:00
zadam e58dc829f5 bootstrap 4.4.0 2019-11-27 18:59:42 +01:00
zadam 90d10c1ff3 Merge branch 'stable'
# Conflicts:
#	package-lock.json
#	src/public/javascripts/services/tree_context_menu.js
#	src/services/import/enex.js
2019-11-27 18:54:49 +01:00
zadam 5b30291601 release 0.37.6 2019-11-26 22:50:08 +01:00
zadam 5193f073e9 if there's no updated field use created #725 2019-11-26 22:47:54 +01:00
zadam 6c7d8a9667 preserve dateCreated and dateModified in ENEX import, fixes #725 2019-11-26 22:02:21 +01:00
zadam 5e9bedd903 fixed sidebar switch in the options dialog 2019-11-26 20:46:49 +01:00
zadam e712990c03 don't remove active tab after deleting note to preserve tab state, fixes #727 2019-11-26 20:42:34 +01:00
zadam 91487b338a make the context menu scrollable when exceeding total window height, closes #723 2019-11-26 19:55:52 +01:00
zadam 3ff24d53e5 fix decrypting note titles on the server installation, closes #724 2019-11-26 19:49:52 +01:00
zadam 94c904fb40 fix context menu over root, closes #726 2019-11-26 19:42:47 +01:00
zadam 5f258fbbbf release 0.37.5 2019-11-25 22:46:15 +01:00
zadam 56c7b7f5bd API docs update 2019-11-25 22:45:09 +01:00
zadam bf9ad976b9 fixing non-root path issues, #404 2019-11-25 21:44:46 +01:00
zadam 19206d1e0d backend API note creation updates 2019-11-25 21:24:41 +01:00
zadam 420be6d8c6 fix demo document relation map and shortcuts before db initialization 2019-11-24 22:42:54 +01:00
zadam dbd2040bee shortcut fixes 2019-11-24 22:15:33 +01:00
zadam 1e979d71c7 changed note selection in tree using keyboard - now only sibling nodes are selected 2019-11-24 22:01:10 +01:00
zadam 6bbd4c59bc reduce flicker of "create new day note" 2019-11-24 21:40:50 +01:00
zadam 3a54d00e2b added shortcut filter in the options dialog 2019-11-24 20:20:13 +01:00
zadam 499c9a7381 separated some context menu items into "advanced" 2019-11-24 20:00:54 +01:00
zadam cd139bdd76 note can be activated through 'keyboardShortcut' label 2019-11-24 18:32:18 +01:00
zadam 60c3b5cccc Merge remote-tracking branch 'origin/master' 2019-11-24 14:10:19 +01:00
zadam 540f9f933a update demo.tar with correct system links 2019-11-24 14:10:11 +01:00
Benoit Stahl a59943094e this input is text (#719) 2019-11-24 10:44:09 +01:00
zadam c400a7143c reorganization of shortcuts in the options 2019-11-24 10:40:18 +01:00
zadam 1f37d00e42 updating also shortcuts in element titles 2019-11-24 10:14:30 +01:00
zadam d21e824343 binding remaining actions 2019-11-24 09:50:19 +01:00
zadam ff3f0ee0a0 Merge branch 'stable' 2019-11-23 23:07:07 +01:00
zadam 01ff34b5d4 moving edit branch prefix, search in subtree and toggle note hoisting to global entrypoints instead of being tree specific 2019-11-23 23:06:25 +01:00
zadam 0cde7ede24 updated note menu with current shortcuts 2019-11-23 22:56:35 +01:00
zadam 92cb723d0c updated inapp help with current shortcuts 2019-11-23 22:17:08 +01:00
zadam e4bec265c1 system links (internal, image, relation map) should follow camelCase convention used for other attributes 2019-11-23 20:54:49 +01:00
zadam 434d8ef48c added extra autofixers for completely missing note_contents or note_revision_contents row 2019-11-23 19:56:52 +01:00
zadam c8ba07a4ae fix migration script in case of not fully consistent database (missing note_contents for note). closes #717 2019-11-23 11:13:57 +01:00
zadam 4da6234911 Merge branch 'stable' 2019-11-22 22:59:08 +01:00
zadam 38e7649ac3 release 0.37.4 2019-11-22 22:38:03 +01:00
zadam ff0245f05f dynamically translating kbd based on actions 2019-11-22 22:35:59 +01:00
zadam 7a2c7edd7e allow multiple instances of @in operator, closes #716 2019-11-22 21:17:46 +01:00
zadam 98c81faedb Merge branch 'stable' 2019-11-22 20:36:47 +01:00
zadam cfb850acb2 download fixes for the sub-domain web deployment 2019-11-22 20:35:17 +01:00
zadam a16aaf7a81 fix setup on non-root paths #404 2019-11-22 20:24:49 +01:00
zadam 465c3b87a7 tree keyboard shortcuts 2019-11-21 22:24:07 +01:00
zadam 0e5028acd3 support global shortcuts with global: prefix 2019-11-21 21:12:07 +01:00
zadam 00c295e4bf upgrade to electron 8.0.0-beta.3 2019-11-21 19:36:34 +01:00
zadam 7084ed4fb1 Merge branch 'stable' 2019-11-20 23:11:21 +01:00
zadam 587134c2f8 separators and fixes 2019-11-20 23:10:41 +01:00
zadam 5fac2c7633 saving options keyboard shortcuts 2019-11-20 22:48:32 +01:00
zadam 08a518479b keyboard shortcuts options pane 2019-11-20 21:35:18 +01:00
zadam 522f71cb91 fix tree clipboard 2019-11-20 19:24:23 +01:00
zadam bcdfb47939 Merge branch 'stable' 2019-11-19 23:34:35 +01:00
zadam 667471e7bb toggle zen mode in the global menu 2019-11-19 23:33:07 +01:00
zadam d357943ebb release 0.37.3 2019-11-19 23:05:54 +01:00
zadam 07043fb177 switch search in subtree to ctrl+shift+s to stay consistent with ctrl+s 2019-11-19 23:04:43 +01:00
zadam 3c4ec7fe1a keyboard shortcuts WIP 2019-11-19 23:02:54 +01:00
zadam 1f8d382b1f added "search in subtree" context menu, #534 2019-11-19 21:11:20 +01:00
zadam 4bd7438fca shortcuts WIP 2019-11-19 20:53:04 +01:00
zadam 0ae9c8da17 Merge branch 'stable' 2019-11-19 19:32:49 +01:00
zadam 61e8cbbcba add log for content hash failures 2019-11-19 19:07:14 +01:00
zadam 86c5dd6494 fix recent changes, closes #713 2019-11-19 19:02:16 +01:00
zadam f921562346 Merge branch 'm38'
# Conflicts:
#	docs/backend_api/Note.html
#	docs/frontend_api/NoteShort.html
#	src/services/import/enex.js
2019-11-18 23:26:29 +01:00
zadam c5acb7fc9b release 0.37.2 2019-11-18 23:04:09 +01:00
zadam 643d9077fc configurable keyboard shortcuts WIP 2019-11-18 23:01:31 +01:00
zadam b4709e8ee5 "distraction free mode" renamed to more standard "zen mode" 2019-11-18 19:32:27 +01:00
zadam a1181623b7 hide sidebar button styling 2019-11-17 11:58:05 +01:00
zadam 73a6c66379 header styling changes 2019-11-17 11:39:06 +01:00
zadam 1d5daa8dfd action icons now have hover border as well 2019-11-17 11:30:11 +01:00
zadam 834e1f7253 fix activating tab when app was closed last time with "new tab" 2019-11-17 10:24:06 +01:00
zadam c141f4b2c0 tab row styling change 2019-11-17 10:22:26 +01:00
zadam 1a87190f43 added IN operator to search, closes #534 2019-11-17 09:07:35 +01:00
zadam a1262aaaf3 release 0.37.1-beta 2019-11-16 19:09:52 +01:00
zadam 1838f097e5 added getAllNotePaths, fixes #708 2019-11-16 19:07:32 +01:00
zadam 95d0ad1cad simpler ENEX parsing 2019-11-16 18:13:00 +01:00
zadam 767aaa18f4 fix OPML import 2019-11-16 18:04:13 +01:00
zadam 8a7228146c simpler ENEX parsing 2019-11-16 17:56:49 +01:00
zadam f9b5e473f2 added missing options 2019-11-16 17:03:18 +01:00
zadam 3d294c5163 refactoring of note creation APIs WIP 2019-11-16 17:00:22 +01:00
zadam 60231de0ed refactoring of note creation APIs WIP 2019-11-16 12:28:47 +01:00
zadam 35e9df6170 fixes in the handling of cssClass and iconClass when working with newly created / single note 2019-11-16 11:23:28 +01:00
zadam 13c0411533 refactoring of note creation APIs WIP 2019-11-16 11:09:52 +01:00
zadam c5e040c4a4 updated demo document with icon classes 2019-11-16 09:57:19 +01:00
zadam e00ab5dbf9 new "iconClass" property on note_short, now together with cssClass respecting template relationship, #707 2019-11-16 08:59:27 +01:00
zadam de02e9e889 redesign of createNote APIs, WIP 2019-11-14 23:10:56 +01:00
zadam e143becb7a styling changes 2019-11-14 20:27:28 +01:00
180 changed files with 13360 additions and 7705 deletions
+111 -114
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataSource name="document.db">
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.16">
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.17">
<root id="1">
<ServerVersion>3.25.1</ServerVersion>
<ServerVersion>3.16.1</ServerVersion>
</root>
<schema id="2" parent="1" name="main">
<Current>1</Current>
@@ -134,509 +134,506 @@
<ColNames>name
value</ColNames>
</index>
<index id="40" parent="7" name="IDX_attributes_name_index">
<ColNames>name</ColNames>
</index>
<index id="41" parent="7" name="IDX_attributes_value_index">
<index id="40" parent="7" name="IDX_attributes_value_index">
<ColNames>value</ColNames>
</index>
<key id="42" parent="7">
<key id="41" parent="7">
<ColNames>attributeId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_attributes_1</UnderlyingIndexName>
</key>
<column id="43" parent="8" name="branchId">
<column id="42" parent="8" name="branchId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="44" parent="8" name="noteId">
<column id="43" parent="8" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="45" parent="8" name="parentNoteId">
<column id="44" parent="8" name="parentNoteId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="46" parent="8" name="notePosition">
<column id="45" parent="8" name="notePosition">
<Position>4</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="47" parent="8" name="prefix">
<column id="46" parent="8" name="prefix">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="48" parent="8" name="isExpanded">
<column id="47" parent="8" name="isExpanded">
<Position>6</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="49" parent="8" name="isDeleted">
<column id="48" parent="8" name="isDeleted">
<Position>7</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="50" parent="8" name="utcDateModified">
<column id="49" parent="8" name="utcDateModified">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="51" parent="8" name="utcDateCreated">
<column id="50" parent="8" name="utcDateCreated">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="52" parent="8" name="hash">
<column id="51" parent="8" name="hash">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="53" parent="8" name="sqlite_autoindex_branches_1">
<index id="52" parent="8" name="sqlite_autoindex_branches_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames>
<Unique>1</Unique>
</index>
<index id="54" parent="8" name="IDX_branches_noteId_parentNoteId">
<index id="53" parent="8" name="IDX_branches_noteId_parentNoteId">
<ColNames>noteId
parentNoteId</ColNames>
</index>
<index id="55" parent="8" name="IDX_branches_noteId">
<ColNames>noteId</ColNames>
</index>
<index id="56" parent="8" name="IDX_branches_parentNoteId">
<index id="54" parent="8" name="IDX_branches_parentNoteId">
<ColNames>parentNoteId</ColNames>
</index>
<key id="57" parent="8">
<key id="55" parent="8">
<ColNames>branchId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
</key>
<column id="58" parent="9" name="noteId">
<column id="56" parent="9" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="59" parent="9" name="content">
<column id="57" parent="9" name="content">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<DefaultExpression>NULL</DefaultExpression>
</column>
<column id="60" parent="9" name="hash">
<column id="58" parent="9" name="hash">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="61" parent="9" name="utcDateModified">
<column id="59" parent="9" name="utcDateModified">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="62" parent="9" name="sqlite_autoindex_note_contents_1">
<index id="60" parent="9" name="sqlite_autoindex_note_contents_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<Unique>1</Unique>
</index>
<key id="63" parent="9">
<key id="61" parent="9">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_contents_1</UnderlyingIndexName>
</key>
<column id="64" parent="10" name="noteRevisionId">
<column id="62" parent="10" name="noteRevisionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="65" parent="10" name="content">
<column id="63" parent="10" name="content">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="66" parent="10" name="hash">
<column id="64" parent="10" name="hash">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="67" parent="10" name="utcDateModified">
<column id="65" parent="10" name="utcDateModified">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="68" parent="10" name="sqlite_autoindex_note_revision_contents_1">
<index id="66" parent="10" name="sqlite_autoindex_note_revision_contents_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<Unique>1</Unique>
</index>
<key id="69" parent="10">
<key id="67" parent="10">
<ColNames>noteRevisionId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revision_contents_1</UnderlyingIndexName>
</key>
<column id="70" parent="11" name="noteRevisionId">
<column id="68" parent="11" name="noteRevisionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="71" parent="11" name="noteId">
<column id="69" parent="11" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="72" parent="11" name="title">
<column id="70" parent="11" name="title">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="73" parent="11" name="contentLength">
<column id="71" parent="11" name="contentLength">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="74" parent="11" name="isErased">
<column id="72" parent="11" name="isErased">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="75" parent="11" name="isProtected">
<column id="73" parent="11" name="isProtected">
<Position>6</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="76" parent="11" name="utcDateLastEdited">
<column id="74" parent="11" name="utcDateLastEdited">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="77" parent="11" name="utcDateCreated">
<column id="75" parent="11" name="utcDateCreated">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="78" parent="11" name="utcDateModified">
<column id="76" parent="11" name="utcDateModified">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="79" parent="11" name="dateLastEdited">
<column id="77" parent="11" name="dateLastEdited">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="80" parent="11" name="dateCreated">
<column id="78" parent="11" name="dateCreated">
<Position>11</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="81" parent="11" name="type">
<column id="79" parent="11" name="type">
<Position>12</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="82" parent="11" name="mime">
<column id="80" parent="11" name="mime">
<Position>13</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="83" parent="11" name="hash">
<column id="81" parent="11" name="hash">
<Position>14</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<index id="84" parent="11" name="sqlite_autoindex_note_revisions_1">
<index id="82" parent="11" name="sqlite_autoindex_note_revisions_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<Unique>1</Unique>
</index>
<index id="85" parent="11" name="IDX_note_revisions_noteId">
<index id="83" parent="11" name="IDX_note_revisions_noteId">
<ColNames>noteId</ColNames>
</index>
<index id="86" parent="11" name="IDX_note_revisions_utcDateLastEdited">
<index id="84" parent="11" name="IDX_note_revisions_utcDateLastEdited">
<ColNames>utcDateLastEdited</ColNames>
</index>
<index id="87" parent="11" name="IDX_note_revisions_utcDateCreated">
<index id="85" parent="11" name="IDX_note_revisions_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
</index>
<index id="88" parent="11" name="IDX_note_revisions_dateLastEdited">
<index id="86" parent="11" name="IDX_note_revisions_dateLastEdited">
<ColNames>dateLastEdited</ColNames>
</index>
<index id="89" parent="11" name="IDX_note_revisions_dateCreated">
<index id="87" parent="11" name="IDX_note_revisions_dateCreated">
<ColNames>dateCreated</ColNames>
</index>
<key id="90" parent="11">
<key id="88" parent="11">
<ColNames>noteRevisionId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
</key>
<column id="91" parent="12" name="noteId">
<column id="89" parent="12" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="92" parent="12" name="title">
<column id="90" parent="12" name="title">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;note&quot;</DefaultExpression>
</column>
<column id="93" parent="12" name="contentLength">
<column id="91" parent="12" name="contentLength">
<Position>3</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="94" parent="12" name="isProtected">
<column id="92" parent="12" name="isProtected">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="95" parent="12" name="type">
<column id="93" parent="12" name="type">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text&apos;</DefaultExpression>
</column>
<column id="96" parent="12" name="mime">
<column id="94" parent="12" name="mime">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text/html&apos;</DefaultExpression>
</column>
<column id="97" parent="12" name="hash">
<column id="95" parent="12" name="hash">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="98" parent="12" name="isDeleted">
<column id="96" parent="12" name="isDeleted">
<Position>8</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="99" parent="12" name="isErased">
<column id="97" parent="12" name="isErased">
<Position>9</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="100" parent="12" name="dateCreated">
<column id="98" parent="12" name="dateCreated">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="101" parent="12" name="dateModified">
<column id="99" parent="12" name="dateModified">
<Position>11</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="102" parent="12" name="utcDateCreated">
<column id="100" parent="12" name="utcDateCreated">
<Position>12</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="103" parent="12" name="utcDateModified">
<column id="101" parent="12" name="utcDateModified">
<Position>13</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="104" parent="12" name="sqlite_autoindex_notes_1">
<index id="102" parent="12" name="sqlite_autoindex_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<Unique>1</Unique>
</index>
<index id="105" parent="12" name="IDX_notes_title">
<index id="103" parent="12" name="IDX_notes_title">
<ColNames>title</ColNames>
</index>
<index id="106" parent="12" name="IDX_notes_type">
<index id="104" parent="12" name="IDX_notes_type">
<ColNames>type</ColNames>
</index>
<index id="107" parent="12" name="IDX_notes_isDeleted">
<index id="105" parent="12" name="IDX_notes_isDeleted">
<ColNames>isDeleted</ColNames>
</index>
<index id="108" parent="12" name="IDX_notes_dateCreated">
<index id="106" parent="12" name="IDX_notes_dateCreated">
<ColNames>dateCreated</ColNames>
</index>
<index id="109" parent="12" name="IDX_notes_dateModified">
<index id="107" parent="12" name="IDX_notes_dateModified">
<ColNames>dateModified</ColNames>
</index>
<index id="110" parent="12" name="IDX_notes_utcDateCreated">
<index id="108" parent="12" name="IDX_notes_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
</index>
<index id="111" parent="12" name="IDX_notes_utcDateModified">
<index id="109" parent="12" name="IDX_notes_utcDateModified">
<ColNames>utcDateModified</ColNames>
</index>
<key id="112" parent="12">
<key id="110" parent="12">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
</key>
<column id="113" parent="13" name="name">
<column id="111" parent="13" name="name">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="114" parent="13" name="value">
<column id="112" parent="13" name="value">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="115" parent="13" name="isSynced">
<column id="113" parent="13" name="isSynced">
<Position>3</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="116" parent="13" name="hash">
<column id="114" parent="13" name="hash">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="117" parent="13" name="utcDateCreated">
<column id="115" parent="13" name="utcDateCreated">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="118" parent="13" name="utcDateModified">
<column id="116" parent="13" name="utcDateModified">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="119" parent="13" name="sqlite_autoindex_options_1">
<index id="117" parent="13" name="sqlite_autoindex_options_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>name</ColNames>
<Unique>1</Unique>
</index>
<key id="120" parent="13">
<key id="118" parent="13">
<ColNames>name</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
</key>
<column id="121" parent="14" name="noteId">
<column id="119" parent="14" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="122" parent="14" name="notePath">
<column id="120" parent="14" name="notePath">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="123" parent="14" name="hash">
<column id="121" parent="14" name="hash">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="124" parent="14" name="utcDateCreated">
<column id="122" parent="14" name="utcDateCreated">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="125" parent="14" name="isDeleted">
<column id="123" parent="14" name="isDeleted">
<Position>5</Position>
<DataType>INT|0s</DataType>
</column>
<index id="126" parent="14" name="sqlite_autoindex_recent_notes_1">
<index id="124" parent="14" name="sqlite_autoindex_recent_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<Unique>1</Unique>
</index>
<key id="127" parent="14">
<key id="125" parent="14">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
</key>
<column id="128" parent="15" name="sourceId">
<column id="126" parent="15" name="sourceId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="129" parent="15" name="utcDateCreated">
<column id="127" parent="15" name="utcDateCreated">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="130" parent="15" name="sqlite_autoindex_source_ids_1">
<index id="128" parent="15" name="sqlite_autoindex_source_ids_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>sourceId</ColNames>
<Unique>1</Unique>
</index>
<key id="131" parent="15">
<index id="129" parent="15" name="IDX_source_ids_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
</index>
<key id="130" parent="15">
<ColNames>sourceId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
</key>
<column id="132" parent="16" name="type">
<column id="131" parent="16" name="type">
<Position>1</Position>
<DataType>text|0s</DataType>
</column>
<column id="133" parent="16" name="name">
<column id="132" parent="16" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
</column>
<column id="134" parent="16" name="tbl_name">
<column id="133" parent="16" name="tbl_name">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="135" parent="16" name="rootpage">
<column id="134" parent="16" name="rootpage">
<Position>4</Position>
<DataType>int|0s</DataType>
<DataType>integer|0s</DataType>
</column>
<column id="136" parent="16" name="sql">
<column id="135" parent="16" name="sql">
<Position>5</Position>
<DataType>text|0s</DataType>
</column>
<column id="137" parent="17" name="name">
<column id="136" parent="17" name="name">
<Position>1</Position>
</column>
<column id="138" parent="17" name="seq">
<column id="137" parent="17" name="seq">
<Position>2</Position>
</column>
<column id="139" parent="18" name="id">
<column id="138" parent="18" name="id">
<Position>1</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="140" parent="18" name="entityName">
<column id="139" parent="18" name="entityName">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="141" parent="18" name="entityId">
<column id="140" parent="18" name="entityId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="142" parent="18" name="sourceId">
<column id="141" parent="18" name="sourceId">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="143" parent="18" name="utcSyncDate">
<column id="142" parent="18" name="utcSyncDate">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="144" parent="18" name="IDX_sync_entityName_entityId">
<index id="143" parent="18" name="IDX_sync_entityName_entityId">
<ColNames>entityName
entityId</ColNames>
<Unique>1</Unique>
</index>
<index id="145" parent="18" name="IDX_sync_utcSyncDate">
<index id="144" parent="18" name="IDX_sync_utcSyncDate">
<ColNames>utcSyncDate</ColNames>
</index>
<key id="146" parent="18">
<key id="145" parent="18">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:12.13.0-alpine
FROM node:12.14.0-alpine
# Create app directory
WORKDIR /usr/src/app
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
PKG_DIR=dist/trilium-linux-x64-server
NODE_VERSION=12.13.0
NODE_VERSION=12.14.0
if [ "$1" != "DONTCOPY" ]
then
+3
View File
@@ -2,6 +2,9 @@
# Instance name can be used to distinguish between different instances
instanceName=
# Disable automatically generating desktop icon
# noDesktopIcon=true
[Network]
# host setting is relevant only for web deployments - set the host on which the server will listen
# host=0.0.0.0
BIN
View File
Binary file not shown.
@@ -20,7 +20,7 @@ SELECT noteId, title, -1, isProtected, type, mime, hash, isDeleted, isErased, da
DROP TABLE notes;
ALTER TABLE notes_mig RENAME TO notes;
UPDATE notes SET contentLength = (SELECT COALESCE(LENGTH(content), 0) FROM note_contents WHERE note_contents.noteId = notes.noteId);
UPDATE notes SET contentLength = COALESCE((SELECT COALESCE(LENGTH(content), 0) FROM note_contents WHERE note_contents.noteId = notes.noteId), -1);
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
@@ -0,0 +1,3 @@
UPDATE attributes SET name = 'internalLink' WHERE name = 'internal-link';
UPDATE attributes SET name = 'imageLink' WHERE name = 'image-link';
UPDATE attributes SET name = 'relationMapLink' WHERE name = 'relation-map-link';
+5
View File
@@ -0,0 +1,5 @@
DROP INDEX IF EXISTS IDX_attributes_name_index;
DROP INDEX IF EXISTS IDX_branches_noteId;
CREATE INDEX IDX_source_ids_utcDateCreated
on source_ids (utcDateCreated);
+2 -3
View File
@@ -9,6 +9,8 @@ CREATE TABLE IF NOT EXISTS "source_ids" (
`utcDateCreated` TEXT NOT NULL,
PRIMARY KEY(`sourceId`)
);
CREATE INDEX IDX_source_ids_utcDateCreated
on source_ids (utcDateCreated);
CREATE TABLE IF NOT EXISTS "api_tokens"
(
apiTokenId TEXT PRIMARY KEY NOT NULL,
@@ -47,8 +49,6 @@ CREATE INDEX `IDX_sync_utcSyncDate` ON `sync` (
);
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_name_index
on attributes (name);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
@@ -80,7 +80,6 @@ CREATE TABLE IF NOT EXISTS "branches" (
utcDateCreated TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`branchId`));
CREATE INDEX `IDX_branches_noteId` ON `branches` (`noteId`);
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
+9 -6
View File
@@ -28,7 +28,7 @@
<header>
<h2><span class="attribs"><span class="type-signature"></span></span>ApiToken<span class="signature">(apiTokenId, token, isDeleted, utcDateCreated)</span><span class="type-signature"></span></h2>
<h2><span class="attribs"><span class="type-signature"></span></span>ApiToken<span class="signature">()</span><span class="type-signature"></span></h2>
<div class="class-description">ApiToken is an entity representing token used to authenticate against Trilium API from client applications. Currently used only by Trilium Sender.</div>
@@ -45,7 +45,7 @@
<h4 class="name" id="ApiToken"><span class="type-signature"></span>new ApiToken<span class="signature">(apiTokenId, token, isDeleted, utcDateCreated)</span><span class="type-signature"></span></h4>
<h4 class="name" id="ApiToken"><span class="type-signature"></span>new ApiToken<span class="signature">()</span><span class="type-signature"></span></h4>
@@ -60,10 +60,15 @@
<h5>Parameters:</h5>
<h5 class="subsection-title">Properties:</h5>
<table class="params">
<table class="props">
<thead>
<tr>
@@ -180,8 +185,6 @@
<dl class="details">
+9 -6
View File
@@ -28,7 +28,7 @@
<header>
<h2><span class="attribs"><span class="type-signature"></span></span>Attribute<span class="signature">(attributeId, noteId, type, name, value, position, isInheritable, isDeleted, utcDateCreated, utcDateModified)</span><span class="type-signature"></span></h2>
<h2><span class="attribs"><span class="type-signature"></span></span>Attribute<span class="signature">()</span><span class="type-signature"></span></h2>
<div class="class-description">Attribute is key value pair owned by a note.</div>
@@ -45,7 +45,7 @@
<h4 class="name" id="Attribute"><span class="type-signature"></span>new Attribute<span class="signature">(attributeId, noteId, type, name, value, position, isInheritable, isDeleted, utcDateCreated, utcDateModified)</span><span class="type-signature"></span></h4>
<h4 class="name" id="Attribute"><span class="type-signature"></span>new Attribute<span class="signature">()</span><span class="type-signature"></span></h4>
@@ -60,10 +60,15 @@
<h5>Parameters:</h5>
<h5 class="subsection-title">Properties:</h5>
<table class="params">
<table class="props">
<thead>
<tr>
@@ -318,8 +323,6 @@
<dl class="details">
+491 -103
View File
@@ -396,7 +396,7 @@ the backend.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line313">line 313</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line394">line 394</a>
</li></ul></dd>
@@ -534,6 +534,375 @@ the backend.
<h4 class="name" id="createDataNote"><span class="type-signature"></span>createDataNote<span class="signature">(parentNoteId, title, content)</span><span class="type-signature"> &rarr; {Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}>}</span></h4>
<div class="description">
Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and
JSON MIME type. See also createNewNote() for more options.
</div>
<h5>Parameters:</h5>
<table class="params">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="name"><code>parentNoteId</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last"></td>
</tr>
<tr>
<td class="name"><code>title</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last"></td>
</tr>
<tr>
<td class="name"><code>content</code></td>
<td class="type">
<span class="param-type">object</span>
</td>
<td class="description last"></td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line204">line 204</a>
</li></ul></dd>
</dl>
<h5>Returns:</h5>
<dl>
<dt>
Type
</dt>
<dd>
<span class="param-type">Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}></span>
</dd>
</dl>
<h4 class="name" id="createNewNote"><span class="type-signature"></span>createNewNote<span class="signature">(params<span class="signature-attributes">opt</span>)</span><span class="type-signature"> &rarr; {Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}>}</span></h4>
<h5>Parameters:</h5>
<table class="params">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Attributes</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="name"><code>params</code></td>
<td class="type">
<span class="param-type"><a href="global.html#CreateNewNoteParams">CreateNewNoteParams</a></span>
</td>
<td class="attributes">
&lt;optional><br>
</td>
<td class="description last"></td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line231">line 231</a>
</li></ul></dd>
</dl>
<h5>Returns:</h5>
<div class="param-desc">
object contains newly created entities note and branch
</div>
<dl>
<dt>
Type
</dt>
<dd>
<span class="param-type">Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}></span>
</dd>
</dl>
<h4 class="name" id="createNote"><span class="type-signature"></span>createNote<span class="signature">(parentNoteId, title, content<span class="signature-attributes">opt</span>, extraOptions<span class="signature-attributes">opt</span>)</span><span class="type-signature"> &rarr; {Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}>}</span></h4>
@@ -760,7 +1129,7 @@ the backend.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line198">line 198</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line258">line 258</a>
</li></ul></dd>
@@ -818,7 +1187,7 @@ the backend.
<h4 class="name" id="createNoteAndRefresh"><span class="type-signature"></span>createNoteAndRefresh<span class="signature">(parentNoteId, title, content<span class="signature-attributes">opt</span>, extraOptions<span class="signature-attributes">opt</span>)</span><span class="type-signature"> &rarr; {Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}>}</span></h4>
<h4 class="name" id="createTextNote"><span class="type-signature"></span>createTextNote<span class="signature">(parentNoteId, title, content)</span><span class="type-signature"> &rarr; {Promise.&lt;{note: <a href="Note.html">Note</a>, branch: <a href="Branch.html">Branch</a>}>}</span></h4>
@@ -826,7 +1195,7 @@ the backend.
<div class="description">
Creates new note according to given params and force all connected clients to refresh their tree.
Create text note. See also createNewNote() for more options.
</div>
@@ -850,12 +1219,8 @@ the backend.
<th>Type</th>
<th>Attributes</th>
<th>Default</th>
<th class="last">Description</th>
</tr>
@@ -879,22 +1244,10 @@ the backend.
</td>
<td class="attributes">
</td>
<td class="default">
</td>
<td class="description last">create new note under this parent</td>
<td class="description last"></td>
</tr>
@@ -914,19 +1267,7 @@ the backend.
</td>
<td class="attributes">
</td>
<td class="default">
</td>
<td class="description last"></td>
@@ -949,62 +1290,7 @@ the backend.
</td>
<td class="attributes">
&lt;optional><br>
</td>
<td class="default">
""
</td>
<td class="description last"></td>
</tr>
<tr>
<td class="name"><code>extraOptions</code></td>
<td class="type">
<span class="param-type"><a href="global.html#CreateNoteExtraOptions">CreateNoteExtraOptions</a></span>
</td>
<td class="attributes">
&lt;optional><br>
</td>
<td class="default">
{}
</td>
<td class="description last"></td>
@@ -1048,7 +1334,7 @@ the backend.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line211">line 211</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line188">line 188</a>
</li></ul></dd>
@@ -1076,10 +1362,6 @@ the backend.
<h5>Returns:</h5>
<div class="param-desc">
object contains newly created entities note and branch
</div>
<dl>
@@ -1533,7 +1815,7 @@ the backend.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line318">line 318</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line399">line 399</a>
</li></ul></dd>
@@ -1997,7 +2279,7 @@ the backend.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line241">line 241</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line314">line 314</a>
</li></ul></dd>
@@ -2765,7 +3047,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line260">line 260</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line341">line 341</a>
</li></ul></dd>
@@ -3418,7 +3700,113 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line232">line 232</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line305">line 305</a>
</li></ul></dd>
</dl>
<h5>Returns:</h5>
<dl>
<dt>
Type
</dt>
<dd>
<span class="param-type">Promise.&lt;(<a href="Note.html">Note</a>|null)></span>
</dd>
</dl>
<h4 class="name" id="getTodayNote"><span class="type-signature"></span>getTodayNote<span class="signature">()</span><span class="type-signature"> &rarr; {Promise.&lt;(<a href="Note.html">Note</a>|null)>}</span></h4>
<div class="description">
Returns today's day note. If such note doesn't exist, it is created.
</div>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line322">line 322</a>
</li></ul></dd>
@@ -3596,7 +3984,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line251">line 251</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line332">line 332</a>
</li></ul></dd>
@@ -3751,7 +4139,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line269">line 269</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line350">line 350</a>
</li></ul></dd>
@@ -3901,7 +4289,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line224">line 224</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line297">line 297</a>
</li></ul></dd>
@@ -4404,7 +4792,7 @@ This method looks similar to toggleNoteInParent() but differs because we're look
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line290">line 290</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line371">line 371</a>
</li></ul></dd>
@@ -4537,7 +4925,7 @@ This method looks similar to toggleNoteInParent() but differs because we're look
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line276">line 276</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line357">line 357</a>
</li></ul></dd>
@@ -4912,7 +5300,7 @@ transactional by default.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line303">line 303</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line384">line 384</a>
</li></ul></dd>
+9 -6
View File
@@ -28,7 +28,7 @@
<header>
<h2><span class="attribs"><span class="type-signature"></span></span>Branch<span class="signature">(branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, utcDateModified, utcDateCreated)</span><span class="type-signature"></span></h2>
<h2><span class="attribs"><span class="type-signature"></span></span>Branch<span class="signature">()</span><span class="type-signature"></span></h2>
<div class="class-description">Branch represents note's placement in the tree - it's essentially pair of noteId and parentNoteId.
Each note can have multiple (at least one) branches, meaning it can be placed into multiple places in the tree.</div>
@@ -46,7 +46,7 @@ Each note can have multiple (at least one) branches, meaning it can be placed in
<h4 class="name" id="Branch"><span class="type-signature"></span>new Branch<span class="signature">(branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, utcDateModified, utcDateCreated)</span><span class="type-signature"></span></h4>
<h4 class="name" id="Branch"><span class="type-signature"></span>new Branch<span class="signature">()</span><span class="type-signature"></span></h4>
@@ -61,10 +61,15 @@ Each note can have multiple (at least one) branches, meaning it can be placed in
<h5>Parameters:</h5>
<h5 class="subsection-title">Properties:</h5>
<table class="params">
<table class="props">
<thead>
<tr>
@@ -296,8 +301,6 @@ Each note can have multiple (at least one) branches, meaning it can be placed in
<dl class="details">
+2360 -71
View File
File diff suppressed because it is too large Load Diff
+60 -11
View File
@@ -28,7 +28,7 @@
<header>
<h2><span class="attribs"><span class="type-signature"></span></span>NoteRevision<span class="signature">(noteRevisionId, noteId, type, mime, title, isProtected, dateLastEdited, dateCreated, utcDateLastEdited, utcDateCreated, utcDateModified)</span><span class="type-signature"></span></h2>
<h2><span class="attribs"><span class="type-signature"></span></span>NoteRevision<span class="signature">()</span><span class="type-signature"></span></h2>
<div class="class-description">NoteRevision represents snapshot of note's title and content at some point in the past. It's used for seamless note versioning.</div>
@@ -45,7 +45,7 @@
<h4 class="name" id="NoteRevision"><span class="type-signature"></span>new NoteRevision<span class="signature">(noteRevisionId, noteId, type, mime, title, isProtected, dateLastEdited, dateCreated, utcDateLastEdited, utcDateCreated, utcDateModified)</span><span class="type-signature"></span></h4>
<h4 class="name" id="NoteRevision"><span class="type-signature"></span>new NoteRevision<span class="signature">()</span><span class="type-signature"></span></h4>
@@ -60,10 +60,15 @@
<h5>Parameters:</h5>
<h5 class="subsection-title">Properties:</h5>
<table class="params">
<table class="props">
<thead>
<tr>
@@ -193,6 +198,52 @@
<td class="description last"></td>
</tr>
<tr>
<td class="name"><code>contentLength</code></td>
<td class="type">
<span class="param-type">int</span>
</td>
<td class="description last"></td>
</tr>
<tr>
<td class="name"><code>isErased</code></td>
<td class="type">
<span class="param-type">boolean</span>
</td>
<td class="description last"></td>
</tr>
@@ -206,7 +257,7 @@
<td class="type">
<span class="param-type">string</span>
<span class="param-type">boolean</span>
@@ -341,8 +392,6 @@
<dl class="details">
@@ -372,7 +421,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_revision.js.html">entities/note_revision.js</a>, <a href="entities_note_revision.js.html#line33">line 33</a>
<a href="entities_note_revision.js.html">entities/note_revision.js</a>, <a href="entities_note_revision.js.html#line35">line 35</a>
</li></ul></dd>
@@ -487,7 +536,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_revision.js.html">entities/note_revision.js</a>, <a href="entities_note_revision.js.html#line67">line 67</a>
<a href="entities_note_revision.js.html">entities/note_revision.js</a>, <a href="entities_note_revision.js.html#line69">line 69</a>
</li></ul></dd>
@@ -589,7 +638,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_revision.js.html">entities/note_revision.js</a>, <a href="entities_note_revision.js.html#line53">line 53</a>
<a href="entities_note_revision.js.html">entities/note_revision.js</a>, <a href="entities_note_revision.js.html#line55">line 55</a>
</li></ul></dd>
@@ -695,7 +744,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_revision.js.html">entities/note_revision.js</a>, <a href="entities_note_revision.js.html#line102">line 102</a>
<a href="entities_note_revision.js.html">entities/note_revision.js</a>, <a href="entities_note_revision.js.html#line104">line 104</a>
</li></ul></dd>
+9 -6
View File
@@ -28,7 +28,7 @@
<header>
<h2><span class="attribs"><span class="type-signature"></span></span>Option<span class="signature">(name, value, isSynced, utcDateModified, utcDateCreated)</span><span class="type-signature"></span></h2>
<h2><span class="attribs"><span class="type-signature"></span></span>Option<span class="signature">()</span><span class="type-signature"></span></h2>
<div class="class-description">Option represents name-value pair, either directly configurable by the user or some system property.</div>
@@ -45,7 +45,7 @@
<h4 class="name" id="Option"><span class="type-signature"></span>new Option<span class="signature">(name, value, isSynced, utcDateModified, utcDateCreated)</span><span class="type-signature"></span></h4>
<h4 class="name" id="Option"><span class="type-signature"></span>new Option<span class="signature">()</span><span class="type-signature"></span></h4>
@@ -60,10 +60,15 @@
<h5>Parameters:</h5>
<h5 class="subsection-title">Properties:</h5>
<table class="params">
<table class="props">
<thead>
<tr>
@@ -203,8 +208,6 @@
<dl class="details">
+9 -6
View File
@@ -28,7 +28,7 @@
<header>
<h2><span class="attribs"><span class="type-signature"></span></span>RecentNote<span class="signature">(noteId, notePath, isDeleted, utcDateModified)</span><span class="type-signature"></span></h2>
<h2><span class="attribs"><span class="type-signature"></span></span>RecentNote<span class="signature">()</span><span class="type-signature"></span></h2>
<div class="class-description">RecentNote represents recently visited note.</div>
@@ -45,7 +45,7 @@
<h4 class="name" id="RecentNote"><span class="type-signature"></span>new RecentNote<span class="signature">(noteId, notePath, isDeleted, utcDateModified)</span><span class="type-signature"></span></h4>
<h4 class="name" id="RecentNote"><span class="type-signature"></span>new RecentNote<span class="signature">()</span><span class="type-signature"></span></h4>
@@ -60,10 +60,15 @@
<h5>Parameters:</h5>
<h5 class="subsection-title">Properties:</h5>
<table class="params">
<table class="props">
<thead>
<tr>
@@ -180,8 +185,6 @@
<dl class="details">
+4 -4
View File
@@ -34,10 +34,10 @@ const dateUtils = require('../services/date_utils');
/**
* ApiToken is an entity representing token used to authenticate against Trilium API from client applications. Currently used only by Trilium Sender.
*
* @param {string} apiTokenId - primary key
* @param {string} token
* @param {boolean} isDeleted - true if API token is deleted
* @param {string} utcDateCreated
* @property {string} apiTokenId - primary key
* @property {string} token
* @property {boolean} isDeleted - true if API token is deleted
* @property {string} utcDateCreated
*
* @extends Entity
*/
+15 -10
View File
@@ -36,16 +36,16 @@ const sql = require('../services/sql');
/**
* Attribute is key value pair owned by a note.
*
* @param {string} attributeId
* @param {string} noteId
* @param {string} type
* @param {string} name
* @param {string} value
* @param {int} position
* @param {boolean} isInheritable
* @param {boolean} isDeleted
* @param {string} utcDateCreated
* @param {string} utcDateModified
* @property {string} attributeId
* @property {string} noteId
* @property {string} type
* @property {string} name
* @property {string} value
* @property {int} position
* @property {boolean} isInheritable
* @property {boolean} isDeleted
* @property {string} utcDateCreated
* @property {string} utcDateModified
*
* @extends Entity
*/
@@ -107,6 +107,10 @@ class Attribute extends Entity {
async beforeSaving() {
if (!this.value) {
if (this.type === 'relation') {
throw new Error(`Cannot save relation ${this.name} since it does not target any note.`);
}
// null value isn't allowed
this.value = "";
}
@@ -137,6 +141,7 @@ class Attribute extends Entity {
// cannot be static!
updatePojo(pojo) {
delete pojo.isOwned;
delete pojo.__note;
}
}
+9 -9
View File
@@ -37,15 +37,15 @@ const sql = require('../services/sql');
* Branch represents note's placement in the tree - it's essentially pair of noteId and parentNoteId.
* Each note can have multiple (at least one) branches, meaning it can be placed into multiple places in the tree.
*
* @param {string} branchId - primary key
* @param {string} noteId
* @param {string} parentNoteId
* @param {int} notePosition
* @param {string} prefix
* @param {boolean} isExpanded
* @param {boolean} isDeleted
* @param {string} utcDateModified
* @param {string} utcDateCreated
* @property {string} branchId - primary key
* @property {string} noteId
* @property {string} parentNoteId
* @property {int} notePosition
* @property {string} prefix
* @property {boolean} isExpanded
* @property {boolean} isDeleted
* @property {string} utcDateModified
* @property {string} utcDateCreated
*
* @extends Entity
*/
+205 -33
View File
@@ -49,6 +49,7 @@ const RELATION_DEFINITION = 'relation-definition';
* @property {string} type - one of "text", "code", "file" or "render"
* @property {string} mime - MIME type, e.g. "text/html"
* @property {string} title - note title
* @property {int} contentLength - length of content
* @property {boolean} isProtected - true if note is protected
* @property {boolean} isDeleted - true if note is deleted
* @property {boolean} isErased - true if note's content is erased after it has been deleted
@@ -141,8 +142,13 @@ class Note extends Entity {
/** @returns {Promise} */
async setContent(content) {
if (content === null || content === undefined) {
throw new Error(`Cannot set null content to note ${this.noteId}`);
}
// force updating note itself so that dateModified is represented correctly even for the content
this.forcedChange = true;
this.contentLength = content.length;
await this.save();
this.content = content;
@@ -218,26 +224,36 @@ class Note extends Entity {
return null;
}
async loadOwnedAttributesToCache() {
this.__ownedAttributeCache = await repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
return this.__ownedAttributeCache;
}
/**
* @returns {Promise&lt;Attribute[]>} attributes belonging to this specific note (excludes inherited attributes)
* This method is a faster variant of getAttributes() which looks for only owned attributes.
* Use when inheritance is not needed and/or in batch/performance sensitive operations.
*
* This method can be significantly faster than the getAttributes()
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise&lt;Attribute[]>} note's "owned" attributes - excluding inherited ones
*/
async getOwnedAttributes(type, name) {
let query = `SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`;
const params = [this.noteId];
if (type) {
query += ` AND type = ?`;
params.push(type);
if (!this.__ownedAttributeCache) {
await this.loadOwnedAttributesToCache();
}
if (name) {
query += ` AND name = ?`;
params.push(name);
if (type &amp;&amp; name) {
return this.__ownedAttributeCache.filter(attr => attr.type === type &amp;&amp; attr.name === name);
}
else if (type) {
return this.__ownedAttributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__ownedAttributeCache.filter(attr => attr.name === name);
}
else {
return this.__ownedAttributeCache.slice();
}
return await repository.getEntities(query, params);
}
/**
@@ -259,19 +275,26 @@ class Note extends Entity {
}
/**
* @param {string} [name] - attribute name to filter
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise&lt;Attribute[]>} all note's attributes, including inherited ones
*/
async getAttributes(name) {
async getAttributes(type, name) {
if (!this.__attributeCache) {
await this.loadAttributesToCache();
}
if (name) {
if (type &amp;&amp; name) {
return this.__attributeCache.filter(attr => attr.type === type &amp;&amp; attr.name === name);
}
else if (type) {
return this.__attributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__attributeCache.filter(attr => attr.name === name);
}
else {
return this.__attributeCache;
return this.__attributeCache.slice();
}
}
@@ -280,7 +303,15 @@ class Note extends Entity {
* @returns {Promise&lt;Attribute[]>} all note's labels (attributes with type label), including inherited ones
*/
async getLabels(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL);
return await this.getAttributes(LABEL, name);
}
/**
* @param {string} [name] - label name to filter
* @returns {Promise&lt;Attribute[]>} all note's labels (attributes with type label), excluding inherited ones
*/
async getOwnedLabels(name) {
return await this.getOwnedAttributes(LABEL, name);
}
/**
@@ -288,7 +319,7 @@ class Note extends Entity {
* @returns {Promise&lt;Attribute[]>} all note's label definitions, including inherited ones
*/
async getLabelDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL_DEFINITION);
return await this.getAttributes(LABEL_DEFINITION, name);
}
/**
@@ -296,7 +327,15 @@ class Note extends Entity {
* @returns {Promise&lt;Attribute[]>} all note's relations (attributes with type relation), including inherited ones
*/
async getRelations(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION);
return await this.getAttributes(RELATION, name);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise&lt;Attribute[]>} all note's relations (attributes with type relation), excluding inherited ones
*/
async getOwnedRelations(name) {
return await this.getOwnedAttributes(RELATION, name);
}
/**
@@ -319,7 +358,7 @@ class Note extends Entity {
* @returns {Promise&lt;Attribute[]>} all note's relation definitions including inherited ones
*/
async getRelationDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION_DEFINITION);
return await this.getAttributes(RELATION_DEFINITION, name);
}
/**
@@ -328,6 +367,7 @@ class Note extends Entity {
*/
invalidateAttributeCache() {
this.__attributeCache = null;
this.__ownedAttributeCache = null;
}
/** @returns {Promise&lt;void>} */
@@ -337,11 +377,10 @@ class Note extends Entity {
tree(noteId, level) AS (
SELECT ?, 0
UNION
SELECT branches.parentNoteId, tree.level + 1 FROM branches
SELECT branches.parentNoteId, tree.level + 1
FROM branches
JOIN tree ON branches.noteId = tree.noteId
JOIN notes ON notes.noteId = branches.parentNoteId
WHERE notes.isDeleted = 0
AND branches.isDeleted = 0
WHERE branches.isDeleted = 0
),
treeWithAttrs(noteId, level) AS (
SELECT * FROM tree
@@ -360,6 +399,11 @@ class Note extends Entity {
// we order by noteId so that attributes from same note stay together. Actual noteId ordering doesn't matter.
const filteredAttributes = attributes.filter((attr, index) => {
// if this exact attribute already appears then don't include it (can happen via cloning)
if (attributes.findIndex(it => it.attributeId === attr.attributeId) !== index) {
return false;
}
if (attr.isDefinition()) {
const firstDefinitionIndex = attributes.findIndex(el => el.type === attr.type &amp;&amp; el.name === attr.name);
@@ -403,6 +447,15 @@ class Note extends Entity {
return !!await this.getAttribute(type, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise&lt;boolean>} true if note has an attribute with given type and name (excluding inherited)
*/
async hasOwnedAttribute(type, name) {
return !!await this.getOwnedAttribute(type, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
@@ -417,7 +470,7 @@ class Note extends Entity {
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise&lt;string>} attribute value of given type and name or null if no such attribute exists.
* @returns {Promise&lt;string|null>} attribute value of given type and name or null if no such attribute exists.
*/
async getAttributeValue(type, name) {
const attr = await this.getAttribute(type, name);
@@ -425,6 +478,17 @@ class Note extends Entity {
return attr ? attr.value : null;
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise&lt;string|null>} attribute value of given type and name or null if no such attribute exists.
*/
async getOwnedAttributeValue(type, name) {
const attr = await this.getOwnedAttribute(type, name);
return attr ? attr.value : null;
}
/**
* Based on enabled, attribute is either set or removed.
*
@@ -452,7 +516,7 @@ class Note extends Entity {
* @returns {Promise&lt;void>}
*/
async setAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
const attributes = await this.loadOwnedAttributesToCache();
let attr = attributes.find(attr => attr.type === type &amp;&amp; attr.name === name);
if (attr) {
@@ -486,7 +550,7 @@ class Note extends Entity {
* @returns {Promise&lt;void>}
*/
async removeAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
const attributes = await this.loadOwnedAttributesToCache();
for (const attribute of attributes) {
if (attribute.type === type &amp;&amp; (value === undefined || value === attribute.value)) {
@@ -498,42 +562,104 @@ class Note extends Entity {
}
}
/**
* @return {Promise&lt;Attribute>}
*/
async addAttribute(type, name, value = "") {
const attr = new Attribute({
noteId: this.noteId,
type: type,
name: name,
value: value
});
await attr.save();
this.invalidateAttributeCache();
return attr;
}
async addLabel(name, value = "") {
return await this.addAttribute(LABEL, name, value);
}
async addRelation(name, targetNoteId) {
return await this.addAttribute(RELATION, name, targetNoteId);
}
/**
* @param {string} name - label name
* @returns {Promise&lt;boolean>} true if label exists (including inherited)
*/
async hasLabel(name) { return await this.hasAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;boolean>} true if label exists (excluding inherited)
*/
async hasOwnedLabel(name) { return await this.hasOwnedAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;boolean>} true if relation exists (including inherited)
*/
async hasRelation(name) { return await this.hasAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;boolean>} true if relation exists (excluding inherited)
*/
async hasOwnedRelation(name) { return await this.hasOwnedAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;Attribute>} label if it exists, null otherwise
* @returns {Promise&lt;Attribute|null>} label if it exists, null otherwise
*/
async getLabel(name) { return await this.getAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;Attribute|null>} label if it exists, null otherwise
*/
async getOwnedLabel(name) { return await this.getOwnedAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;Attribute>} relation if it exists, null otherwise
* @returns {Promise&lt;Attribute|null>} relation if it exists, null otherwise
*/
async getRelation(name) { return await this.getAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;Attribute|null>} relation if it exists, null otherwise
*/
async getOwnedRelation(name) { return await this.getOwnedAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;string>} label value if label exists, null otherwise
* @returns {Promise&lt;string|null>} label value if label exists, null otherwise
*/
async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise&lt;string|null>} label value if label exists, null otherwise
*/
async getOwnedLabelValue(name) { return await this.getOwnedAttributeValue(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;string>} relation value if relation exists, null otherwise
* @returns {Promise&lt;string|null>} relation value if relation exists, null otherwise
*/
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise&lt;string|null>} relation value if relation exists, null otherwise
*/
async getOwnedRelationValue(name) { return await this.getOwnedAttributeValue(RELATION, name); }
/**
* @param {string} name
* @returns {Promise&lt;Note>|null} target note of the relation or null (if target is empty or note was not found)
@@ -544,6 +670,16 @@ class Note extends Entity {
return relation ? await repository.getNote(relation.value) : null;
}
/**
* @param {string} name
* @returns {Promise&lt;Note>|null} target note of the relation or null (if target is empty or note was not found)
*/
async getOwnedRelationTarget(name) {
const relation = await this.getOwnedRelation(name);
return relation ? await repository.getNote(relation.value) : null;
}
/**
* Based on enabled, label is either set or removed.
*
@@ -697,7 +833,7 @@ class Note extends Entity {
WHERE noteId = ? AND
isDeleted = 0 AND
type = 'relation' AND
name IN ('internal-link', 'image-link', 'relation-map-link')`, [this.noteId]);
name IN ('internalLink', 'imageLink', 'relationMapLink')`, [this.noteId]);
}
/**
@@ -754,6 +890,36 @@ class Note extends Entity {
AND parent_notes.isDeleted = 0`, [this.noteId]);
}
/**
* @return {Promise&lt;string[][]>} - array of notePaths (each represented by array of noteIds constituting the particular note path)
*/
async getAllNotePaths() {
if (this.noteId === 'root') {
return [['root']];
}
const notePaths = [];
for (const parentNote of await this.getParentNotes()) {
for (const parentPath of await parentNote.getAllNotePaths()) {
parentPath.push(this.noteId);
notePaths.push(parentPath);
}
}
return notePaths;
}
/**
* @param ancestorNoteId
* @return {Promise&lt;boolean>} - true if ancestorNoteId occurs in at least one of the note's paths
*/
async isDescendantOfNote(ancestorNoteId) {
const notePaths = await this.getAllNotePaths();
return notePaths.some(path => path.includes(ancestorNoteId));
}
beforeSaving() {
if (!this.isDeleted) {
this.isDeleted = false;
@@ -767,6 +933,10 @@ class Note extends Entity {
this.utcDateCreated = dateUtils.utcNowDateTime();
}
if (this.contentLength === undefined) {
this.contentLength = -1;
}
super.beforeSaving();
if (this.isChanged) {
@@ -789,7 +959,9 @@ class Note extends Entity {
delete pojo.isContentAvailable;
delete pojo.__attributeCache;
delete pojo.__ownedAttributeCache;
delete pojo.content;
/** zero references to contentHash, probably can be removed */
delete pojo.contentHash;
}
}
+23 -13
View File
@@ -39,24 +39,26 @@ const syncTableService = require('../services/sync_table');
/**
* NoteRevision represents snapshot of note's title and content at some point in the past. It's used for seamless note versioning.
*
* @param {string} noteRevisionId
* @param {string} noteId
* @param {string} type
* @param {string} mime
* @param {string} title
* @param {string} isProtected
* @param {string} dateLastEdited
* @param {string} dateCreated
* @param {string} utcDateLastEdited
* @param {string} utcDateCreated
* @param {string} utcDateModified
* @property {string} noteRevisionId
* @property {string} noteId
* @property {string} type
* @property {string} mime
* @property {string} title
* @property {int} contentLength
* @property {boolean} isErased
* @property {boolean} isProtected
* @property {string} dateLastEdited
* @property {string} dateCreated
* @property {string} utcDateLastEdited
* @property {string} utcDateCreated
* @property {string} utcDateModified
*
* @extends Entity
*/
class NoteRevision extends Entity {
static get entityName() { return "note_revisions"; }
static get primaryKeyName() { return "noteRevisionId"; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "contentLength", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "contentLength", "isErased", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }
constructor(row) {
super(row);
@@ -130,7 +132,7 @@ class NoteRevision extends Entity {
async setContent(content) {
// force updating note itself so that utcDateModified is represented correctly even for the content
this.forcedChange = true;
this.contentLength = content.length;
this.contentLength = content === null ? 0 : content.length;
await this.save();
this.content = content;
@@ -156,6 +158,14 @@ class NoteRevision extends Entity {
await syncTableService.addNoteRevisionContentSync(this.noteRevisionId);
}
beforeSaving() {
super.beforeSaving();
if (this.isChanged) {
this.utcDateModified = dateUtils.utcNowDateTime();
}
}
// cannot be static!
updatePojo(pojo) {
if (pojo.isProtected) {
+5 -5
View File
@@ -34,11 +34,11 @@ const dateUtils = require('../services/date_utils');
/**
* Option represents name-value pair, either directly configurable by the user or some system property.
*
* @param {string} name
* @param {string} value
* @param {boolean} isSynced
* @param {string} utcDateModified
* @param {string} utcDateCreated
* @property {string} name
* @property {string} value
* @property {boolean} isSynced
* @property {string} utcDateModified
* @property {string} utcDateCreated
*
* @extends Entity
*/
@@ -34,10 +34,10 @@ const dateUtils = require('../services/date_utils');
/**
* RecentNote represents recently visited note.
*
* @param {string} noteId
* @param {string} notePath
* @param {boolean} isDeleted
* @param {string} utcDateModified
* @property {string} noteId
* @property {string} notePath
* @property {boolean} isDeleted
* @property {string} utcDateModified
*
* @extends Entity
*/
+496 -1
View File
@@ -102,6 +102,313 @@
<h4 class="name" id="CreateNewNoteParams">CreateNewNoteParams</h4>
<h5>Type:</h5>
<ul>
<li>
<span class="param-type">object</span>
</li>
</ul>
<h5 class="subsection-title">Properties:</h5>
<table class="props">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="name"><code>parentNoteId</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last">MANDATORY</td>
</tr>
<tr>
<td class="name"><code>title</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last">MANDATORY</td>
</tr>
<tr>
<td class="name"><code>content</code></td>
<td class="type">
<span class="param-type">string</span>
|
<span class="param-type">buffer</span>
</td>
<td class="description last">MANDATORY</td>
</tr>
<tr>
<td class="name"><code>type</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last">text, code, file, image, search, book, relation-map - MANDATORY</td>
</tr>
<tr>
<td class="name"><code>mime</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last">value is derived from default mimes for type</td>
</tr>
<tr>
<td class="name"><code>isProtected</code></td>
<td class="type">
<span class="param-type">boolean</span>
</td>
<td class="description last">default is false</td>
</tr>
<tr>
<td class="name"><code>isExpanded</code></td>
<td class="type">
<span class="param-type">boolean</span>
</td>
<td class="description last">default is false</td>
</tr>
<tr>
<td class="name"><code>prefix</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last">default is empty string</td>
</tr>
<tr>
<td class="name"><code>notePosition</code></td>
<td class="type">
<span class="param-type">int</span>
</td>
<td class="description last">default is last existing notePosition in a parent + 10</td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line212">line 212</a>
</li></ul></dd>
</dl>
<h4 class="name" id="CreateNoteAttribute">CreateNoteAttribute</h4>
@@ -290,6 +597,194 @@
<h4 class="name" id="CreateNoteAttribute">CreateNoteAttribute</h4>
<h5>Type:</h5>
<ul>
<li>
<span class="param-type">object</span>
</li>
</ul>
<h5 class="subsection-title">Properties:</h5>
<table class="props">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Attributes</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="name"><code>type</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="attributes">
</td>
<td class="description last">attribute type - label, relation etc.</td>
</tr>
<tr>
<td class="name"><code>name</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="attributes">
</td>
<td class="description last">attribute name</td>
</tr>
<tr>
<td class="name"><code>value</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="attributes">
&lt;optional><br>
</td>
<td class="description last">attribute value</td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line233">line 233</a>
</li></ul></dd>
</dl>
<h4 class="name" id="CreateNoteExtraOptions">CreateNoteExtraOptions</h4>
@@ -558,7 +1053,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line180">line 180</a>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line240">line 240</a>
</li></ul></dd>
@@ -198,6 +198,66 @@ function BackendScriptApi(currentNote, apiParams) {
*/
this.toggleNoteInParent = cloningService.toggleNoteInParent;
/**
* @typedef {object} CreateNoteAttribute
* @property {string} type - attribute type - label, relation etc.
* @property {string} name - attribute name
* @property {string} [value] - attribute value
*/
/**
* Create text note. See also createNewNote() for more options.
*
* @param {string} parentNoteId
* @param {string} title
* @param {string} content
* @return {Promise&lt;{note: Note, branch: Branch}>}
*/
this.createTextNote = async (parentNoteId, title, content = '') => await noteService.createNewNote({
parentNoteId,
title,
content,
type: 'text'
});
/**
* Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and
* JSON MIME type. See also createNewNote() for more options.
*
* @param {string} parentNoteId
* @param {string} title
* @param {object} content
* @return {Promise&lt;{note: Note, branch: Branch}>}
*/
this.createDataNote = async (parentNoteId, title, content = {}) => await noteService.createNewNote({
parentNoteId,
title,
content: JSON.stringify(content),
type: 'code',
mime: 'application/json'
});
/**
* @typedef {object} CreateNewNoteParams
* @property {string} parentNoteId - MANDATORY
* @property {string} title - MANDATORY
* @property {string|buffer} content - MANDATORY
* @property {string} type - text, code, file, image, search, book, relation-map - MANDATORY
* @property {string} mime - value is derived from default mimes for type
* @property {boolean} isProtected - default is false
* @property {boolean} isExpanded - default is false
* @property {string} prefix - default is empty string
* @property {int} notePosition - default is last existing notePosition in a parent + 10
*/
/**
* @method
*
* @param {CreateNewNoteParams} [params]
* @returns {Promise&lt;{note: Note, branch: Branch}>} object contains newly created entities note and branch
*/
this.createNewNote = noteService.createNewNote;
/**
* @typedef {object} CreateNoteAttribute
* @property {string} type - attribute type - label, relation etc.
@@ -223,25 +283,38 @@ function BackendScriptApi(currentNote, apiParams) {
* @param {CreateNoteExtraOptions} [extraOptions={}]
* @returns {Promise&lt;{note: Note, branch: Branch}>} object contains newly created entities note and branch
*/
this.createNote = noteService.createNote;
this.createNote = async (parentNoteId, title, content = "", extraOptions= {}) => {
extraOptions.parentNoteId = parentNoteId;
extraOptions.title = title;
/**
* Creates new note according to given params and force all connected clients to refresh their tree.
*
* @method
*
* @param {string} parentNoteId - create new note under this parent
* @param {string} title
* @param {string} [content=""]
* @param {CreateNoteExtraOptions} [extraOptions={}]
* @returns {Promise&lt;{note: Note, branch: Branch}>} object contains newly created entities note and branch
*/
this.createNoteAndRefresh = async function(parentNoteId, title, content, extraOptions) {
const ret = await noteService.createNote(parentNoteId, title, content, extraOptions);
const parentNote = await repository.getNote(parentNoteId);
ws.refreshTree();
return ret;
// code note type can be inherited, otherwise text is default
extraOptions.type = parentNote.type === 'code' ? 'code' : 'text';
extraOptions.mime = parentNote.type === 'code' ? parentNote.mime : 'text/html';
if (extraOptions.json) {
extraOptions.content = JSON.stringify(content || {}, null, '\t');
extraOptions.type = 'code';
extraOptions.mime = 'application/json';
}
else {
extraOptions.content = content;
}
const {note, branch} = await noteService.createNewNote(extraOptions);
for (const attr of extraOptions.attributes || []) {
await attributeService.createAttribute({
noteId: note.noteId,
type: attr.type,
name: attr.name,
value: attr.value,
isInheritable: !!attr.isInheritable
});
}
return {note, branch};
};
/**
@@ -268,6 +341,14 @@ function BackendScriptApi(currentNote, apiParams) {
*/
this.getDateNote = dateNoteService.getDateNote;
/**
* Returns today's day note. If such note doesn't exist, it is created.
*
* @method
* @returns {Promise&lt;Note|null>}
*/
this.getTodayNote = dateNoteService.getTodayNote;
/**
* Returns note for the first date of the week of the given date.
*
+84
View File
@@ -4975,6 +4975,90 @@ Internally this serializes the anonymous function into string and sends it to ba
<h4 class="name" id="waitUntilSynced"><span class="type-signature"></span>waitUntilSynced<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line389">line 389</a>
</li></ul></dd>
</dl>
File diff suppressed because it is too large Load Diff
+280
View File
@@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Class: KeyboardActions</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Class: KeyboardActions</h1>
<section>
<header>
<h2><span class="attribs"><span class="type-signature"></span></span>KeyboardActions<span class="signature">()</span><span class="type-signature"></span></h2>
<div class="class-description">blaa vlaa</div>
</header>
<article>
<div class="container-overview">
<h2>Constructor</h2>
<h4 class="name" id="KeyboardActions"><span class="type-signature"></span>new KeyboardActions<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_keyboard_actions.js.html">services/keyboard_action.js</a>, <a href="services_keyboard_actions.js.html#line5">line 5</a>
</li></ul></dd>
</dl>
</div>
<h3 class="subsection-title">Members</h3>
<h4 class="name" id="JUMP_TO"><span class="type-signature"></span>JUMP_TO<span class="type-signature"></span></h4>
<h5 class="subsection-title">Properties:</h5>
<table class="props">
<thead>
<tr>
<th>Type</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last"></td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_keyboard_actions.js.html">services/keyboard_action.js</a>, <a href="services_keyboard_actions.js.html#line7">line 7</a>
</li></ul></dd>
</dl>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="KeyboardActions.html">KeyboardActions</a></li><li><a href="NoteFull.html">NoteFull</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.3</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -47,7 +47,7 @@ class Branch {
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
return this.treeCache.getNote(this.noteId);
}
/** @returns {boolean} true if it's top level, meaning its parent is root note */
+26 -15
View File
@@ -49,6 +49,8 @@ class NoteShort {
this.noteId = row.noteId;
/** @param {string} */
this.title = row.title;
/** @param {int} */
this.contentLength = row.contentLength;
/** @param {boolean} */
this.isProtected = row.isProtected;
/** @param {string} one of 'text', 'code', 'file' or 'render' */
@@ -61,6 +63,8 @@ class NoteShort {
this.archived = row.archived;
/** @param {string} */
this.cssClass = row.cssClass;
/** @param {string} */
this.iconClass = row.iconClass;
/** @type {string[]} */
this.parents = [];
@@ -178,20 +182,27 @@ class NoteShort {
}
/**
* @param {string} [name] - attribute name to filter
* @returns {Promise&lt;Attribute[]>}
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise&lt;Attribute[]>} all note's attributes, including inherited ones
*/
async getAttributes(name) {
if (!this.attributeCache) {
this.attributeCache = (await server.get('notes/' + this.noteId + '/attributes'))
async getAttributes(type, name) {
if (!this.__attributeCache) {
this.__attributeCache = (await server.get('notes/' + this.noteId + '/attributes'))
.map(attrRow => new Attribute(this.treeCache, attrRow));
}
if (name) {
return this.attributeCache.filter(attr => attr.name === name);
if (type &amp;&amp; name) {
return this.__attributeCache.filter(attr => attr.type === type &amp;&amp; attr.name === name);
}
else if (type) {
return this.__attributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__attributeCache.filter(attr => attr.name === name);
}
else {
return this.attributeCache;
return this.__attributeCache.slice();
}
}
@@ -200,7 +211,7 @@ class NoteShort {
* @returns {Promise&lt;Attribute[]>} all note's labels (attributes with type label), including inherited ones
*/
async getLabels(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL);
return await this.getAttributes(LABEL, name);
}
/**
@@ -208,7 +219,7 @@ class NoteShort {
* @returns {Promise&lt;Attribute[]>} all note's label definitions, including inherited ones
*/
async getLabelDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL_DEFINITION);
return await this.getAttributes(LABEL_DEFINITION, name);
}
/**
@@ -216,7 +227,7 @@ class NoteShort {
* @returns {Promise&lt;Attribute[]>} all note's relations (attributes with type relation), including inherited ones
*/
async getRelations(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION);
return await this.getAttributes(RELATION, name);
}
/**
@@ -224,7 +235,7 @@ class NoteShort {
* @returns {Promise&lt;Attribute[]>} all note's relation definitions including inherited ones
*/
async getRelationDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION_DEFINITION);
return await this.getAttributes(RELATION_DEFINITION, name);
}
/**
@@ -323,8 +334,8 @@ class NoteShort {
* Clear note's attributes cache to force fresh reload for next attribute request.
* Cache is note instance scoped.
*/
invalidateAttributeCache() {
this.attributeCache = null;
invalidate__attributeCache() {
this.__attributeCache = null;
}
/**
@@ -345,7 +356,7 @@ class NoteShort {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.archived;
delete dto.attributeCache;
delete dto.__attributeCache;
return dto;
}
+280
View File
@@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Class: exports</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Class: exports</h1>
<section>
<header>
<h2><span class="attribs"><span class="type-signature"></span></span>exports<span class="signature">()</span><span class="type-signature"></span></h2>
<div class="class-description">blaa vlaa</div>
</header>
<article>
<div class="container-overview">
<h2>Constructor</h2>
<h4 class="name" id="exports"><span class="type-signature"></span>new exports<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_keyboard_actions.js.html">services/keyboard_action.js</a>, <a href="services_keyboard_actions.js.html#line5">line 5</a>
</li></ul></dd>
</dl>
</div>
<h3 class="subsection-title">Members</h3>
<h4 class="name" id="JUMP_TO"><span class="type-signature"></span>JUMP_TO<span class="type-signature"></span></h4>
<h5 class="subsection-title">Properties:</h5>
<table class="props">
<thead>
<tr>
<th>Type</th>
<th class="last">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="description last"></td>
</tr>
</tbody>
</table>
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_keyboard_actions.js.html">services/keyboard_action.js</a>, <a href="services_keyboard_actions.js.html#line7">line 7</a>
</li></ul></dd>
</dl>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteFull.html">NoteFull</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.3</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>
@@ -115,7 +115,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
const button = $('&lt;button>')
.addClass("btn btn-sm")
.click(opts.action);
.on('click', opts.action);
if (opts.icon) {
button.append($("&lt;span>").addClass("bx bx-" + opts.icon))
@@ -410,6 +410,11 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
* @param {function} handler
*/
this.bindGlobalShortcut = utils.bindGlobalShortcut;
/**
* @method
*/
this.waitUntilSynced = ws.waitForMaxKnownSyncId;
}
export default FrontendScriptApi;</code></pre>
@@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: services/keyboard_action.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: services/keyboard_action.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* blaa vlaa
*/
class KeyboardAction {
constructor(params) {
this.optionName = params.optionName;
}
}
/**
* Open "Jump to note" dialog
* @static
*/
KeyboardAction.JumpToNote = new KeyboardAction({
optionName: "JumpToNote",
defaultShortcuts: "mod+j",
description: 'Open "Jump to note" dialog'
});
/** @static */
KeyboardAction.MarkdownToHTML = new KeyboardAction({
optionName: "MarkdownToHTML",
defaultShortcuts: "mod+return"
});
/** @static */
KeyboardAction.NewTab = new KeyboardAction({
optionName: "NewTab",
defaultShortcuts: "mod+t"
});
/** @static */
KeyboardAction.CloseTab = new KeyboardAction({
optionName: "CloseTab",
defaultShortcuts: "mod+w"
});
/** @static */
KeyboardAction.NextTab = new KeyboardAction({
optionName: "NextTab",
defaultShortcuts: "mod+tab"
});
/** @static */
KeyboardAction.PreviousTab = new KeyboardAction({
optionName: "PreviousTab",
defaultShortcuts: "mod+shift+tab"
});
/** @static */
KeyboardAction.CreateNoteAfter = new KeyboardAction({
optionName: "CreateNoteAfter",
defaultShortcuts: "mod+o"
});
/** @static */
KeyboardAction.CreateNoteInto = new KeyboardAction({
optionName: "CreateNoteInto",
defaultShortcuts: "mod+p"
});
/** @static */
KeyboardAction.ScrollToActiveNote = new KeyboardAction({
optionName: "ScrollToActiveNote",
defaultShortcuts: "mod+."
});
/** @static */
KeyboardAction.CollapseTree = new KeyboardAction({
optionName: "CollapseTree",
defaultShortcuts: "alt+c"
});
/** @static */
KeyboardAction.RunSQL = new KeyboardAction({
optionName: "RunSQL",
defaultShortcuts: "mod+return"
});
/** @static */
KeyboardAction.FocusNote = new KeyboardAction({
optionName: "FocusNote",
defaultShortcuts: "return"
});
/** @static */
KeyboardAction.RunCurrentNote = new KeyboardAction({
optionName: "RunCurrentNote",
defaultShortcuts: "mod+return"
});
/** @static */
KeyboardAction.ClipboardCopy = new KeyboardAction({
optionName: "ClipboardCopy",
defaultShortcuts: "mod+c"
});
/** @static */
KeyboardAction.ClipboardPaste = new KeyboardAction({
optionName: "ClipboardPaste",
defaultShortcuts: "mod+v"
});
/** @static */
KeyboardAction.ClipboardCut = new KeyboardAction({
optionName: "ClipboardCut",
defaultShortcuts: "mod+x"
});
/** @static */
KeyboardAction.SelectAllNotesInParent = new KeyboardAction({
optionName: "SelectAllNotesInParent",
defaultShortcuts: "mod+a"
});
/** @static */
KeyboardAction.Undo = new KeyboardAction({
optionName: "Undo",
defaultShortcuts: "mod+z"
});
/** @static */
KeyboardAction.Redo = new KeyboardAction({
optionName: "Redo",
defaultShortcuts: "mod+y"
});
/** @static */
KeyboardAction.AddLinkToText = new KeyboardAction({
optionName: "AddLinkToText",
defaultShortcuts: "mod+l"
});
/** @static */
KeyboardAction.CloneNotesTo = new KeyboardAction({
optionName: "CloneNotesTo",
defaultShortcuts: "mod+shift+c"
});
/** @static */
KeyboardAction.MoveNotesTo = new KeyboardAction({
optionName: "MoveNotesTo",
defaultShortcuts: "mod+shift+c"
});
/** @static */
KeyboardAction.SearchNotes = new KeyboardAction({
optionName: "SearchNotes",
defaultShortcuts: "mod+s"
});
/** @static */
KeyboardAction.ShowAttributes = new KeyboardAction({
optionName: "ShowAttributes",
defaultShortcuts: "alt+a"
});
/** @static */
KeyboardAction.ShowHelp = new KeyboardAction({
optionName: "ShowHelp",
defaultShortcuts: "f1"
});
/** @static */
KeyboardAction.OpenSQLConsole = new KeyboardAction({
optionName: "OpenSQLConsole",
defaultShortcuts: "alt+o"
});
/** @static */
KeyboardAction.BackInNoteHistory = new KeyboardAction({
optionName: "BackInNoteHistory",
defaultShortcuts: "alt+left"
});
/** @static */
KeyboardAction.ForwardInNoteHistory = new KeyboardAction({
optionName: "ForwardInNoteHistory",
defaultShortcuts: "alt+right"
});
/** @static */
KeyboardAction.ToggleZenMode = new KeyboardAction({
optionName: "ToggleZenMode",
defaultShortcuts: "alt+m"
});
/** @static */
KeyboardAction.InsertDateTime = new KeyboardAction({
optionName: "InsertDateTime",
defaultShortcuts: "alt+t"
});
/** @static */
KeyboardAction.ReloadApp = new KeyboardAction({
optionName: "ReloadApp",
defaultShortcuts: ["f5", "mod+r"]
});
/** @static */
KeyboardAction.OpenDevTools = new KeyboardAction({
optionName: "OpenDevTools",
defaultShortcuts: "mod+shift+i"
});
/** @static */
KeyboardAction.FindInText = new KeyboardAction({
optionName: "FindInText",
defaultShortcuts: "mod+f"
});
/** @static */
KeyboardAction.ToggleFullscreen = new KeyboardAction({
optionName: "ToggleFullscreen",
defaultShortcuts: "f11"
});
/** @static */
KeyboardAction.ZoomOut = new KeyboardAction({
optionName: "ZoomOut",
defaultShortcuts: "mod+-"
});
/** @static */
KeyboardAction.ZoomIn = new KeyboardAction({
optionName: "ZoomIn",
defaultShortcuts: "mod+="
});
export default KeyboardAction;</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="KeyboardAction.html">KeyboardAction</a></li><li><a href="NoteFull.html">NoteFull</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.3</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: services/keyboard_action.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: services/keyboard_action.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* blaa vlaa
*/
class KeyboardActions {
constructor() {
/** @property {string} */
this.JUMP_TO = "";
}
}
export default KeyboardActions;</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="KeyboardActions.html">KeyboardActions</a></li><li><a href="NoteFull.html">NoteFull</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.3</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>
+13 -85
View File
@@ -1,85 +1,17 @@
'use strict';
const {app, globalShortcut, BrowserWindow} = require('electron');
const path = require('path');
const log = require('./src/services/log');
const {app, globalShortcut} = require('electron');
const sqlInit = require('./src/services/sql_init');
const cls = require('./src/services/cls');
const url = require("url");
const port = require('./src/services/port');
const env = require('./src/services/env');
const appIconService = require('./src/services/app_icon');
const windowStateKeeper = require('electron-window-state');
const windowService = require('./src/services/window');
// Adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')();
appIconService.installLocalAppIcon();
// Prevent window being garbage collected
let mainWindow;
require('electron-dl')({ saveAs: true });
function onClosed() {
// Dereference the window
// For multiple windows store them in an array
mainWindow = null;
}
async function createMainWindow() {
await sqlInit.dbConnection;
// if schema doesn't exist -> setup process
// if schema exists, then we need to wait until the migration process is finished
if (await sqlInit.schemaExists()) {
await sqlInit.dbReady;
}
const mainWindowState = windowStateKeeper({
// default window width & height so it's usable on 1600 * 900 display (including some extra panels etc.)
defaultWidth: 1200,
defaultHeight: 800
});
const win = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
title: 'Trilium Notes',
webPreferences: {
nodeIntegration: true
},
icon: path.join(__dirname, 'images/app-icons/png/256x256' + (env.isDev() ? '-dev' : '') + '.png')
});
mainWindowState.manage(win);
win.setMenuBarVisibility(false);
win.loadURL('http://127.0.0.1:' + await port);
win.on('closed', onClosed);
win.webContents.on('new-window', (e, url) => {
if (url !== win.webContents.getURL()) {
e.preventDefault();
require('electron').shell.openExternal(url);
}
});
// prevent drag & drop to navigate away from trilium
win.webContents.on('will-navigate', (ev, targetUrl) => {
const parsedUrl = url.parse(targetUrl);
// we still need to allow internal redirects from setup and migration pages
if (!['localhost', '127.0.0.1'].includes(parsedUrl.hostname) || (parsedUrl.path && parsedUrl.path !== '/')) {
ev.preventDefault();
}
});
return win;
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
@@ -89,27 +21,23 @@ app.on('window-all-closed', () => {
}
});
app.on('activate', () => {
if (!mainWindow) {
mainWindow = createMainWindow();
}
});
app.on('ready', async () => {
app.setAppUserModelId('com.github.zadam.trilium');
mainWindow = await createMainWindow();
await sqlInit.dbConnection;
const result = globalShortcut.register('CommandOrControl+Alt+P', cls.wrap(async () => {
// window may be hidden / not in focus
mainWindow.focus();
// if schema doesn't exist -> setup process
// if schema exists, then we need to wait until the migration process is finished
if (await sqlInit.schemaExists()) {
await sqlInit.dbReady;
mainWindow.webContents.send('create-day-sub-note');
}));
if (!result) {
log.error("Could not register global shortcut CTRL+ALT+P");
await windowService.createMainWindow();
}
else {
await windowService.createSetupWindow();
}
await windowService.registerGlobalShortcuts();
});
app.on('will-quit', () => {
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
+310
View File
@@ -0,0 +1,310 @@
/*
* !!!!!!! This stylesheet is heavily modified compared to the original for similarity with in-editor look !!!!!!!
* This is used for printing and tar HTML export
*
* CKEditor 5 (v15.0.0) content styles.
* Generated on Wed, 27 Nov 2019 13:26:13 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
*/
:root {
--ck-highlight-marker-blue: #72cdfd;
--ck-highlight-marker-green: #63f963;
--ck-highlight-marker-pink: #fc7999;
--ck-highlight-marker-yellow: #fdfd77;
--ck-highlight-pen-green: #118800;
--ck-highlight-pen-red: #e91313;
--ck-image-style-spacing: 1.5em;
--ck-todo-list-checkmark-size: 16px;
font-family: Arial, Sans-Serif;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list {
list-style: none;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list li {
margin-bottom: 5px;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list li .todo-list {
margin-top: 5px;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input[checked]::before {
border-color: hsl(126, 64%, 41%);
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input[checked]::after {
border-color: hsl(126, 64%, 41%);
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* ckeditor5-image/theme/image.css */
.image {
display: table;
clear: both;
text-align: center;
margin: 1em auto;
}
/* ckeditor5-image/theme/image.css */
.image > img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 50px;
}
/* ckeditor5-block-quote/theme/blockquote.css */
blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-image/theme/imageresize.css */
.image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* ckeditor5-image/theme/imageresize.css */
.image.image_resized img {
width: 100%;
}
/* ckeditor5-image/theme/imageresize.css */
.image.image_resized > figcaption {
display: block;
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-side,
.image-style-align-left,
.image-style-align-center,
.image-style-align-right {
max-width: 50%;
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-align-left {
float: left;
margin-right: var(--ck-image-style-spacing);
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-align-center {
margin-left: auto;
margin-right: auto;
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-align-right {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* ckeditor5-media-embed/theme/mediaembed.css */
.media {
clear: both;
margin: 1em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-table/theme/table.css */
.table {
display: table;
margin: 0;
}
/* ckeditor5-table/theme/table.css */
.table table {
border-collapse: collapse;
border-spacing: 0;
border: 1px double hsl(0, 0%, 70%);
}
/* ckeditor5-table/theme/table.css */
.table table td,
.table table th {
min-width: 2em;
padding: .4em;
border: 1px solid #d9d9d9;
}
/* ckeditor5-table/theme/table.css */
.table table th {
font-weight: bold;
background-color: #f5f5f5;
text-align: left;
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* ckeditor5-highlight/theme/highlight.css */
.pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* ckeditor5-highlight/theme/highlight.css */
.pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: #fff;
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* ckeditor5-basic-styles/theme/code.css */
code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* ckeditor5-image/theme/imagecaption.css */
.image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: hsl(0, 0%, 20%);
background-color: hsl(0, 0%, 97%);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-horizontal-line/theme/horizontalline.css */
hr {
border-width: 1px 0 0;
border-style: solid;
border-color: hsl(0, 0%, 37%);
margin: 0;
}
/* ckeditor5-code-block/theme/codeblock.css */
pre {
padding: 1em;
color: #353535;
background: hsla(0, 0%, 78%, 0.3);
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
}
/* ckeditor5-code-block/theme/codeblock.css */
pre code {
background: unset;
padding: 0;
border-radius: 0;
}
.ck-widget__selection-handle {
display: none;
}
@media print {
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break {
padding: 0;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break::after {
display: none;
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -498,7 +498,7 @@
t._started = false;
onRenderStop();
} else {
setImmediate(step);
requestIdleCallback(step, { timeout: 10 });
}
}
+492 -4301
View File
File diff suppressed because it is too large Load Diff
+20 -27
View File
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.37.0-beta",
"version": "0.39.2-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -27,34 +27,34 @@
"commonmark": "0.29.0",
"cookie-parser": "1.4.4",
"csurf": "1.10.0",
"dayjs": "1.8.17",
"dayjs": "1.8.18",
"debug": "4.1.1",
"ejs": "2.7.1",
"ejs": "2.7.4",
"electron-debug": "3.0.1",
"electron-dl": "1.14.0",
"electron-dl": "2.0.0",
"electron-find": "1.0.6",
"electron-spellchecker": "2.2.0",
"electron-spellchecker": "2.2.1",
"electron-window-state": "5.0.3",
"express": "4.17.1",
"express-session": "1.17.0",
"file-type": "12.4.0",
"file-type": "12.4.2",
"fs-extra": "8.1.0",
"helmet": "3.21.2",
"html": "1.0.0",
"html2plaintext": "2.1.2",
"http-proxy-agent": "2.1.0",
"https-proxy-agent": "3.0.1",
"http-proxy-agent": "3.0.0",
"https-proxy-agent": "4.0.0",
"image-type": "4.1.0",
"imagemin": "7.0.0",
"imagemin": "7.0.1",
"imagemin-giflossy": "5.1.10",
"imagemin-mozjpeg": "8.0.0",
"imagemin-pngquant": "8.0.0",
"ini": "1.3.5",
"jimp": "0.8.5",
"mime-types": "2.1.24",
"jimp": "0.9.3",
"mime-types": "2.1.25",
"moment": "2.24.0",
"multer": "1.4.2",
"node-abi": "2.12.0",
"node-abi": "2.13.0",
"open": "7.0.0",
"pngjs": "3.4.0",
"portscanner": "2.2.0",
@@ -63,35 +63,28 @@
"rimraf": "3.0.0",
"sanitize-filename": "1.6.3",
"sax": "1.2.4",
"semver": "6.3.0",
"semver": "7.1.1",
"serve-favicon": "2.5.0",
"session-file-store": "1.3.1",
"simple-node-logger": "18.12.23",
"sqlite": "3.0.3",
"sqlite3": "4.1.0",
"sqlite3": "4.1.1",
"string-similarity": "3.0.0",
"tar-stream": "2.1.0",
"turndown": "5.0.3",
"turndown-plugin-gfm": "1.0.2",
"unescape": "1.0.1",
"ws": "7.2.0",
"xml2js": "0.4.22"
"ws": "7.2.1"
},
"devDependencies": {
"electron": "6.0.12",
"electron-builder": "22.1.0",
"electron-compile": "6.4.4",
"electron-installer-debian": "2.0.1",
"electron-packager": "14.1.0",
"electron-rebuild": "1.8.6",
"electron-packager": "14.1.1",
"electron-rebuild": "1.8.8",
"jsdoc": "3.6.3",
"lorem-ipsum": "2.0.3",
"xo": "0.25.3"
"lorem-ipsum": "2.0.3"
},
"xo": {
"envs": [
"node",
"browser"
]
"optionalDependencies": {
"electron-installer-debian": "2.0.1"
}
}
+5
View File
@@ -79,6 +79,10 @@ class Attribute extends Entity {
async beforeSaving() {
if (!this.value) {
if (this.type === 'relation') {
throw new Error(`Cannot save relation ${this.name} since it does not target any note.`);
}
// null value isn't allowed
this.value = "";
}
@@ -109,6 +113,7 @@ class Attribute extends Entity {
// cannot be static!
updatePojo(pojo) {
delete pojo.isOwned;
delete pojo.__note;
}
}
+199 -33
View File
@@ -114,6 +114,10 @@ class Note extends Entity {
/** @returns {Promise} */
async setContent(content) {
if (content === null || content === undefined) {
throw new Error(`Cannot set null content to note ${this.noteId}`);
}
// force updating note itself so that dateModified is represented correctly even for the content
this.forcedChange = true;
this.contentLength = content.length;
@@ -192,26 +196,36 @@ class Note extends Entity {
return null;
}
async loadOwnedAttributesToCache() {
this.__ownedAttributeCache = await repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
return this.__ownedAttributeCache;
}
/**
* @returns {Promise<Attribute[]>} attributes belonging to this specific note (excludes inherited attributes)
* This method is a faster variant of getAttributes() which looks for only owned attributes.
* Use when inheritance is not needed and/or in batch/performance sensitive operations.
*
* This method can be significantly faster than the getAttributes()
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise<Attribute[]>} note's "owned" attributes - excluding inherited ones
*/
async getOwnedAttributes(type, name) {
let query = `SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`;
const params = [this.noteId];
if (type) {
query += ` AND type = ?`;
params.push(type);
if (!this.__ownedAttributeCache) {
await this.loadOwnedAttributesToCache();
}
if (name) {
query += ` AND name = ?`;
params.push(name);
if (type && name) {
return this.__ownedAttributeCache.filter(attr => attr.type === type && attr.name === name);
}
else if (type) {
return this.__ownedAttributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__ownedAttributeCache.filter(attr => attr.name === name);
}
else {
return this.__ownedAttributeCache.slice();
}
return await repository.getEntities(query, params);
}
/**
@@ -233,19 +247,26 @@ class Note extends Entity {
}
/**
* @param {string} [name] - attribute name to filter
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise<Attribute[]>} all note's attributes, including inherited ones
*/
async getAttributes(name) {
async getAttributes(type, name) {
if (!this.__attributeCache) {
await this.loadAttributesToCache();
}
if (name) {
if (type && name) {
return this.__attributeCache.filter(attr => attr.type === type && attr.name === name);
}
else if (type) {
return this.__attributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__attributeCache.filter(attr => attr.name === name);
}
else {
return this.__attributeCache;
return this.__attributeCache.slice();
}
}
@@ -254,7 +275,15 @@ class Note extends Entity {
* @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones
*/
async getLabels(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL);
return await this.getAttributes(LABEL, name);
}
/**
* @param {string} [name] - label name to filter
* @returns {Promise<Attribute[]>} all note's labels (attributes with type label), excluding inherited ones
*/
async getOwnedLabels(name) {
return await this.getOwnedAttributes(LABEL, name);
}
/**
@@ -262,7 +291,7 @@ class Note extends Entity {
* @returns {Promise<Attribute[]>} all note's label definitions, including inherited ones
*/
async getLabelDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL_DEFINITION);
return await this.getAttributes(LABEL_DEFINITION, name);
}
/**
@@ -270,7 +299,15 @@ class Note extends Entity {
* @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones
*/
async getRelations(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION);
return await this.getAttributes(RELATION, name);
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), excluding inherited ones
*/
async getOwnedRelations(name) {
return await this.getOwnedAttributes(RELATION, name);
}
/**
@@ -293,7 +330,7 @@ class Note extends Entity {
* @returns {Promise<Attribute[]>} all note's relation definitions including inherited ones
*/
async getRelationDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION_DEFINITION);
return await this.getAttributes(RELATION_DEFINITION, name);
}
/**
@@ -302,6 +339,7 @@ class Note extends Entity {
*/
invalidateAttributeCache() {
this.__attributeCache = null;
this.__ownedAttributeCache = null;
}
/** @returns {Promise<void>} */
@@ -311,11 +349,10 @@ class Note extends Entity {
tree(noteId, level) AS (
SELECT ?, 0
UNION
SELECT branches.parentNoteId, tree.level + 1 FROM branches
SELECT branches.parentNoteId, tree.level + 1
FROM branches
JOIN tree ON branches.noteId = tree.noteId
JOIN notes ON notes.noteId = branches.parentNoteId
WHERE notes.isDeleted = 0
AND branches.isDeleted = 0
WHERE branches.isDeleted = 0
),
treeWithAttrs(noteId, level) AS (
SELECT * FROM tree
@@ -334,6 +371,11 @@ class Note extends Entity {
// we order by noteId so that attributes from same note stay together. Actual noteId ordering doesn't matter.
const filteredAttributes = attributes.filter((attr, index) => {
// if this exact attribute already appears then don't include it (can happen via cloning)
if (attributes.findIndex(it => it.attributeId === attr.attributeId) !== index) {
return false;
}
if (attr.isDefinition()) {
const firstDefinitionIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name);
@@ -377,6 +419,15 @@ class Note extends Entity {
return !!await this.getAttribute(type, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<boolean>} true if note has an attribute with given type and name (excluding inherited)
*/
async hasOwnedAttribute(type, name) {
return !!await this.getOwnedAttribute(type, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
@@ -391,7 +442,7 @@ class Note extends Entity {
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<string>} attribute value of given type and name or null if no such attribute exists.
* @returns {Promise<string|null>} attribute value of given type and name or null if no such attribute exists.
*/
async getAttributeValue(type, name) {
const attr = await this.getAttribute(type, name);
@@ -399,6 +450,17 @@ class Note extends Entity {
return attr ? attr.value : null;
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {Promise<string|null>} attribute value of given type and name or null if no such attribute exists.
*/
async getOwnedAttributeValue(type, name) {
const attr = await this.getOwnedAttribute(type, name);
return attr ? attr.value : null;
}
/**
* Based on enabled, attribute is either set or removed.
*
@@ -426,7 +488,7 @@ class Note extends Entity {
* @returns {Promise<void>}
*/
async setAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
const attributes = await this.loadOwnedAttributesToCache();
let attr = attributes.find(attr => attr.type === type && attr.name === name);
if (attr) {
@@ -460,7 +522,7 @@ class Note extends Entity {
* @returns {Promise<void>}
*/
async removeAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
const attributes = await this.loadOwnedAttributesToCache();
for (const attribute of attributes) {
if (attribute.type === type && (value === undefined || value === attribute.value)) {
@@ -472,42 +534,104 @@ class Note extends Entity {
}
}
/**
* @return {Promise<Attribute>}
*/
async addAttribute(type, name, value = "") {
const attr = new Attribute({
noteId: this.noteId,
type: type,
name: name,
value: value
});
await attr.save();
this.invalidateAttributeCache();
return attr;
}
async addLabel(name, value = "") {
return await this.addAttribute(LABEL, name, value);
}
async addRelation(name, targetNoteId) {
return await this.addAttribute(RELATION, name, targetNoteId);
}
/**
* @param {string} name - label name
* @returns {Promise<boolean>} true if label exists (including inherited)
*/
async hasLabel(name) { return await this.hasAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise<boolean>} true if label exists (excluding inherited)
*/
async hasOwnedLabel(name) { return await this.hasOwnedAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<boolean>} true if relation exists (including inherited)
*/
async hasRelation(name) { return await this.hasAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise<boolean>} true if relation exists (excluding inherited)
*/
async hasOwnedRelation(name) { return await this.hasOwnedAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise<Attribute>} label if it exists, null otherwise
* @returns {Promise<Attribute|null>} label if it exists, null otherwise
*/
async getLabel(name) { return await this.getAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise<Attribute|null>} label if it exists, null otherwise
*/
async getOwnedLabel(name) { return await this.getOwnedAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<Attribute>} relation if it exists, null otherwise
* @returns {Promise<Attribute|null>} relation if it exists, null otherwise
*/
async getRelation(name) { return await this.getAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise<Attribute|null>} relation if it exists, null otherwise
*/
async getOwnedRelation(name) { return await this.getOwnedAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {Promise<string>} label value if label exists, null otherwise
* @returns {Promise<string|null>} label value if label exists, null otherwise
*/
async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); }
/**
* @param {string} name - label name
* @returns {Promise<string|null>} label value if label exists, null otherwise
*/
async getOwnedLabelValue(name) { return await this.getOwnedAttributeValue(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {Promise<string>} relation value if relation exists, null otherwise
* @returns {Promise<string|null>} relation value if relation exists, null otherwise
*/
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {Promise<string|null>} relation value if relation exists, null otherwise
*/
async getOwnedRelationValue(name) { return await this.getOwnedAttributeValue(RELATION, name); }
/**
* @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
@@ -518,6 +642,16 @@ class Note extends Entity {
return relation ? await repository.getNote(relation.value) : null;
}
/**
* @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
*/
async getOwnedRelationTarget(name) {
const relation = await this.getOwnedRelation(name);
return relation ? await repository.getNote(relation.value) : null;
}
/**
* Based on enabled, label is either set or removed.
*
@@ -671,7 +805,7 @@ class Note extends Entity {
WHERE noteId = ? AND
isDeleted = 0 AND
type = 'relation' AND
name IN ('internal-link', 'image-link', 'relation-map-link')`, [this.noteId]);
name IN ('internalLink', 'imageLink', 'relationMapLink')`, [this.noteId]);
}
/**
@@ -728,6 +862,36 @@ class Note extends Entity {
AND parent_notes.isDeleted = 0`, [this.noteId]);
}
/**
* @return {Promise<string[][]>} - array of notePaths (each represented by array of noteIds constituting the particular note path)
*/
async getAllNotePaths() {
if (this.noteId === 'root') {
return [['root']];
}
const notePaths = [];
for (const parentNote of await this.getParentNotes()) {
for (const parentPath of await parentNote.getAllNotePaths()) {
parentPath.push(this.noteId);
notePaths.push(parentPath);
}
}
return notePaths;
}
/**
* @param ancestorNoteId
* @return {Promise<boolean>} - true if ancestorNoteId occurs in at least one of the note's paths
*/
async isDescendantOfNote(ancestorNoteId) {
const notePaths = await this.getAllNotePaths();
return notePaths.some(path => path.includes(ancestorNoteId));
}
beforeSaving() {
if (!this.isDeleted) {
this.isDeleted = false;
@@ -767,7 +931,9 @@ class Note extends Entity {
delete pojo.isContentAvailable;
delete pojo.__attributeCache;
delete pojo.__ownedAttributeCache;
delete pojo.content;
/** zero references to contentHash, probably can be removed */
delete pojo.contentHash;
}
}
+62 -17
View File
@@ -30,6 +30,10 @@ import cssLoader from './services/css_loader.js';
import dateNoteService from './services/date_notes.js';
import sidebarService from './services/sidebar.js';
import importService from './services/import.js';
import keyboardActionService from "./services/keyboard_actions.js";
import splitService from "./services/split.js";
import optionService from "./services/options.js";
import noteContentRenderer from "./services/note_content_renderer.js";
window.glob.isDesktop = utils.isDesktop;
window.glob.isMobile = utils.isMobile;
@@ -38,6 +42,20 @@ window.glob.isMobile = utils.isMobile;
window.glob.getActiveNode = treeService.getActiveNode;
window.glob.getHeaders = server.getHeaders;
window.glob.showAddLinkDialog = () => import('./dialogs/add_link.js').then(d => d.showDialog());
window.glob.showIncludeNoteDialog = cb => import('./dialogs/include_note.js').then(d => d.showDialog(cb));
window.glob.loadIncludedNote = async (noteId, el) => {
const note = await treeCache.getNote(noteId);
if (note) {
$(el).empty().append($("<h3>").append(await linkService.createNoteLink(note.noteId, {
showTooltip: false
})));
const {renderedContent} = await noteContentRenderer.getRenderedContent(note);
$(el).append(renderedContent);
}
};
// this is required by CKEditor when uploading images
window.glob.noteChanged = noteDetailService.noteChanged;
window.glob.refreshTree = treeService.reload;
@@ -78,7 +96,7 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
};
for (const appCssNoteId of window.appCssNoteIds) {
cssLoader.requireCss(`/api/notes/download/${appCssNoteId}`);
cssLoader.requireCss(`api/notes/download/${appCssNoteId}`);
}
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/";
@@ -108,18 +126,8 @@ $("body").on("click", "a.external", function () {
});
if (utils.isElectron()) {
require('electron').ipcRenderer.on('create-day-sub-note', async function(event) {
const todayNote = await dateNoteService.getTodayNote();
const notePath = await treeService.getSomeNotePath(todayNote);
const node = await treeService.expandToNote(notePath);
await noteDetailService.openEmptyTab(false);
await treeService.createNote(node, todayNote.noteId, 'into', {
type: "text",
isProtected: node.data.isProtected
});
require('electron').ipcRenderer.on('globalShortcut', async function(event, actionName) {
keyboardActionService.triggerAction(actionName);
});
}
@@ -136,7 +144,7 @@ $noteTabContainer.on("click", ".export-note-button", function () {
$noteTabContainer.on("click", ".import-files-button",
() => import('./dialogs/import.js').then(d => d.showDialog(treeService.getActiveNode())));
$noteTabContainer.on("click", ".print-note-button", async function () {
async function printActiveNote() {
if ($(this).hasClass("disabled")) {
return;
}
@@ -151,10 +159,17 @@ $noteTabContainer.on("click", ".print-note-button", async function () {
$tabContext.$tabContent.find('.note-detail-component:visible').printThis({
header: $("<h2>").text($tabContext.note && $tabContext.note.title).prop('outerHTML') ,
importCSS: false,
loadCSS: "libraries/codemirror/codemirror.css",
loadCSS: [
"libraries/codemirror/codemirror.css",
"libraries/ckeditor/ckeditor-content.css"
],
debug: true
});
});
}
keyboardActionService.setGlobalActionHandler("PrintActiveNote", printActiveNote);
$noteTabContainer.on("click", ".print-note-button", printActiveNote);
$('[data-toggle="tooltip"]').tooltip({
html: true
@@ -181,4 +196,34 @@ noteAutocompleteService.init();
if (utils.isElectron()) {
import("./services/spell_check.js").then(spellCheckService => spellCheckService.initSpellCheck());
}
}
optionService.waitForOptions().then(options => {
if (utils.isElectron() && !options.is('nativeTitleBarVisible')) {
$("#title-bar-buttons").show();
$("#minimize-btn").on('click', () => {
$("#minimize-btn").trigger('blur');
const {remote} = require('electron');
remote.BrowserWindow.getFocusedWindow().minimize();
});
$("#maximize-btn").on('click', () => {
$("#maximize-btn").trigger('blur');
const {remote} = require('electron');
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
if (focusedWindow.isMaximized()) {
focusedWindow.unmaximize();
} else {
focusedWindow.maximize();
}
});
$("#close-btn").on('click', () => {
$("#close-btn").trigger('blur');
const {remote} = require('electron');
remote.BrowserWindow.getFocusedWindow().close();
});
}
});
@@ -7,6 +7,7 @@ import utils from "../services/utils.js";
import linkService from "../services/link.js";
import libraryLoader from "../services/library_loader.js";
import noteAutocompleteService from "../services/note_autocomplete.js";
import treeService from "../services/tree.js";
const $dialog = $("#attributes-dialog");
const $saveAttributesButton = $("#save-attributes-button");
@@ -175,6 +176,7 @@ function AttributesModel() {
ctx.attributes.refreshAttributes();
noteDetailService.reload();
treeService.reload();
};
function addLastEmptyRow() {
@@ -0,0 +1,32 @@
import server from "../services/server.js";
import utils from "../services/utils.js";
const $dialog = $("#backend-log-dialog");
const $backendLogTextArea = $("#backend-log-textarea");
const $refreshBackendLog = $("#refresh-backend-log-button");
export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$dialog.modal();
load();
}
function scrollToBottom() {
$backendLogTextArea.scrollTop($backendLogTextArea[0].scrollHeight);
}
async function load() {
const backendLog = await server.get('backend-log');
$backendLogTextArea.text(backendLog);
scrollToBottom();
}
$refreshBackendLog.on('click', load);
$dialog.on('shown.bs.modal', scrollToBottom);
@@ -17,11 +17,21 @@ export async function showDialog(node) {
glob.activeDialog = $dialog;
$dialog.modal();
branchId = node.data.branchId;
const branch = treeCache.getBranch(branchId);
if (branch.noteId === 'root') {
return;
}
const parentNote = await treeCache.getNote(branch.parentNoteId);
if (parentNote.type === 'search') {
return;
}
$dialog.modal();
$treePrefixInput.val(branch.prefix);
const noteTitle = await treeUtils.getNoteTitle(node.data.noteId);
+1 -1
View File
@@ -74,7 +74,7 @@ $form.on('submit', () => {
function exportBranch(branchId, type, format, version) {
taskId = utils.randomString(10);
const url = utils.getHost() + `/api/notes/${branchId}/export/${type}/${format}/${version}/${taskId}`;
const url = utils.getUrlForDownload(`api/notes/${branchId}/export/${type}/${format}/${version}/${taskId}`);
utils.download(url);
}
@@ -0,0 +1,40 @@
import treeUtils from '../services/tree_utils.js';
import noteAutocompleteService from '../services/note_autocomplete.js';
import utils from "../services/utils.js";
const $dialog = $("#include-note-dialog");
const $form = $("#include-note-form");
const $autoComplete = $("#include-note-autocomplete");
let callback = null;
export async function showDialog(cb) {
callback = cb;
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$autoComplete.val('');
$dialog.modal();
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true });
noteAutocompleteService.showRecentNotes($autoComplete);
}
$form.on('submit', () => {
const notePath = $autoComplete.getSelectedPath();
if (notePath) {
$dialog.modal('hide');
if (callback) {
callback(treeUtils.getNoteIdFromNotePath(notePath));
}
}
else {
console.error("No noteId to include.");
}
return false;
});
@@ -26,6 +26,10 @@ async function convertMarkdownToHtml(text) {
}
export async function importMarkdownInline() {
if (noteDetailService.getActiveTabNoteType() !== 'text') {
return;
}
if (utils.isElectron()) {
const {clipboard} = require('electron');
const text = clipboard.readText();
@@ -102,7 +102,9 @@ async function setContentPane() {
const $downloadButton = $('<button class="btn btn-sm btn-primary" type="button">Download</button>');
$downloadButton.on('click', () => {
utils.download(utils.getHost() + `/api/notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}/download`);
const url = utils.getUrlForDownload(`api/notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}/download`);
utils.download(url);
});
$titleButtons.append($downloadButton);
@@ -22,6 +22,7 @@ export async function showDialog() {
import('./options/other.js'),
import('./options/sidebar.js'),
import('./options/sync.js'),
import('./options/keyboard_shortcuts.js'),
]))
.map(m => new m.default)
.forEach(tab => {
@@ -3,19 +3,23 @@ import toastService from "../../services/toast.js";
const TPL = `
<h4 style="margin-top: 0;">Sync</h4>
<button id="force-full-sync-button" class="btn btn-secondary">Force full sync</button>
<button id="force-full-sync-button" class="btn">Force full sync</button>
<br/>
<br/>
<button id="fill-sync-rows-button" class="btn btn-secondary">Fill sync rows</button>
<button id="fill-sync-rows-button" class="btn">Fill sync rows</button>
<br/>
<br/>
<h4>Consistency checks</h4>
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button><br/><br/>
<h4>Debugging</h4>
<button id="anonymize-button" class="btn btn-secondary">Save anonymized database</button><br/><br/>
<button id="anonymize-button" class="btn">Save anonymized database</button><br/><br/>
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
@@ -24,7 +28,7 @@ const TPL = `
<p>This will rebuild database which will typically result in smaller database file. No data will be actually changed.</p>
<button id="vacuum-database-button" class="btn btn-secondary">Vacuum database</button>`;
<button id="vacuum-database-button" class="btn">Vacuum database</button>`;
export default class AdvancedOptions {
constructor() {
@@ -33,9 +37,8 @@ export default class AdvancedOptions {
this.$forceFullSyncButton = $("#force-full-sync-button");
this.$fillSyncRowsButton = $("#fill-sync-rows-button");
this.$anonymizeButton = $("#anonymize-button");
this.$cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button");
this.$cleanupUnusedImagesButton = $("#cleanup-unused-images-button");
this.$vacuumDatabaseButton = $("#vacuum-database-button");
this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button");
this.$forceFullSyncButton.on('click', async () => {
await server.post('sync/force-full-sync');
@@ -55,26 +58,16 @@ export default class AdvancedOptions {
toastService.showMessage("Created anonymized database");
});
this.$cleanupSoftDeletedButton.on('click', async () => {
if (confirm("Do you really want to clean up soft-deleted items?")) {
await server.post('cleanup/cleanup-soft-deleted-items');
toastService.showMessage("Soft deleted items have been cleaned up");
}
});
this.$cleanupUnusedImagesButton.on('click', async () => {
if (confirm("Do you really want to clean up unused images?")) {
await server.post('cleanup/cleanup-unused-images');
toastService.showMessage("Unused images have been cleaned up");
}
});
this.$vacuumDatabaseButton.on('click', async () => {
await server.post('cleanup/vacuum-database');
toastService.showMessage("Database has been vacuumed");
});
this.$findAndFixConsistencyIssuesButton.on('click', async () => {
await server.post('cleanup/find-and-fix-consistency-issues');
toastService.showMessage("Consistency issues should be fixed.");
});
}
}
@@ -19,12 +19,13 @@ const TPL = `
<input type="number" class="form-control" id="zoom-factor-select" min="0.3" max="2.0" step="0.1"/>
</div>
<div class="col-4">
<label for="one-tab-display-select">If there's only one tab, then...</label>
<select class="form-control" id="one-tab-display-select">
<option value="show">show the tab bar</option>
<option value="hide">hide the tab bar</option>
<label for="native-title-bar-select">Native title bar (requires app restart)</label>
<select class="form-control" id="native-title-bar-select">
<option value="show">enabled</option>
<option value="hide">disabled</option>
</select>
</div>
</div>
@@ -69,37 +70,6 @@ const TPL = `
</div>
<p>Note that tree and detail font sizing is relative to the main font size setting.</p>
<h4>Left pane sizing</h4>
<div class="form-group row">
<div class="col-6">
<label for="left-pane-min-width">Left pane minimum width (in pixels)</label>
<div class="input-group">
<input type="number" class="form-control" id="left-pane-min-width" min="100" max="2000" step="1"/>
<div class="input-group-append">
<span class="input-group-text">px</span>
</div>
</div>
</div>
<div class="col-6">
<label for="left-pane-min-width">Left pane width percent of window size</label>
<div class="input-group">
<input type="number" class="form-control" id="left-pane-width-percent" min="0" max="99" step="1"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
<p>Left pane width is calculated from the percent of window size, if this is smaller than minimum width, then minimum width is used. If you want to have fixed width left pane, set minimum width to the desired width and set percent to 0.</p>
</form>`;
export default class ApperanceOptions {
@@ -108,9 +78,7 @@ export default class ApperanceOptions {
this.$themeSelect = $("#theme-select");
this.$zoomFactorSelect = $("#zoom-factor-select");
this.$oneTabDisplaySelect = $("#one-tab-display-select");
this.$leftPaneMinWidth = $("#left-pane-min-width");
this.$leftPaneWidthPercent = $("#left-pane-width-percent");
this.$nativeTitleBarSelect = $("#native-title-bar-select");
this.$mainFontSize = $("#main-font-size");
this.$treeFontSize = $("#tree-font-size");
this.$detailFontSize = $("#detail-font-size");
@@ -131,7 +99,7 @@ export default class ApperanceOptions {
if (noteId) {
// make sure the CSS is loaded
// if the CSS has been loaded and then updated then the changes won't take effect though
cssLoader.requireCss(`/api/notes/download/${noteId}`);
cssLoader.requireCss(`api/notes/download/${noteId}`);
}
this.$body.addClass("theme-" + newTheme);
@@ -141,23 +109,10 @@ export default class ApperanceOptions {
this.$zoomFactorSelect.on('change', () => { zoomService.setZoomFactorAndSave(this.$zoomFactorSelect.val()); });
this.$oneTabDisplaySelect.on('change', () => {
const hideTabRowForOneTab = this.$oneTabDisplaySelect.val() === 'hide' ? 'true' : 'false';
this.$nativeTitleBarSelect.on('change', () => {
const nativeTitleBarVisible = this.$nativeTitleBarSelect.val() === 'show' ? 'true' : 'false';
server.put('options/hideTabRowForOneTab/' + hideTabRowForOneTab)
.then(optionsService.reloadOptions);
});
this.$leftPaneMinWidth.on('change', async () => {
await server.put('options/leftPaneMinWidth/' + this.$leftPaneMinWidth.val());
this.resizeLeftPanel();
});
this.$leftPaneWidthPercent.on('change', async () => {
await server.put('options/leftPaneWidthPercent/' + this.$leftPaneWidthPercent.val());
this.resizeLeftPanel();
server.put('options/nativeTitleBarVisible/' + nativeTitleBarVisible);
});
this.$mainFontSize.on('change', async () => {
@@ -204,24 +159,13 @@ export default class ApperanceOptions {
this.$zoomFactorSelect.prop('disabled', true);
}
this.$oneTabDisplaySelect.val(options.hideTabRowForOneTab === 'true' ? 'hide' : 'show');
this.$leftPaneMinWidth.val(options.leftPaneMinWidth);
this.$leftPaneWidthPercent.val(options.leftPaneWidthPercent);
this.$nativeTitleBarSelect.val(options.nativeTitleBarVisible === 'true' ? 'show' : 'hide');
this.$mainFontSize.val(options.mainFontSize);
this.$treeFontSize.val(options.treeFontSize);
this.$detailFontSize.val(options.detailFontSize);
}
resizeLeftPanel() {
const leftPanePercent = parseInt(this.$leftPaneWidthPercent.val());
const rightPanePercent = 100 - leftPanePercent;
const leftPaneMinWidth = this.$leftPaneMinWidth.val();
this.$container.css("grid-template-columns", `minmax(${leftPaneMinWidth}px, ${leftPanePercent}fr) ${rightPanePercent}fr`);
}
applyFontSizes() {
this.$body.get(0).style.setProperty("--main-font-size", this.$mainFontSize.val() + "%");
this.$body.get(0).style.setProperty("--tree-font-size", this.$treeFontSize.val() + "%");
@@ -0,0 +1,141 @@
import server from "../../services/server.js";
import utils from "../../services/utils.js";
const TPL = `
<h4>Keyboard shortcuts</h4>
<p>Multiple shortcuts for the same action can be separated by comma.</p>
<div class="form-group">
<input type="text" class="form-control" id="keyboard-shortcut-filter" placeholder="Type text to filter shortcuts...">
</div>
<div style="overflow: auto; height: 500px;">
<table id="keyboard-shortcut-table" cellpadding="10">
<thead>
<tr>
<th>Action name</th>
<th>Shortcuts</th>
<th>Default shortcuts</th>
<th>Description</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div style="display: flex; justify-content: space-between">
<button class="btn btn-primary" id="options-keyboard-shortcuts-reload-app">Reload app to apply changes</button>
<button class="btn" id="options-keyboard-shortcuts-set-all-to-default">Set all shortcuts to the default</button>
</div>
`;
let globActions;
export default class KeyboardShortcutsOptions {
constructor() {
$("#options-keyboard-shortcuts").html(TPL);
$("#options-keyboard-shortcuts-reload-app").on("click", () => utils.reloadApp());
const $table = $("#keyboard-shortcut-table tbody");
server.get('keyboard-actions').then(actions => {
globActions = actions;
for (const action of actions) {
const $tr = $("<tr>");
if (action.separator) {
$tr.append(
$('<td colspan="4">')
.attr("style","background-color: var(--accented-background-color); font-weight: bold;")
.text(action.separator)
)
}
else {
$tr.append($("<td>").text(action.actionName))
.append($("<td>").append(
$(`<input type="text" class="form-control">`)
.val(action.effectiveShortcuts.join(", "))
.attr('data-keyboard-action-name', action.actionName)
.attr('data-default-keyboard-shortcuts', action.defaultShortcuts.join(", "))
)
)
.append($("<td>").text(action.defaultShortcuts.join(", ")))
.append($("<td>").text(action.description));
}
$table.append($tr);
}
});
$table.on('change', 'input.form-control', e => {
const $input = $(e.target);
const actionName = $input.attr('data-keyboard-action-name');
const shortcuts = $input.val()
.replace('+,', "+Comma")
.split(",")
.map(shortcut => shortcut.replace("+Comma", "+,"))
.filter(shortcut => !!shortcut);
const opts = {};
opts['keyboardShortcuts' + actionName] = JSON.stringify(shortcuts);
server.put('options', opts);
});
$("#options-keyboard-shortcuts-set-all-to-default").on('click', async () => {
const confirmDialog = await import('../confirm.js');
if (!await confirmDialog.confirm("Do you really want to reset all keyboard shortcuts to the default?")) {
return;
}
$table.find('input.form-control').each(function() {
const defaultShortcuts = $(this).attr('data-default-keyboard-shortcuts');
if ($(this).val() !== defaultShortcuts) {
$(this)
.val(defaultShortcuts)
.trigger('change');
}
});
});
const $filter = $("#keyboard-shortcut-filter");
$filter.on('keyup', () => {
const filter = $filter.val().trim().toLowerCase();
$table.find("tr").each((i, el) => {
if (!filter) {
$(el).show();
return;
}
const actionName = $(el).find('input').attr('data-keyboard-action-name');
if (!actionName) {
$(el).hide();
return;
}
const action = globActions.find(act => act.actionName === actionName);
if (!action) {
$(el).hide();
return;
}
$(el).toggle(!!( // !! to avoid toggle overloads with different behavior
action.actionName.toLowerCase().includes(filter)
|| action.defaultShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter))
|| action.effectiveShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter))
|| (action.description && action.description.toLowerCase().includes(filter))
));
});
});
}
}
@@ -17,7 +17,7 @@ const TPL = `
<div class="form-group">
<label for="spell-check-language-code">Language code</label>
<input type="email" class="form-control" id="spell-check-language-code" placeholder="for example &quot;en-US&quot;, &quot;de-AT&quot;">
<input type="text" class="form-control" id="spell-check-language-code" placeholder="for example &quot;en-US&quot;, &quot;de-AT&quot;">
</div>
<p>Changes to the spell check options will take effect after application restart.</p>
@@ -132,4 +132,4 @@ export default class ProtectedSessionOptions {
this.$imageMaxWidthHeight.val(options['imageMaxWidthHeight']);
this.$imageJpegQuality.val(options['imageJpegQuality']);
}
}
}
@@ -3,45 +3,6 @@ import server from "../../services/server.js";
import optionsService from "../../services/options.js";
const TPL = `
<h4>Show sidebar in new tab</h4>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="show-sidebar-in-new-tab">
<label class="form-check-label" for="show-sidebar-in-new-tab">Show sidebar in new tab</label>
</div>
<br>
<h4>Sidebar sizing</h4>
<div class="form-group row">
<div class="col-6">
<label for="sidebar-min-width">Sidebar minimum width (in pixels)</label>
<div class="input-group">
<input type="number" class="form-control" id="sidebar-min-width" min="100" max="2000" step="1"/>
<div class="input-group-append">
<span class="input-group-text">px</span>
</div>
</div>
</div>
<div class="col-6">
<label for="left-pane-min-width">Sidebar width percent of the detail pane</label>
<div class="input-group">
<input type="number" class="form-control" id="sidebar-width-percent" min="0" max="70" step="1"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
<p>Sidebar width is calculated from the percent of the detail pane, if this is smaller than minimum width, then minimum width is used. If you want to have fixed width sidebar, set minimum width to the desired width and set percent to 0.</p>
<h4>Widgets</h4>
<div id="widgets-configuration" class="row">
@@ -58,48 +19,15 @@ export default class SidebarOptions {
constructor() {
$("#options-sidebar").html(TPL);
this.$sidebarMinWidth = $("#sidebar-min-width");
this.$sidebarWidthPercent = $("#sidebar-width-percent");
this.$showSidebarInNewTab = $("#show-sidebar-in-new-tab");
this.$widgetsConfiguration = $("#widgets-configuration");
this.$widgetsEnabled = $("#widgets-enabled");
this.$widgetsDisabled = $("#widgets-disabled");
this.$sidebarMinWidth.on('change', async () => {
await server.put('options/sidebarMinWidth/' + this.$sidebarMinWidth.val());
this.resizeSidebar();
});
this.$sidebarWidthPercent.on('change', async () => {
await server.put('options/sidebarWidthPercent/' + this.$sidebarWidthPercent.val());
this.resizeSidebar();
});
this.$showSidebarInNewTab.on('change', async () => {
const flag = this.$showSidebarInNewTab.is(":checked") ? 'true' : 'false';
await server.put('options/showSidebarInNewTab/' + flag);
optionsService.reloadOptions();
});
}
async optionsLoaded(options) {
this.$widgetsEnabled.empty();
this.$widgetsDisabled.empty();
this.$sidebarMinWidth.val(options.sidebarMinWidth);
this.$sidebarWidthPercent.val(options.sidebarWidthPercent);
if (parseInt(options.showSidebarInNewTab)) {
this.$showSidebarInNewTab.attr("checked", "checked");
}
else {
this.$showSidebarInNewTab.removeAttr("checked");
}
const widgets = [
{name: 'attributes', title: 'Attributes'},
{name: 'linkMap', title: 'Link map'},
@@ -188,19 +116,4 @@ export default class SidebarOptions {
return null;
}
}
resizeSidebar() {
const sidebarWidthPercent = parseInt(this.$sidebarWidthPercent.val());
const sidebarMinWidth = this.$sidebarMinWidth.val();
// need to find them dynamically since they change
const $sidebar = $(".note-detail-sidebar");
const $content = $(".note-detail-content");
$sidebar.css("width", sidebarWidthPercent + '%');
$sidebar.css("min-width", sidebarMinWidth + 'px');
$content.css("width", (100 - sidebarWidthPercent) + '%');
}
}
@@ -25,7 +25,7 @@ const TPL = `
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-secondary" type="button" data-help-page="Synchronization">Help</button>
<button class="btn" type="button" data-help-page="Synchronization">Help</button>
</div>
</form>
@@ -35,7 +35,7 @@ const TPL = `
<p>This will test connection and handshake to the sync server. If sync server isn't initialized, this will set it up to sync with local document.</p>
<button id="test-sync-button" class="btn btn-secondary">Test sync</button>`;
<button id="test-sync-button" class="btn">Test sync</button>`;
export default class SyncOptions {
constructor() {
@@ -44,7 +44,10 @@ export async function showDialog() {
const note = await treeCache.getNote(change.noteId);
const notePath = await treeService.getSomeNotePath(note);
noteLink = await linkService.createNoteLinkWithPath(notePath, change.title);
noteLink = await linkService.createNoteLink(notePath, {
title: change.title,
showNotePath: true
});
}
changesListEl.append($('<li>')
+40 -26
View File
@@ -6,9 +6,8 @@ import utils from "../services/utils.js";
const $dialog = $("#sql-console-dialog");
const $query = $('#sql-console-query');
const $executeButton = $('#sql-console-execute');
const $resultHead = $('#sql-console-results thead');
const $resultBody = $('#sql-console-results tbody');
const $tables = $("#sql-console-tables");
const $tableSchemas = $("#sql-console-table-schemas");
const $resultContainer = $("#result-container");
let codeEditor;
@@ -19,7 +18,7 @@ export async function showDialog() {
glob.activeDialog = $dialog;
await showTables();
await showTableSchemas();
$dialog.modal();
}
@@ -45,6 +44,10 @@ async function initEditor() {
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
codeEditor.setValue(`SELECT title, isProtected, type, mime FROM notes WHERE noteId = 'root';
---
SELECT noteId, parentNoteId, notePosition, prefix FROM branches WHERE branchId = 'root';`);
}
codeEditor.focus();
@@ -70,55 +73,66 @@ async function execute() {
toastService.showMessage("Query was executed successfully.");
}
const rows = result.rows;
const results = result.results;
$resultHead.empty();
$resultBody.empty();
$resultContainer.empty();
for (const rows of results) {
if (rows.length === 0) {
continue;
}
const $table = $('<table class="table table-striped">');
$resultContainer.append($table);
if (rows.length > 0) {
const result = rows[0];
const rowEl = $("<tr>");
const $row = $("<tr>");
for (const key in result) {
rowEl.append($("<th>").html(key));
$row.append($("<th>").html(key));
}
$resultHead.append(rowEl);
}
$table.append($row);
for (const result of rows) {
const rowEl = $("<tr>");
for (const result of rows) {
const $row = $("<tr>");
for (const key in result) {
rowEl.append($("<td>").html(result[key]));
for (const key in result) {
$row.append($("<td>").html(result[key]));
}
$table.append($row);
}
$resultBody.append(rowEl);
}
}
async function showTables() {
async function showTableSchemas() {
const tables = await server.get('sql/schema');
$tables.empty();
$tableSchemas.empty();
for (const table of tables) {
const $tableLink = $('<button class="btn">').text(table.name);
const $columns = $("<table>");
const $columns = $("<ul>");
for (const column of table.columns) {
$columns.append(
$("<tr>")
.append($("<td>").text(column.name))
.append($("<td>").text(column.type))
$("<li>")
.append($("<span>").text(column.name))
.append($("<span>").text(column.type))
);
}
$tables.append($tableLink).append(" ");
$tableSchemas.append($tableLink).append(" ");
$tableLink
.tooltip({html: true, title: $columns.html()})
.tooltip({
html: true,
placement: 'bottom',
boundary: 'window',
title: $columns[0].outerHTML
})
.on('click', () => codeEditor.setValue("SELECT * FROM " + table.name + " LIMIT 100"));
}
}
+1 -2
View File
@@ -6,7 +6,6 @@ class Branch {
this.branchId = row.branchId;
/** @param {string} */
this.noteId = row.noteId;
this.note = null;
/** @param {string} */
this.parentNoteId = row.parentNoteId;
/** @param {int} */
@@ -19,7 +18,7 @@ class Branch {
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
return this.treeCache.getNote(this.noteId);
}
/** @returns {boolean} true if it's top level, meaning its parent is root note */
+24 -15
View File
@@ -35,6 +35,8 @@ class NoteShort {
this.archived = row.archived;
/** @param {string} */
this.cssClass = row.cssClass;
/** @param {string} */
this.iconClass = row.iconClass;
/** @type {string[]} */
this.parents = [];
@@ -152,20 +154,27 @@ class NoteShort {
}
/**
* @param {string} [name] - attribute name to filter
* @returns {Promise<Attribute[]>}
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {Promise<Attribute[]>} all note's attributes, including inherited ones
*/
async getAttributes(name) {
if (!this.attributeCache) {
this.attributeCache = (await server.get('notes/' + this.noteId + '/attributes'))
async getAttributes(type, name) {
if (!this.__attributeCache) {
this.__attributeCache = (await server.get('notes/' + this.noteId + '/attributes'))
.map(attrRow => new Attribute(this.treeCache, attrRow));
}
if (name) {
return this.attributeCache.filter(attr => attr.name === name);
if (type && name) {
return this.__attributeCache.filter(attr => attr.type === type && attr.name === name);
}
else if (type) {
return this.__attributeCache.filter(attr => attr.type === type);
}
else if (name) {
return this.__attributeCache.filter(attr => attr.name === name);
}
else {
return this.attributeCache;
return this.__attributeCache.slice();
}
}
@@ -174,7 +183,7 @@ class NoteShort {
* @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones
*/
async getLabels(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL);
return await this.getAttributes(LABEL, name);
}
/**
@@ -182,7 +191,7 @@ class NoteShort {
* @returns {Promise<Attribute[]>} all note's label definitions, including inherited ones
*/
async getLabelDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === LABEL_DEFINITION);
return await this.getAttributes(LABEL_DEFINITION, name);
}
/**
@@ -190,7 +199,7 @@ class NoteShort {
* @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones
*/
async getRelations(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION);
return await this.getAttributes(RELATION, name);
}
/**
@@ -198,7 +207,7 @@ class NoteShort {
* @returns {Promise<Attribute[]>} all note's relation definitions including inherited ones
*/
async getRelationDefinitions(name) {
return (await this.getAttributes(name)).filter(attr => attr.type === RELATION_DEFINITION);
return await this.getAttributes(RELATION_DEFINITION, name);
}
/**
@@ -297,8 +306,8 @@ class NoteShort {
* Clear note's attributes cache to force fresh reload for next attribute request.
* Cache is note instance scoped.
*/
invalidateAttributeCache() {
this.attributeCache = null;
invalidate__attributeCache() {
this.__attributeCache = null;
}
/**
@@ -319,7 +328,7 @@ class NoteShort {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.archived;
delete dto.attributeCache;
delete dto.__attributeCache;
return dto;
}
+42 -28
View File
@@ -4,83 +4,97 @@ import cloningService from "./cloning.js";
import toastService from "./toast.js";
import hoistedNoteService from "./hoisted_note.js";
let clipboardIds = [];
/*
* Clipboard contains node keys which are not stable. If a (part of the) tree is reloaded,
* node keys in the clipboard might not exist anymore. Code here should then be ready to deal
* with this.
*/
let clipboardNodeKeys = [];
let clipboardMode = null;
async function pasteAfter(node) {
async function pasteAfter(afterNode) {
if (isClipboardEmpty()) {
return;
}
if (clipboardMode === 'cut') {
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
const nodes = clipboardNodeKeys.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
await treeChangesService.moveAfterNode(nodes, node);
await treeChangesService.moveAfterNode(nodes, afterNode);
clipboardIds = [];
clipboardNodeKeys = [];
clipboardMode = null;
}
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloningService.cloneNoteAfter(noteId, node.data.branchId);
for (const nodeKey of clipboardNodeKeys) {
const clipNode = treeUtils.getNodeByKey(nodeKey);
await cloningService.cloneNoteAfter(clipNode.data.noteId, afterNode.data.branchId);
}
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
}
else if (clipboardIds.length === 0) {
// just do nothing
}
else {
toastService.throwError("Unrecognized clipboard mode=" + clipboardMode);
}
}
async function pasteInto(node) {
async function pasteInto(parentNode) {
if (isClipboardEmpty()) {
return;
}
if (clipboardMode === 'cut') {
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
const nodes = clipboardNodeKeys.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
await treeChangesService.moveToNode(nodes, node);
await treeChangesService.moveToNode(nodes, parentNode);
await node.setExpanded(true);
await parentNode.setExpanded(true);
clipboardIds = [];
clipboardNodeKeys = [];
clipboardMode = null;
}
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloningService.cloneNoteTo(noteId, node.data.noteId);
for (const nodeKey of clipboardNodeKeys) {
const clipNode = treeUtils.getNodeByKey(nodeKey);
await cloningService.cloneNoteTo(clipNode.data.noteId, parentNode.data.noteId);
}
await node.setExpanded(true);
await parentNode.setExpanded(true);
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
}
else if (clipboardIds.length === 0) {
// just do nothing
}
else {
toastService.throwError("Unrecognized clipboard mode=" + mode);
}
}
function copy(nodes) {
clipboardIds = nodes.map(node => node.data.noteId);
clipboardNodeKeys = nodes.map(node => node.key);
clipboardMode = 'copy';
toastService.showMessage("Note(s) have been copied into clipboard.");
}
function cut(nodes) {
clipboardIds = nodes
clipboardNodeKeys = nodes
.filter(node => node.data.noteId !== hoistedNoteService.getHoistedNoteNoPromise())
.filter(node => node.getParent().data.noteType !== 'search')
.map(node => node.data.noteId);
.map(node => node.key);
if (clipboardIds.length > 0) {
if (clipboardNodeKeys.length > 0) {
clipboardMode = 'cut';
toastService.showMessage("Note(s) have been cut into clipboard.");
}
}
function isEmpty() {
return clipboardIds.length === 0;
function isClipboardEmpty() {
clipboardNodeKeys = clipboardNodeKeys.filter(key => !!treeUtils.getNodeByKey(key));
return clipboardNodeKeys.length === 0;
}
export default {
@@ -88,5 +102,5 @@ export default {
pasteInto,
cut,
copy,
isEmpty
isClipboardEmpty
}
@@ -1,3 +1,4 @@
import keyboardActionService from './keyboard_actions.js';
const $contextMenuContainer = $("#context-menu-container");
let dateContextMenuOpenedMs = 0;
@@ -69,6 +70,8 @@ async function initContextMenu(event, contextMenu) {
addItems($contextMenuContainer, await contextMenu.getContextMenuItems());
keyboardActionService.updateDisplayedShortcuts($contextMenuContainer);
// code below tries to detect when dropdown would overflow from page
// in such case we'll position it above click coordinates so it will fit into client
const clickPosition = event.pageY;
+135 -53
View File
@@ -4,6 +4,12 @@ import zoomService from "./zoom.js";
import protectedSessionService from "./protected_session.js";
import searchNotesService from "./search_notes.js";
import treeService from "./tree.js";
import dateNoteService from "./date_notes.js";
import noteDetailService from "./note_detail.js";
import keyboardActionService from "./keyboard_actions.js";
import hoistedNoteService from "./hoisted_note.js";
import treeCache from "./tree_cache.js";
import server from "./server.js";
const NOTE_REVISIONS = "../dialogs/note_revisions.js";
const OPTIONS = "../dialogs/options.js";
@@ -12,6 +18,7 @@ const JUMP_TO_NOTE = "../dialogs/jump_to_note.js";
const NOTE_SOURCE = "../dialogs/note_source.js";
const RECENT_CHANGES = "../dialogs/recent_changes.js";
const SQL_CONSOLE = "../dialogs/sql_console.js";
const BACKEND_LOG = "../dialogs/backend_log.js";
const ATTRIBUTES = "../dialogs/attributes.js";
const HELP = "../dialogs/help.js";
const NOTE_INFO = "../dialogs/note_info.js";
@@ -26,92 +33,123 @@ function registerEntrypoints() {
jQuery.hotkeys.options.filterContentEditable = false;
jQuery.hotkeys.options.filterTextInputs = false;
utils.bindGlobalShortcut('ctrl+l', () => import(ADD_LINK).then(d => d.showDialog()));
utils.bindGlobalShortcut('ctrl+shift+l', () => import(ADD_LINK).then(d => d.showDialogForClone()));
keyboardActionService.setGlobalActionHandler("AddLinkToText", () => import(ADD_LINK).then(d => d.showDialog()));
$("#jump-to-note-dialog-button").on('click', () => import(JUMP_TO_NOTE).then(d => d.showDialog()));
utils.bindGlobalShortcut('ctrl+j', () => import(JUMP_TO_NOTE).then(d => d.showDialog()));
const showJumpToNoteDialog = () => import(JUMP_TO_NOTE).then(d => d.showDialog());
$("#jump-to-note-dialog-button").on('click', showJumpToNoteDialog);
keyboardActionService.setGlobalActionHandler("JumpToNote", showJumpToNoteDialog);
$("#recent-changes-button").on('click', () => import(RECENT_CHANGES).then(d => d.showDialog()));
const showRecentChanges = () => import(RECENT_CHANGES).then(d => d.showDialog());
$("#recent-changes-button").on('click', showRecentChanges);
keyboardActionService.setGlobalActionHandler("ShowRecentChanges", showRecentChanges);
$("#enter-protected-session-button").on('click', protectedSessionService.enterProtectedSession);
$("#leave-protected-session-button").on('click', protectedSessionService.leaveProtectedSession);
$("#toggle-search-button").on('click', searchNotesService.toggleSearch);
utils.bindGlobalShortcut('ctrl+s', searchNotesService.toggleSearch);
keyboardActionService.setGlobalActionHandler('SearchNotes', searchNotesService.toggleSearch);
const $noteTabContainer = $("#note-tab-container");
$noteTabContainer.on("click", ".show-attributes-button", () => import(ATTRIBUTES).then(d => d.showDialog()));
utils.bindGlobalShortcut('alt+a', () => import(ATTRIBUTES).then(d => d.showDialog()));
$noteTabContainer.on("click", ".show-note-info-button", () => import(NOTE_INFO).then(d => d.showDialog()));
const showAttributesDialog = () => import(ATTRIBUTES).then(d => d.showDialog());
$noteTabContainer.on("click", ".show-attributes-button", showAttributesDialog);
keyboardActionService.setGlobalActionHandler("ShowAttributes", showAttributesDialog);
$noteTabContainer.on("click", ".show-note-revisions-button", function() {
const showNoteInfoDialog = () => import(NOTE_INFO).then(d => d.showDialog());
$noteTabContainer.on("click", ".show-note-info-button", showNoteInfoDialog);
keyboardActionService.setGlobalActionHandler("ShowNoteInfo", showNoteInfoDialog);
const showNoteRevisionsDialog = function() {
if ($(this).hasClass("disabled")) {
return;
}
import(NOTE_REVISIONS).then(d => d.showCurrentNoteRevisions());
});
};
$noteTabContainer.on("click", ".show-source-button", function() {
$noteTabContainer.on("click", ".show-note-revisions-button", showNoteRevisionsDialog);
keyboardActionService.setGlobalActionHandler("ShowNoteRevisions", showNoteRevisionsDialog);
const showNoteSourceDialog = function() {
if ($(this).hasClass("disabled")) {
return;
}
import(NOTE_SOURCE).then(d => d.showDialog());
});
};
$noteTabContainer.on("click", ".show-link-map-button", function() {
import(LINK_MAP).then(d => d.showDialog());
});
$noteTabContainer.on("click", ".show-source-button", showNoteSourceDialog);
keyboardActionService.setGlobalActionHandler("ShowNoteSource", showNoteSourceDialog);
$("#options-button").on('click', () => import(OPTIONS).then(d => d.showDialog()));
const showLinkMapDialog = () => import(LINK_MAP).then(d => d.showDialog());
$noteTabContainer.on("click", ".show-link-map-button", showLinkMapDialog);
keyboardActionService.setGlobalActionHandler("ShowLinkMap", showLinkMapDialog);
$("#show-help-button").on('click', () => import(HELP).then(d => d.showDialog()));
utils.bindGlobalShortcut('f1', () => import(HELP).then(d => d.showDialog()));
const showOptionsDialog = () => import(OPTIONS).then(d => d.showDialog());
$("#options-button").on('click', showOptionsDialog);
keyboardActionService.setGlobalActionHandler("ShowOptions", showOptionsDialog);
$("#open-sql-console-button").on('click', () => import(SQL_CONSOLE).then(d => d.showDialog()));
utils.bindGlobalShortcut('alt+o', () => import(SQL_CONSOLE).then(d => d.showDialog()));
const showHelpDialog = () => import(HELP).then(d => d.showDialog());
$("#show-help-button").on('click', showHelpDialog);
keyboardActionService.setGlobalActionHandler("ShowHelp", showHelpDialog);
const showSqlConsoleDialog = () => import(SQL_CONSOLE).then(d => d.showDialog());
$("#open-sql-console-button").on('click', showSqlConsoleDialog);
keyboardActionService.setGlobalActionHandler("ShowSQLConsole", showSqlConsoleDialog);
const showBackendLogDialog = () => import(BACKEND_LOG).then(d => d.showDialog());
$("#show-backend-log-button").on('click', showBackendLogDialog);
keyboardActionService.setGlobalActionHandler("ShowBackendLog", showBackendLogDialog);
$("#show-about-dialog-button").on('click', () => import(ABOUT).then(d => d.showDialog()));
if (utils.isElectron()) {
$("#history-navigation").show();
$("#history-back-button").on('click', window.history.back);
$("#history-forward-button").on('click', window.history.forward);
keyboardActionService.setGlobalActionHandler("BackInNoteHistory", window.history.back);
if (utils.isMac()) {
// Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376
utils.bindGlobalShortcut('meta+left', window.history.back);
utils.bindGlobalShortcut('meta+right', window.history.forward);
}
else {
utils.bindGlobalShortcut('alt+left', window.history.back);
utils.bindGlobalShortcut('alt+right', window.history.forward);
}
$("#history-forward-button").on('click', window.history.forward);
keyboardActionService.setGlobalActionHandler("ForwardInNoteHistory", window.history.forward);
}
utils.bindGlobalShortcut('alt+m', e => {
$(".hide-toggle").toggle();
$("#container").toggleClass("distraction-free-mode");
});
let zenModeActive = false;
// hide (toggle) everything except for the note content for distraction free writing
utils.bindGlobalShortcut('alt+t', e => {
// hide (toggle) everything except for the note content for zen mode
const toggleZenMode = () => {
if (!zenModeActive) {
$(".hide-in-zen-mode,.gutter").addClass("hidden-by-zen-mode");
$("#container").addClass("zen-mode");
zenModeActive = true;
}
else {
// not hiding / showing explicitly since element might be hidden also for other reasons
$(".hide-in-zen-mode,.gutter").removeClass("hidden-by-zen-mode");
$("#container").removeClass("zen-mode");
zenModeActive = false;
}
};
$("#toggle-zen-mode-button").on('click', toggleZenMode);
keyboardActionService.setGlobalActionHandler("ToggleZenMode", toggleZenMode);
keyboardActionService.setGlobalActionHandler("InsertDateTimeToText", () => {
const date = new Date();
const dateString = utils.formatDateTime(date);
linkService.addTextToEditor(dateString);
});
utils.bindGlobalShortcut('f5', utils.reloadApp);
$("#reload-frontend-button").on('click', utils.reloadApp);
utils.bindGlobalShortcut('ctrl+r', utils.reloadApp);
keyboardActionService.setGlobalActionHandler("ReloadFrontendApp", utils.reloadApp);
$("#open-dev-tools-button").toggle(utils.isElectron());
keyboardActionService.setGlobalActionHandler("PasteMarkdownIntoText", async () => {
const dialog = await import("../dialogs/markdown_import.js");
dialog.importMarkdownInline();
});
if (utils.isElectron()) {
const openDevTools = () => {
require('electron').remote.getCurrentWindow().toggleDevTools();
@@ -119,8 +157,8 @@ function registerEntrypoints() {
return false;
};
utils.bindGlobalShortcut('ctrl+shift+i', openDevTools);
$("#open-dev-tools-button").on('click', openDevTools);
keyboardActionService.setGlobalActionHandler("OpenDevTools", openDevTools);
}
let findInPage;
@@ -141,18 +179,16 @@ function registerEntrypoints() {
textHoverBgColor: '#555',
caseSelectedColor: 'var(--main-border-color)'
});
}
if (utils.isElectron()) {
utils.bindGlobalShortcut('ctrl+f', () => {
findInPage.openFindWindow();
return false;
keyboardActionService.setGlobalActionHandler("FindInText", () => {
if (!glob.activeDialog || !glob.activeDialog.is(":visible")) {
findInPage.openFindWindow();
}
});
}
if (utils.isElectron()) {
const toggleFullscreen = function() {
const toggleFullscreen = () => {
const win = require('electron').remote.getCurrentWindow();
if (win.isFullScreenable()) {
@@ -164,7 +200,7 @@ function registerEntrypoints() {
$("#toggle-fullscreen-button").on('click', toggleFullscreen);
utils.bindGlobalShortcut('f11', toggleFullscreen);
keyboardActionService.setGlobalActionHandler("ToggleFullscreen", toggleFullscreen);
}
else {
// outside of electron this is handled by the browser
@@ -172,8 +208,8 @@ function registerEntrypoints() {
}
if (utils.isElectron()) {
utils.bindGlobalShortcut('ctrl+-', zoomService.decreaseZoomFactor);
utils.bindGlobalShortcut('ctrl+=', zoomService.increaseZoomFactor);
keyboardActionService.setGlobalActionHandler("ZoomOut", zoomService.decreaseZoomFactor);
keyboardActionService.setGlobalActionHandler("ZoomIn", zoomService.increaseZoomFactor);
}
$(document).on('click', "a[data-action='note-revision']", async event => {
@@ -188,7 +224,7 @@ function registerEntrypoints() {
return false;
});
utils.bindGlobalShortcut('ctrl+shift+c', () => import(CLONE_TO).then(d => {
keyboardActionService.setGlobalActionHandler("CloneNotesTo", () => import(CLONE_TO).then(d => {
const activeNode = treeService.getActiveNode();
const selectedOrActiveNodes = treeService.getSelectedOrActiveNodes(activeNode);
@@ -198,14 +234,60 @@ function registerEntrypoints() {
d.showDialog(noteIds);
}));
utils.bindGlobalShortcut('ctrl+shift+x', () => import(MOVE_TO).then(d => {
keyboardActionService.setGlobalActionHandler("MoveNotesTo", () => import(MOVE_TO).then(d => {
const activeNode = treeService.getActiveNode();
const selectedOrActiveNodes = treeService.getSelectedOrActiveNodes(activeNode);
d.showDialog(selectedOrActiveNodes);
}));
keyboardActionService.setGlobalActionHandler("CreateNoteIntoDayNote", async () => {
const todayNote = await dateNoteService.getTodayNote();
const {note} = await server.post(`notes/${todayNote.noteId}/children?target=into`, {
title: 'new note',
content: '',
type: 'text',
isProtected: todayNote.isProtected
});
await treeService.expandToNote(note.noteId);
await noteDetailService.openInTab(note.noteId, true);
noteDetailService.focusAndSelectTitle();
});
keyboardActionService.setGlobalActionHandler("EditBranchPrefix", async () => {
const node = treeService.getActiveNode();
const editBranchPrefixDialog = await import("../dialogs/branch_prefix.js");
editBranchPrefixDialog.showDialog(node);
});
keyboardActionService.setGlobalActionHandler("ToggleNoteHoisting", async () => {
const node = treeService.getActiveNode();
hoistedNoteService.getHoistedNoteId().then(async hoistedNoteId => {
if (node.data.noteId === hoistedNoteId) {
hoistedNoteService.unhoist();
}
else {
const note = await treeCache.getNote(node.data.noteId);
if (note.type !== 'search') {
hoistedNoteService.setHoistedNoteId(node.data.noteId);
}
}
});
});
keyboardActionService.setGlobalActionHandler("SearchInSubtree", () => {
const node = treeService.getActiveNode();
searchNotesService.searchInSubtree(node.data.noteId);
});
}
export default {
@@ -125,14 +125,14 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
}
/**
* Executes given anonymous function on the server.
* Executes given anonymous function on the backend.
* Internally this serializes the anonymous function into string and sends it to backend via AJAX.
*
* @param {string} script - script to be executed on the backend
* @param {Array.<?>} params - list of parameters to the anonymous function to be send to backend
* @return {Promise<*>} return value of the executed function on the backend
*/
this.runOnServer = async (script, params = []) => {
this.runOnBackend = async (script, params = []) => {
if (typeof script === "function") {
script = script.toString();
}
@@ -159,6 +159,12 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
}
};
/**
* @deprecated new name of this API call is runOnBackend so use that
* @method
*/
this.runOnServer = this.runOnBackend;
/**
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "@dateModified =* MONTH AND @log". See full documentation for all options at: https://github.com/zadam/trilium/wiki/Search
@@ -382,6 +388,17 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
* @param {function} handler
*/
this.bindGlobalShortcut = utils.bindGlobalShortcut;
/**
* Trilium runs in backend and frontend process, when something is changed on the backend from script,
* frontend will get asynchronously synchronized.
*
* This method returns a promise which resolves once all the backend -> frontend synchronization is finished.
* Typical use case is when new note has been created, we should wait until it is synced into frontend and only then activate it.
*
* @method
*/
this.waitUntilSynced = ws.waitForMaxKnownSyncId;
}
export default FrontendScriptApi;
@@ -0,0 +1,117 @@
import server from "./server.js";
import utils from "./utils.js";
const keyboardActionRepo = {};
const keyboardActionsLoaded = server.get('keyboard-actions').then(actions => {
for (const action of actions) {
keyboardActionRepo[action.actionName] = action;
}
});
server.get('keyboard-shortcuts-for-notes').then(shortcutForNotes => {
for (const shortcut in shortcutForNotes) {
utils.bindGlobalShortcut(shortcut, async () => {
const treeService = (await import("./tree.js")).default;
treeService.activateNote(shortcutForNotes[shortcut]);
});
}
});
function setGlobalActionHandler(actionName, handler) {
keyboardActionsLoaded.then(() => {
const action = keyboardActionRepo[actionName];
if (!action) {
throw new Error(`Cannot find keyboard action '${actionName}'`);
}
action.handler = handler;
for (const shortcut of action.effectiveShortcuts) {
if (shortcut && !shortcut.startsWith("global:")) { // global shortcuts should be handled in the electron code
utils.bindGlobalShortcut(shortcut, handler);
}
}
});
}
function setElementActionHandler($el, actionName, handler) {
keyboardActionsLoaded.then(() => {
const action = keyboardActionRepo[actionName];
if (!action) {
throw new Error(`Cannot find keyboard action '${actionName}'`);
}
// not setting action.handler since this is not global
for (const shortcut of action.effectiveShortcuts) {
if (shortcut) {
utils.bindElShortcut($el, shortcut, handler);
}
}
});
}
async function triggerAction(actionName) {
const action = await getAction(actionName);
if (!action.handler) {
throw new Error(`Action ${actionName} has no handler`);
}
await action.handler();
}
async function getAction(actionName, silent = false) {
await keyboardActionsLoaded;
const action = keyboardActionRepo[actionName];
if (!action) {
if (silent) {
console.log(`Cannot find action ${actionName}`);
}
else {
throw new Error(`Cannot find action ${actionName}`);
}
}
return action;
}
function updateDisplayedShortcuts($container) {
$container.find('kbd[data-kb-action]').each(async (i, el) => {
const actionName = $(el).attr('data-kb-action');
const action = await getAction(actionName, true);
if (action) {
$(el).text(action.effectiveShortcuts.join(', '));
}
});
$container.find('button[data-kb-action],a.icon-action[data-kb-action]').each(async (i, el) => {
const actionName = $(el).attr('data-kb-action');
const action = await getAction(actionName, true);
if (action) {
const title = $(el).attr('title');
const shortcuts = action.effectiveShortcuts.join(', ');
const newTitle = !title || !title.trim() ? shortcuts : `${title} (${shortcuts})`;
$(el).attr('title', newTitle);
}
});
}
$(() => updateDisplayedShortcuts($(document)));
export default {
setGlobalActionHandler,
setElementActionHandler,
triggerAction,
getAction,
updateDisplayedShortcuts
};
+16
View File
@@ -0,0 +1,16 @@
class Actions {
constructor() {
this.JUMP_TO = "";
}
}
const actions = new Actions();
function bind() {
}
export default {
actions,
bind
};
+39 -43
View File
@@ -6,15 +6,14 @@ import noteDetailService from "./note_detail.js";
function getNotePathFromUrl(url) {
const notePathMatch = /#(root[A-Za-z0-9/]*)$/.exec(url);
if (notePathMatch === null) {
return null;
}
else {
return notePathMatch[1];
}
return notePathMatch === null ? null : notePathMatch[1];
}
async function createNoteLink(notePath, noteTitle = null, tooltip = true) {
async function createNoteLink(notePath, options = {}) {
let noteTitle = options.title;
const showTooltip = options.showTooltip === undefined ? true : options.showTooltip;
const showNotePath = options.showNotePath === undefined ? false : options.showNotePath;
if (!noteTitle) {
const {noteId, parentNoteId} = treeUtils.getNoteIdAndParentIdFromNotePath(notePath);
@@ -27,30 +26,26 @@ async function createNoteLink(notePath, noteTitle = null, tooltip = true) {
}).attr('data-action', 'note')
.attr('data-note-path', notePath);
if (!tooltip) {
if (!showTooltip) {
$noteLink.addClass("no-tooltip-preview");
}
return $noteLink;
}
const $container = $("<span>").append($noteLink);
async function createNoteLinkWithPath(notePath, noteTitle = null) {
const $link = await createNoteLink(notePath, noteTitle);
if (showNotePath) {
notePath = await treeService.resolveNotePath(notePath);
const $res = $("<span>").append($link);
if (notePath.includes("/")) {
const noteIds = notePath.split("/");
noteIds.pop(); // remove last element
const parentNotePath = noteIds.join("/").trim();
if (parentNotePath) {
$res.append($("<small>").text(" (" + await treeUtils.getNotePathTitle(parentNotePath) + ")"));
$container.append($("<small>").text(" (" + await treeUtils.getNotePathTitle(parentNotePath) + ")"));
}
}
return $res;
return $container;
}
function getNotePathFromLink($link) {
@@ -66,13 +61,16 @@ function getNotePathFromLink($link) {
}
function goToLink(e) {
const $link = $(e.target);
e.preventDefault();
e.stopPropagation();
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
if (notePath) {
if ((e.which === 1 && e.ctrlKey) || e.which === 2) {
noteDetailService.openInTab(notePath);
noteDetailService.openInTab(notePath, false);
}
else if (e.which === 1) {
treeService.activateNote(notePath);
@@ -89,9 +87,6 @@ function goToLink(e) {
}
}
e.preventDefault();
e.stopPropagation();
return true;
}
@@ -118,7 +113,7 @@ function addTextToEditor(text) {
}
function newTabContextMenu(e) {
const $link = $(e.target);
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
@@ -142,22 +137,27 @@ function newTabContextMenu(e) {
});
}
$(document).on('contextmenu', '.note-detail-text a', newTabContextMenu);
$(document).on('contextmenu', "a[data-action='note']", newTabContextMenu);
$(document).on('contextmenu', ".note-detail-render a", newTabContextMenu);
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
// of opening the link in new window/tab
$(document).on('mousedown', "a[data-action='note']", goToLink);
$(document).on('mousedown', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
$(document).on('dblclick', '.note-detail-text a', goToLink);
$(document).on('mousedown', '.note-detail-text a', function (e) {
const notePath = getNotePathFromLink($(e.target));
if (notePath && ((e.which === 1 && e.ctrlKey) || e.which === 2)) {
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
if ((e.which === 1 && e.ctrlKey) || e.which === 2) {
// if it's a ctrl-click, then we open on new tab, otherwise normal flow (CKEditor opens link-editing dialog)
e.preventDefault();
noteDetailService.loadNoteDetail(notePath, { newTab: true });
if (notePath) {
noteDetailService.loadNoteDetail(notePath, {newTab: true});
}
else {
const address = $link.attr('href');
window.open(address, '_blank');
}
return true;
}
@@ -166,24 +166,20 @@ $(document).on('mousedown', '.note-detail-text a', function (e) {
$(document).on('mousedown', '.note-detail-book a', goToLink);
$(document).on('mousedown', '.note-detail-render a', goToLink);
$(document).on('mousedown', '.note-detail-text.ck-read-only a', goToLink);
$(document).on('mousedown', 'span.ck-button__label', e => {
// this is a link preview dialog from CKEditor link editing
// for some reason clicked element is span
const url = $(e.target).text();
const notePath = getNotePathFromUrl(url);
if (notePath) {
treeService.activateNote(notePath);
e.preventDefault();
}
$(document).on('mousedown', 'a.ck-link-actions__preview', goToLink);
$(document).on('click', 'a.ck-link-actions__preview', e => {
e.preventDefault();
e.stopPropagation();
});
$(document).on('contextmenu', 'a.ck-link-actions__preview', newTabContextMenu);
$(document).on('contextmenu', '.note-detail-text a', newTabContextMenu);
$(document).on('contextmenu', "a[data-action='note']", newTabContextMenu);
$(document).on('contextmenu', ".note-detail-render a", newTabContextMenu);
export default {
getNotePathFromUrl,
createNoteLink,
createNoteLinkWithPath,
addLinkToEditor,
addTextToEditor,
goToLink
+1 -1
View File
@@ -88,7 +88,7 @@ export default class LinkMap {
.addClass("note-box")
.prop("id", noteBoxId);
linkService.createNoteLink(noteId, note.title).then($link => {
linkService.createNoteLink(noteId, {title: note.title}).then($link => {
$link.on('click', e => {
try {
$link.tooltip('dispose');
@@ -1,6 +1,7 @@
import optionsService from "./options.js";
const MIME_TYPES_DICT = [
{ default: true, title: "Plain text", mime: "text/plain" },
{ title: "APL", mime: "text/apl" },
{ title: "PGP", mime: "application/pgp" },
{ title: "ASN.1", mime: "text/x-ttcn-asn" },
@@ -91,7 +92,6 @@ const MIME_TYPES_DICT = [
{ default: true, title: "Perl", mime: "text/x-perl" },
{ default: true, title: "PHP", mime: "text/x-php" },
{ title: "Pig", mime: "text/x-pig" },
{ title: "Plain Text", mime: "text/plain" },
{ title: "PLSQL", mime: "text/x-plsql" },
{ title: "PostgreSQL", mime: "text/x-pgsql" },
{ title: "PowerShell", mime: "application/x-powershell" },
@@ -168,7 +168,7 @@ function loadMimeTypes(options) {
|| MIME_TYPES_DICT.filter(mt => mt.default).map(mt => mt.mime);
for (const mt of mimeTypes) {
mt.enabled = enabledMimeTypes.includes(mt.mime);
mt.enabled = enabledMimeTypes.includes(mt.mime) || mt.mime === 'text/plain'; // text/plain is always enabled
}
}
@@ -0,0 +1,107 @@
import server from "./server.js";
import utils from "./utils.js";
import renderService from "./render.js";
import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
async function getRenderedContent(note) {
const type = getRenderingType(note);
let rendered;
if (type === 'text') {
const fullNote = await server.get('notes/' + note.noteId);
const $content = $("<div>").html(fullNote.content);
if (utils.isHtmlEmpty(fullNote.content)) {
rendered = "";
}
else {
rendered = $content;
}
}
else if (type === 'code') {
const fullNote = await server.get('notes/' + note.noteId);
if (fullNote.content.trim() === "") {
rendered = "";
}
rendered = $("<pre>").text(fullNote.content);
}
else if (type === 'image') {
rendered = $("<img>").attr("src", `api/images/${note.noteId}/${note.title}`);
}
else if (type === 'file') {
function getFileUrl() {
return utils.getUrlForDownload("api/notes/" + note.noteId + "/download");
}
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>');
$downloadButton.on('click', () => utils.download(getFileUrl()));
$openButton.on('click', () => {
if (utils.isElectron()) {
const open = require("open");
open(getFileUrl(), {url: true});
}
else {
window.location.href = getFileUrl();
}
});
// open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected);
rendered = $('<div>')
.append($downloadButton)
.append(' &nbsp; ')
.append($openButton);
}
else if (type === 'render') {
const $el = $('<div>');
await renderService.render(note, $el, this.ctx);
rendered = $el;
}
else if (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);
}
else {
rendered = "<em>Content of this note cannot be displayed in the book format</em>";
}
return {
renderedContent: rendered,
type
};
}
function getRenderingType(note) {
let type = note.type;
if (note.isProtected) {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
protectedSessionHolder.touchProtectedSession();
}
else {
type = 'protected-session';
}
}
return type;
}
export default {
getRenderedContent
};
+26 -26
View File
@@ -4,11 +4,11 @@ import server from './server.js';
import ws from "./ws.js";
import treeCache from "./tree_cache.js";
import NoteFull from "../entities/note_full.js";
import bundleService from "./bundle.js";
import utils from "./utils.js";
import contextMenuService from "./context_menu.js";
import treeUtils from "./tree_utils.js";
import tabRow from "./tab_row.js";
import keyboardActionService from "./keyboard_actions.js";
const $tabContentsContainer = $("#note-tab-container");
const $savedIndicator = $(".saved-indicator");
@@ -35,8 +35,8 @@ async function reloadAllTabs() {
}
}
async function openInTab(notePath) {
await loadNoteDetail(notePath, { newTab: true });
async function openInTab(notePath, activate) {
await loadNoteDetail(notePath, { newTab: true, activate });
}
async function switchToNote(notePath) {
@@ -274,7 +274,9 @@ async function filterTabs(noteId) {
async function noteDeleted(noteId) {
for (const tc of tabContexts) {
if (tc.notePath && tc.notePath.split("/").includes(noteId)) {
// not removing active even if it contains deleted note since that one will move to another note (handled by deletion logic)
// and we would lose tab context state (e.g. sidebar visibility)
if (!tc.isActive() && tc.notePath && tc.notePath.split("/").includes(noteId)) {
await tabRow.removeTab(tc.$tab[0]);
}
}
@@ -420,33 +422,31 @@ $(tabRow.el).on('contextmenu', '.note-tab', e => {
});
});
if (utils.isElectron()) {
utils.bindGlobalShortcut('ctrl+t', () => {
openEmptyTab();
});
keyboardActionService.setGlobalActionHandler('OpenNewTab', () => {
openEmptyTab();
});
utils.bindGlobalShortcut('ctrl+w', () => {
if (tabRow.activeTabEl) {
tabRow.removeTab(tabRow.activeTabEl);
}
});
keyboardActionService.setGlobalActionHandler('CloseActiveTab', () => {
if (tabRow.activeTabEl) {
tabRow.removeTab(tabRow.activeTabEl);
}
});
utils.bindGlobalShortcut('ctrl+tab', () => {
const nextTab = tabRow.nextTabEl;
keyboardActionService.setGlobalActionHandler('ActivateNextTab', () => {
const nextTab = tabRow.nextTabEl;
if (nextTab) {
tabRow.activateTab(nextTab);
}
});
if (nextTab) {
tabRow.activateTab(nextTab);
}
});
utils.bindGlobalShortcut('ctrl+shift+tab', () => {
const prevTab = tabRow.previousTabEl;
keyboardActionService.setGlobalActionHandler('ActivatePreviousTab', () => {
const prevTab = tabRow.previousTabEl;
if (prevTab) {
tabRow.activateTab(prevTab);
}
});
}
if (prevTab) {
tabRow.activateTab(prevTab);
}
});
tabRow.addListener('activeTabChange', openTabsChanged);
tabRow.addListener('tabRemove', openTabsChanged);
@@ -1,10 +1,6 @@
import server from "./server.js";
import linkService from "./link.js";
import utils from "./utils.js";
import treeCache from "./tree_cache.js";
import renderService from "./render.js";
import protectedSessionHolder from "./protected_session_holder.js";
import protectedSessionService from "./protected_session.js";
import noteContentRenderer from "./note_content_renderer.js";
const MIN_ZOOM_LEVEL = 1;
const MAX_ZOOM_LEVEL = 6;
@@ -129,18 +125,18 @@ class NoteDetailBook {
async renderIntoElement(note, $container) {
for (const childNote of await note.getChildNotes()) {
const type = this.getRenderingType(childNote);
const childNotePath = this.ctx.notePath + '/' + childNote.noteId;
const {type, renderedContent} = await noteContentRenderer.getRenderedContent(childNote);
const $card = $('<div class="note-book-card">')
.attr('data-note-id', childNote.noteId)
.css("flex-basis", ZOOMS[this.zoomLevel].width)
.addClass("type-" + type)
.append($('<h5 class="note-book-title">').append(await linkService.createNoteLink(childNotePath, null, false)))
.append($('<h5 class="note-book-title">').append(await linkService.createNoteLink(childNotePath, {showTooltip: false})))
.append($('<div class="note-book-content">')
.css("max-height", ZOOMS[this.zoomLevel].height)
.append(await this.getNoteContent(type, childNote)));
.append(renderedContent));
const childCount = childNote.getChildNoteIds().length;
@@ -158,81 +154,6 @@ class NoteDetailBook {
}
}
async getNoteContent(type, note) {
if (type === 'text') {
const fullNote = await server.get('notes/' + note.noteId);
const $content = $("<div>").html(fullNote.content);
if (utils.isHtmlEmpty(fullNote.content)) {
return "";
}
else {
return $content;
}
}
else if (type === 'code') {
const fullNote = await server.get('notes/' + note.noteId);
if (fullNote.content.trim() === "") {
return "";
}
return $("<pre>").text(fullNote.content);
}
else if (type === 'image') {
return $("<img>").attr("src", `api/images/${note.noteId}/${note.title}`);
}
else if (type === 'file') {
function getFileUrl() {
// electron needs absolute URL so we extract current host, port, protocol
return utils.getHost() + "/api/notes/" + note.noteId + "/download";
}
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>');
$downloadButton.on('click', () => utils.download(getFileUrl()));
$openButton.on('click', () => {
if (utils.isElectron()) {
const open = require("open");
open(getFileUrl(), {url: true});
}
else {
window.location.href = getFileUrl();
}
});
// open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected);
return $('<div>')
.append($downloadButton)
.append(' &nbsp; ')
.append($openButton);
}
else if (type === 'render') {
const $el = $('<div>');
await renderService.render(note, $el, this.ctx);
return $el;
}
else if (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);
return $("<div>")
.append("<div>This note is protected and to access it you need to enter password.</div>")
.append("<br/>")
.append($button);
}
else {
return "<em>Content of this note cannot be displayed in the book format</em>";
}
}
/** @return {boolean} true if this is "auto book" activated (empty text note) and not explicit book note */
isAutoBook() {
return this.ctx.note.type !== 'book';
@@ -257,21 +178,6 @@ class NoteDetailBook {
}
}
getRenderingType(childNote) {
let type = childNote.type;
if (childNote.isProtected) {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
protectedSessionHolder.touchProtectedSession();
}
else {
type = 'protected-session';
}
}
return type;
}
getContent() {
// for auto-book cases when renaming title there should be content
return "";
@@ -3,7 +3,7 @@ import bundleService from "./bundle.js";
import toastService from "./toast.js";
import server from "./server.js";
import noteDetailService from "./note_detail.js";
import utils from "./utils.js";
import keyboardActionService from "./keyboard_actions.js";
class NoteDetailCode {
@@ -17,7 +17,7 @@ class NoteDetailCode {
this.$editorEl = this.$component.find('.note-detail-code-editor');
this.$executeScriptButton = ctx.$tabContent.find(".execute-script-button");
utils.bindElShortcut(ctx.$tabContent, "ctrl+return", () => this.executeCurrentNote());
keyboardActionService.setElementActionHandler(ctx.$tabContent, 'RunActiveNote', () => this.executeCurrentNote());
this.$executeScriptButton.on('click', () => this.executeCurrentNote());
}
@@ -55,18 +55,21 @@ class NoteDetailCode {
this.onNoteChange(() => this.ctx.noteChanged());
}
// CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check)
// we provide fallback
this.codeEditor.setValue(this.ctx.note.content || "");
// lazy loading above can take time and tab might have been already switched to another note
if (this.ctx.note && this.ctx.note.type === 'code') {
// CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check)
// we provide fallback
this.codeEditor.setValue(this.ctx.note.content || "");
const info = CodeMirror.findModeByMIME(this.ctx.note.mime);
const info = CodeMirror.findModeByMIME(this.ctx.note.mime);
if (info) {
this.codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(this.codeEditor, info.mode);
if (info) {
this.codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(this.codeEditor, info.mode);
}
this.show();
}
this.show();
}
show() {
@@ -39,8 +39,11 @@ class NoteDetailFile {
});
this.$uploadNewRevisionInput.on('change', async () => {
const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
this.$uploadNewRevisionInput.val('');
const formData = new FormData();
formData.append('upload', this.$uploadNewRevisionInput[0].files[0]);
formData.append('upload', fileToUpload);
const result = await $.ajax({
url: baseApiUrl + 'notes/' + this.ctx.note.noteId + '/file',
@@ -87,8 +90,7 @@ class NoteDetailFile {
}
getFileUrl() {
// electron needs absolute URL so we extract current host, port, protocol
return utils.getHost() + "/api/notes/" + this.ctx.note.noteId + "/download";
return utils.getUrlForDownload("api/notes/" + this.ctx.note.noteId + "/download");
}
show() {}
@@ -48,8 +48,11 @@ class NoteDetailImage {
});
this.$uploadNewRevisionInput.on('change', async () => {
const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
this.$uploadNewRevisionInput.val('');
const formData = new FormData();
formData.append('upload', this.$uploadNewRevisionInput[0].files[0]);
formData.append('upload', fileToUpload);
const result = await $.ajax({
url: baseApiUrl + 'images/' + this.ctx.note.noteId,
@@ -98,8 +101,7 @@ class NoteDetailImage {
}
getFileUrl() {
// electron needs absolute URL so we extract current host, port, protocol
return utils.getHost() + `/api/notes/${this.ctx.note.noteId}/download`;
return utils.getUrlForDownload(`api/notes/${this.ctx.note.noteId}/download`);
}
show() {}
@@ -129,9 +129,10 @@ class NoteDetailRelationMap {
return;
}
const {note} = await server.post(`notes/${this.ctx.note.noteId}/children`, {
const {note} = await server.post(`notes/${this.ctx.note.noteId}/children?target=into`, {
title,
target: 'into'
content: '',
type: 'text'
});
toastService.showMessage("Click on canvas to place new note");
@@ -159,7 +160,7 @@ class NoteDetailRelationMap {
const noteId = this.idToNoteId($noteBox.prop("id"));
if (cmd === "open-in-new-tab") {
noteDetailService.openInTab(noteId);
noteDetailService.openInTab(noteId, false);
}
else if (cmd === "remove") {
const confirmDialog = await import('../dialogs/confirm.js');
@@ -238,14 +239,17 @@ class NoteDetailRelationMap {
await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP);
this.loadMapData();
jsPlumb.ready(() => {
this.initJsPlumbInstance();
// lazy loading above can take time and tab might have been already switched to another note
if (this.ctx.note && this.ctx.note.type === 'relation-map') {
this.loadMapData();
this.initPanZoom();
this.initJsPlumbInstance();
this.loadNotesAndRelations();
this.initPanZoom();
this.loadNotesAndRelations();
}
});
}
@@ -490,7 +494,7 @@ class NoteDetailRelationMap {
}
async createNoteBox(noteId, title, x, y) {
const $link = await linkService.createNoteLink(noteId, title);
const $link = await linkService.createNoteLink(noteId, {title});
$link.mousedown(e => {
console.log(e);
@@ -1,6 +1,42 @@
import libraryLoader from "./library_loader.js";
import treeService from './tree.js';
import noteAutocompleteService from './note_autocomplete.js';
import mimeTypesService from './mime_types.js';
import treeCache from "./tree_cache.js";
const mentionSetup = {
feeds: [
{
marker: '@',
feed: queryText => {
return new Promise((res, rej) => {
noteAutocompleteService.autocompleteSource(queryText, rows => {
if (rows.length === 1 && rows[0].title === 'No results') {
rows = [];
}
for (const row of rows) {
row.text = row.name = row.noteTitle;
row.id = '@' + row.text;
row.link = '#' + row.path;
}
res(rows);
});
});
},
itemRenderer: item => {
const itemElement = document.createElement('span');
itemElement.classList.add('mentions-item');
itemElement.innerHTML = `${item.highlightedTitle} `;
return itemElement;
},
minimumCharacters: 0
}
]
};
class NoteDetailText {
/**
@@ -26,13 +62,23 @@ class NoteDetailText {
else {
window.open(src, '_blank');
}
})
});
}
async render() {
if (!this.textEditor) {
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
const codeBlockLanguages =
(await mimeTypesService.getMimeTypes())
.filter(mt => mt.enabled)
.map(mt => {
return {
language: mt.mime.toLowerCase().replace(/[\W_]+/g,"-"),
label: mt.title
}
});
// CKEditor since version 12 needs the element to be visible before initialization. At the same time
// we want to avoid flicker - i.e. show editor only once everything is ready. That's why we have separate
// display of $component in both branches.
@@ -43,52 +89,29 @@ class NoteDetailText {
if (!this.textEditor) {
this.textEditor = await BalloonEditor.create(this.$editorEl[0], {
placeholder: "Type the content of your note here ...",
mention: {
feeds: [
{
marker: '@',
feed: queryText => {
return new Promise((res, rej) => {
noteAutocompleteService.autocompleteSource(queryText, rows => {
if (rows.length === 1 && rows[0].title === 'No results') {
rows = [];
}
for (const row of rows) {
row.text = row.name = row.noteTitle;
row.id = '@' + row.text;
row.link = '#' + row.path;
}
console.log(rows.slice(0, Math.min(5, rows.length)));
res(rows);
});
});
},
itemRenderer: item => {
const itemElement = document.createElement('span');
itemElement.classList.add('mentions-item');
itemElement.innerHTML = `${item.highlightedTitle} `;
return itemElement;
},
minimumCharacters: 0
}
]
mention: mentionSetup,
codeBlock: {
languages: codeBlockLanguages
}
});
if (glob.isDev) {
await import('../../libraries/ckeditor/inspector.js');
CKEditorInspector.attach(this.textEditor);
}
this.onNoteChange(() => this.ctx.noteChanged());
}
}
this.textEditor.isReadOnly = await this.isReadOnly();
// lazy loading above can take time and tab might have been already switched to another note
if (this.ctx.note && this.ctx.note.type === 'text') {
this.textEditor.isReadOnly = await this.isReadOnly();
this.$component.show();
this.$component.show();
this.textEditor.setData(this.ctx.note.content);
this.textEditor.setData(this.ctx.note.content);
}
}
getContent() {
@@ -13,6 +13,10 @@ class Options {
return this.arr[key];
}
getNames() {
return Object.keys(this.arr);
}
getJson(key) {
try {
return JSON.parse(this.arr[key]);
@@ -142,6 +142,12 @@ async function refreshSearch() {
toastService.showMessage("Saved search note refreshed.");
}
function searchInSubtree(noteId) {
showSearch();
$searchInput.val(`@in=${noteId} @text*=*`);
}
function init() {
const hashValue = document.location.hash ? document.location.hash.substr(1) : ""; // strip initial #
@@ -178,5 +184,6 @@ export default {
refreshSearch,
doSearch,
init,
searchInSubtree,
getHelpText: () => helpText
};
+58 -24
View File
@@ -42,12 +42,16 @@ async function remove(url, headers = {}) {
let i = 1;
const reqResolves = {};
let maxKnownSyncId = 0;
async function call(method, url, data, headers = {}) {
let resp;
if (utils.isElectron()) {
const ipc = require('electron').ipcRenderer;
const requestId = i++;
return new Promise((resolve, reject) => {
resp = await new Promise((resolve, reject) => {
reqResolves[requestId] = resolve;
if (REQUEST_LOGGING_ENABLED) {
@@ -64,32 +68,58 @@ async function call(method, url, data, headers = {}) {
});
}
else {
return await ajax(url, method, data, headers);
resp = await ajax(url, method, data, headers);
}
const maxSyncIdStr = resp.headers['trilium-max-sync-id'];
if (maxSyncIdStr && maxSyncIdStr.trim()) {
maxKnownSyncId = Math.max(maxKnownSyncId, parseInt(maxSyncIdStr));
}
return resp.body;
}
async function ajax(url, method, data, headers) {
const options = {
url: baseApiUrl + url,
type: method,
headers: getHeaders(headers),
timeout: 60000
};
function ajax(url, method, data, headers) {
return new Promise((res, rej) => {
const options = {
url: baseApiUrl + url,
type: method,
headers: getHeaders(headers),
timeout: 60000,
success: (body, textStatus, jqXhr) => {
const respHeaders = {};
if (data) {
try {
options.data = JSON.stringify(data);
}
catch (e) {
console.log("Can't stringify data: ", data, " because of error: ", e)
}
options.contentType = "application/json";
}
jqXhr.getAllResponseHeaders().trim().split(/[\r\n]+/).forEach(line => {
const parts = line.split(': ');
const header = parts.shift();
respHeaders[header] = parts.join(': ');
});
return await $.ajax(options).catch(e => {
const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText;
toastService.showError(message);
toastService.throwError(message);
res({
body,
headers: respHeaders
});
},
error: (jqXhr, textStatus, error) => {
const message = "Error when calling " + method + " " + url + ": " + textStatus + " - " + error;
toastService.showError(message);
toastService.throwError(message);
rej(error);
}
};
if (data) {
try {
options.data = JSON.stringify(data);
} catch (e) {
console.log("Can't stringify data: ", data, " because of error: ", e)
}
options.contentType = "application/json";
}
$.ajax(options);
});
}
@@ -101,7 +131,10 @@ if (utils.isElectron()) {
console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode);
}
reqResolves[arg.requestId](arg.body);
reqResolves[arg.requestId]({
body: arg.body,
headers: arg.headers
});
delete reqResolves[arg.requestId];
});
@@ -114,5 +147,6 @@ export default {
remove,
ajax,
// don't remove, used from CKEditor image upload!
getHeaders
getHeaders,
getMaxKnownSyncId: () => maxKnownSyncId
};
+59 -18
View File
@@ -1,6 +1,47 @@
import bundleService from "./bundle.js";
import ws from "./ws.js";
import optionsService from "./options.js";
import splitService from "./split.js";
import optionService from "./options.js";
import server from "./server.js";
import noteDetailService from "./note_detail.js";
const $sidebar = $("#right-pane");
const $sidebarContainer = $('#sidebar-container');
const $showSidebarButton = $("#show-sidebar-button");
const $hideSidebarButton = $("#hide-sidebar-button");
optionService.waitForOptions().then(options => toggleSidebar(options.is('rightPaneVisible')));
function toggleSidebar(show) {
$sidebar.toggle(show);
$showSidebarButton.toggle(!show);
$hideSidebarButton.toggle(show);
if (show) {
splitService.setupSplitWithSidebar();
}
else {
splitService.setupSplitWithoutSidebar();
}
}
$hideSidebarButton.on('click', () => {
toggleSidebar(false);
server.put('options/rightPaneVisible/false');
});
$showSidebarButton.on('click', async () => {
toggleSidebar(true);
await server.put('options/rightPaneVisible/true');
const {sidebar} = noteDetailService.getActiveTabContext();
await sidebar.noteLoaded();
sidebar.show();
});
class Sidebar {
/**
@@ -14,30 +55,18 @@ class Sidebar {
widgets: []
}, state);
this.widgets = [];
this.$sidebar = ctx.$tabContent.find(".note-detail-sidebar");
this.$widgetContainer = this.$sidebar.find(".note-detail-widget-container");
this.$showSideBarButton = this.ctx.$tabContent.find(".show-sidebar-button");
this.$hideSidebarButton = this.$sidebar.find(".hide-sidebar-button");
this.$hideSidebarButton.on('click', () => {
this.$sidebar.hide();
this.$showSideBarButton.show();
this.ctx.stateChanged();
});
this.$widgetContainer = $sidebar.find(`.sidebar-widget-container[data-tab-id=${this.ctx.tabId}]`);
this.$showSideBarButton.on('click', () => {
this.$sidebar.show();
this.$showSideBarButton.hide();
this.ctx.stateChanged();
this.noteLoaded();
});
if (this.$widgetContainer.length === 0) {
this.$widgetContainer = $(`<div class="sidebar-widget-container">`).attr('data-tab-id', this.ctx.tabId);
this.$showSideBarButton.toggle(!state.visible);
this.$sidebar.toggle(state.visible);
$sidebarContainer.append(this.$widgetContainer);
}
}
isVisible() {
return this.$sidebar.css("display") !== "none";
return $sidebar.css("display") !== "none";
}
getSidebarState() {
@@ -91,6 +120,12 @@ class Sidebar {
this.renderWidgets(widgets);
}
show() {
$sidebarContainer.find('.sidebar-widget-container').each((i, el) => {
$(el).toggle($(el).attr('data-tab-id') === this.ctx.tabId);
});
}
// it's important that this method is sync so that the whole render-update is atomic
// otherwise we could get race issues (doubled widgets etc.)
renderWidgets(widgets) {
@@ -119,6 +154,12 @@ class Sidebar {
this.$widgetContainer.empty().append(...widgetsToAppend);
}
remove() {
if (this.$widgetContainer) {
this.$widgetContainer.remove();
}
}
eventReceived(name, data) {
for (const widget of this.widgets) {
if (widget.eventReceived) {
+51
View File
@@ -0,0 +1,51 @@
import server from "./server.js";
import optionService from "./options.js";
let instance;
async function getPaneWidths() {
const options = await optionService.waitForOptions();
return {
leftPaneWidth: options.getInt('leftPaneWidth'),
rightPaneWidth: options.getInt('rightPaneWidth')
};
}
async function setupSplitWithSidebar() {
if (instance) {
instance.destroy();
}
const {leftPaneWidth, rightPaneWidth} = await getPaneWidths();
instance = Split(['#left-pane', '#center-pane', '#right-pane'], {
sizes: [leftPaneWidth, 100 - leftPaneWidth - rightPaneWidth, rightPaneWidth],
gutterSize: 5,
onDragEnd: sizes => {
server.put('options/leftPaneWidth/' + Math.round(sizes[0]));
server.put('options/rightPaneWidth/' + Math.round(sizes[2]));
}
});
}
async function setupSplitWithoutSidebar() {
if (instance) {
instance.destroy();
}
const {leftPaneWidth} = await getPaneWidths();
instance = Split(['#left-pane', '#center-pane'], {
sizes: [leftPaneWidth, 100 - leftPaneWidth],
gutterSize: 5,
onDragEnd: sizes => {
server.put('options/leftPaneWidth/' + Math.round(sizes[0]));
}
});
}
export default {
setupSplitWithSidebar,
setupSplitWithoutSidebar
};
+32 -6
View File
@@ -148,6 +148,10 @@ class TabContext {
this.setCurrentNotePathToHash();
if (this.sidebar) {
this.sidebar.noteLoaded(); // load async
}
this.noteChangeDisabled = true;
try {
@@ -181,10 +185,6 @@ class TabContext {
this.noteType.update();
}
if (this.sidebar) {
this.sidebar.noteLoaded(); // load async
}
bundleService.executeRelationBundles(this.note, 'runOnNoteView', this);
}
@@ -204,6 +204,10 @@ class TabContext {
this.$tabContent.show();
if (this.sidebar) {
this.sidebar.show();
}
this.setCurrentNotePathToHash();
this.setTitleBar();
}
@@ -246,11 +250,15 @@ class TabContext {
}
setCurrentNotePathToHash() {
if (this.$tab[0] === this.tabRow.activeTabEl) {
if (this.isActive()) {
document.location.hash = (this.notePath || "") + "-" + this.tabId;
}
}
isActive() {
return this.$tab[0] === this.tabRow.activeTabEl;
}
setupClasses() {
for (const clazz of Array.from(this.$tab[0].classList)) { // create copy to safely iterate over while removing classes
if (clazz !== 'note-tab') {
@@ -281,6 +289,10 @@ class TabContext {
}
getComponent() {
if (!this.components[this.type]) {
throw new Error("Could not find component for type: " + this.type);
}
return this.components[this.type];
}
@@ -367,7 +379,7 @@ class TabContext {
async addPath(notePath, isCurrent) {
const title = await treeUtils.getNotePathTitle(notePath);
const noteLink = await linkService.createNoteLink(notePath, title);
const noteLink = await linkService.createNoteLink(notePath, {title});
noteLink
.addClass("no-tooltip-preview")
@@ -407,6 +419,16 @@ class TabContext {
await this.addPath(notePath, isCurrent);
}
const cloneLink = $("<div>")
.addClass("dropdown-item")
.append(
$('<button class="btn btn-sm">')
.text('Clone note to new location...')
.on('click', () => import("../dialogs/clone_to.js").then(d => d.showDialog([this.note.noteId])))
);
this.$notePathList.append(cloneLink);
}
}
@@ -418,6 +440,10 @@ class TabContext {
await this.saveNoteIfChanged();
this.$tabContent.remove();
}
if (this.sidebar) {
this.sidebar.remove();
}
}
cleanup() {
File diff suppressed because one or more lines are too long
+33 -27
View File
@@ -9,13 +9,12 @@ import server from './server.js';
import treeCache from './tree_cache.js';
import toastService from "./toast.js";
import treeBuilder from "./tree_builder.js";
import treeKeyBindings from "./tree_keybindings.js";
import Branch from '../entities/branch.js';
import NoteShort from '../entities/note_short.js';
import treeKeyBindingService from "./tree_keybindings.js";
import hoistedNoteService from '../services/hoisted_note.js';
import optionsService from "../services/options.js";
import TreeContextMenu from "./tree_context_menu.js";
import bundle from "./bundle.js";
import keyboardActionService from "./keyboard_actions.js";
const $tree = $("#tree");
const $createTopLevelNoteButton = $("#create-top-level-note-button");
@@ -128,16 +127,16 @@ async function getNodeFromPath(notePath, expand = false, expandOpts = {}) {
// we expand only after hoisted note since before then nodes are not actually present in the tree
if (parentNode) {
checkFolderStatus(parentNode);
if (!parentNode.isLoaded()) {
await parentNode.load();
}
if (expand) {
parentNode.setExpanded(true, expandOpts);
await parentNode.setExpanded(true, expandOpts);
}
await checkFolderStatus(parentNode);
let foundChildNode = findChildNode(parentNode, childNoteId);
if (!foundChildNode) { // note might be recently created so we'll force reload and try again
@@ -368,7 +367,7 @@ async function treeInitialized() {
const notePath = location.hash.substr(1);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (await treeCache.noteExists(noteId)) {
if (noteId && await treeCache.noteExists(noteId)) {
for (const tab of openTabs) {
tab.active = false;
}
@@ -430,7 +429,7 @@ async function treeInitialized() {
setFrontendAsLoaded();
}
function initFancyTree(tree) {
async function initFancyTree(tree) {
utils.assertArguments(tree);
$tree.fancytree({
@@ -473,7 +472,7 @@ function initFancyTree(tree) {
collapse: (event, data) => setExpandedToServer(data.node.data.branchId, false),
init: (event, data) => treeInitialized(), // don't collapse to short form
hotkeys: {
keydown: treeKeyBindings
keydown: await treeKeyBindingService.getKeyboardBindings()
},
dnd5: dragAndDropSetup,
lazyLoad: function(event, data) {
@@ -627,6 +626,8 @@ async function createNewTopLevelNote() {
async function createNote(node, parentNoteId, target, extraOptions = {}) {
utils.assertArguments(node, parentNoteId, target);
extraOptions.activate = extraOptions.activate === undefined ? true : !!extraOptions.activate;
// if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted
// but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often
if (!extraOptions.isProtected || !protectedSessionHolder.isProtectedSessionAvailable()) {
@@ -649,11 +650,9 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
const newNoteName = extraOptions.title || "new note";
const {note, branch} = await server.post('notes/' + parentNoteId + '/children', {
const {note, branch} = await server.post(`notes/${parentNoteId}/children?target=${target}&targetBranchId=${node.data.branchId}`, {
title: newNoteName,
content: extraOptions.content,
target: target,
target_branchId: node.data.branchId,
content: extraOptions.content || "",
isProtected: extraOptions.isProtected,
type: extraOptions.type
});
@@ -670,7 +669,7 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
const noteEntity = await treeCache.getNote(note.noteId);
const branchEntity = treeCache.getBranch(branch.branchId);
let newNode = {
let newNodeData = {
title: newNoteName,
noteId: branchEntity.noteId,
parentNoteId: parentNoteId,
@@ -685,8 +684,11 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
key: utils.randomString(12) // this should prevent some "duplicate key" errors
};
/** @var {FancytreeNode} */
let newNode;
if (target === 'after') {
await node.appendSibling(newNode).setActive(true);
newNode = node.appendSibling(newNodeData);
}
else if (target === 'into') {
if (!node.getChildren() && node.isFolder()) {
@@ -696,10 +698,10 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
await node.setExpanded();
}
else {
node.addChildren(newNode);
node.addChildren(newNodeData);
}
await node.getLastChild().setActive(true);
newNode = node.getLastChild();
const parentNoteEntity = await treeCache.getNote(node.data.noteId);
@@ -711,14 +713,18 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
toastService.throwError("Unrecognized target: " + target);
}
if (extraOptions.activate) {
await newNode.setActive(true);
}
clearSelectedNodes(); // to unmark previously active node
// need to refresh because original doesn't have methods like .getParent()
newNode = getNodesByNoteId(branchEntity.noteId)[0];
newNodeData = getNodesByNoteId(branchEntity.noteId)[0];
// following for cycle will make sure that also clones of a parent are refreshed
for (const newParentNode of getNodesByNoteId(parentNoteId)) {
if (newParentNode.key === newNode.getParent().key) {
if (newParentNode.key === newNodeData.getParent().key) {
// we've added a note into this one so no need to refresh
continue;
}
@@ -756,7 +762,7 @@ async function sortAlphabetically(noteId) {
async function showTree() {
const tree = await loadTree();
initFancyTree(tree);
await initFancyTree(tree);
}
ws.subscribeToMessages(message => {
@@ -789,7 +795,7 @@ ws.subscribeToOutsideSyncMessages(async syncData => {
syncData.filter(sync => sync.entityName === 'attributes').forEach(sync => {
const note = treeCache.notes[sync.noteId];
if (note && note.attributeCache) {
if (note && note.__attributeCache) {
noteIdsToRefresh.add(sync.entityId);
}
});
@@ -799,7 +805,7 @@ ws.subscribeToOutsideSyncMessages(async syncData => {
}
});
utils.bindGlobalShortcut('ctrl+o', async () => {
keyboardActionService.setGlobalActionHandler('CreateNoteAfter', async () => {
const node = getActiveNode();
const parentNoteId = node.data.parentNoteId;
const isProtected = await treeUtils.getParentProtectedStatus(node);
@@ -863,7 +869,7 @@ async function reloadNotes(noteIds, activateNotePath = null) {
if (activateNotePath) {
const node = await getNodeFromPath(activateNotePath);
if (node) {
if (node && !node.isActive()) {
await node.setActive(true);
}
}
@@ -871,9 +877,9 @@ async function reloadNotes(noteIds, activateNotePath = null) {
window.glob.createNoteInto = createNoteInto;
utils.bindGlobalShortcut('ctrl+p', createNoteInto);
keyboardActionService.setGlobalActionHandler('CreateNoteInto', createNoteInto);
utils.bindGlobalShortcut('ctrl+.', scrollToActiveNote);
keyboardActionService.setGlobalActionHandler('ScrollToActiveNote', scrollToActiveNote);
$(window).bind('hashchange', async function() {
if (isNotePathInAddress()) {
@@ -890,7 +896,7 @@ $tree.on('mousedown', '.fancytree-title', e => {
treeUtils.getNotePath(node).then(notePath => {
if (notePath) {
noteDetailService.openInTab(notePath);
noteDetailService.openInTab(notePath, false);
}
});
@@ -911,7 +917,7 @@ async function duplicateNote(noteId, parentNoteId) {
}
utils.bindGlobalShortcut('alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
keyboardActionService.setGlobalActionHandler('CollapseTree', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
$collapseTreeButton.on('click', () => collapseTree());
$createTopLevelNoteButton.on('click', createNewTopLevelNote);
@@ -41,7 +41,10 @@ const NOTE_TYPE_ICONS = {
async function getIcon(note) {
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
if (note.noteId === 'root') {
if (note.iconClass) {
return note.iconClass;
}
else if (note.noteId === 'root') {
return "bx bx-chevrons-right";
}
else if (note.noteId === hoistedNoteId) {
@@ -62,6 +65,11 @@ async function getIcon(note) {
async function prepareNode(branch) {
const note = await branch.getNote();
if (!note) {
throw new Error(`Branch has no note ` + branch.noteId);
}
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
+11 -2
View File
@@ -107,6 +107,10 @@ class TreeCache {
if (note.type === 'search') {
const searchResults = await server.get('search-note/' + note.noteId);
if (!searchResults) {
throw new Error(`Search note ${note.noteId} failed.`);
}
// force to load all the notes at once instead of one by one
await treeCache.getNotes(searchResults.map(res => res.noteId));
@@ -129,7 +133,7 @@ class TreeCache {
/** @return {Promise<NoteShort[]>} */
async getNotes(noteIds, silentNotFoundError = false) {
const missingNoteIds = noteIds.filter(noteId => this.notes[noteId] === undefined);
const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]);
if (missingNoteIds.length > 0) {
await this.reloadNotes(missingNoteIds);
@@ -144,7 +148,7 @@ class TreeCache {
else {
return this.notes[noteId];
}
}).filter(note => note !== null);
}).filter(note => !!note);
}
/** @return {Promise<boolean>} */
@@ -157,6 +161,11 @@ class TreeCache {
/** @return {Promise<NoteShort>} */
async getNote(noteId, silentNotFoundError = false) {
if (noteId === 'none') {
console.log(`No 'none' note.`);
return null;
}
else if (!noteId) {
console.log(`Falsy noteId ${noteId}, returning null.`);
return null;
}
@@ -9,6 +9,7 @@ import hoistedNoteService from './hoisted_note.js';
import noteDetailService from './note_detail.js';
import clipboard from './clipboard.js';
import protectedSessionHolder from "./protected_session_holder.js";
import searchNotesService from "./search_notes.js";
class TreeContextMenu {
constructor(node) {
@@ -41,51 +42,54 @@ class TreeContextMenu {
|| (selNodes.length === 1 && selNodes[0] === this.node);
const notSearch = note.type !== 'search';
const parentNotSearch = parentNote.type !== 'search';
const parentNotSearch = !parentNote || parentNote.type !== 'search';
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
return [
{ title: "Open in new tab", cmd: "openInTab", uiIcon: "empty", enabled: noSelectedNotes },
{ title: "Insert note after <kbd>Ctrl+O</kbd>", cmd: "insertNoteAfter", uiIcon: "plus",
{ title: 'Open in new tab', cmd: "openInTab", uiIcon: "empty", enabled: noSelectedNotes },
{ title: 'Insert note after <kbd data-kb-action="CreateNoteAfter"></kbd>', cmd: "insertNoteAfter", uiIcon: "plus",
items: insertNoteAfterEnabled ? this.getNoteTypeItems("insertNoteAfter") : null,
enabled: insertNoteAfterEnabled && noSelectedNotes },
{ title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "plus",
{ title: 'Insert child note <kbd data-kb-action="CreateNoteInto"></kbd>', cmd: "insertChildNote", uiIcon: "plus",
items: notSearch ? this.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes },
{ title: "Delete <kbd>Delete</kbd>", cmd: "delete", uiIcon: "trash",
{ title: 'Delete <kbd data-kb-action="DeleteNotes"></kbd>', cmd: "delete", uiIcon: "trash",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: "----" },
isHoisted ? null : { title: "Hoist note <kbd>Ctrl-H</kbd>", cmd: "hoist", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: "Unhoist note <kbd>Ctrl-H</kbd>", cmd: "unhoist", uiIcon: "arrow-up" },
{ title: "Edit branch prefix <kbd>F2</kbd>", cmd: "editBranchPrefix", uiIcon: "empty",
{ title: 'Search in subtree <kbd data-kb-action="SearchInSubtree"></kbd>', cmd: "searchInSubtree", uiIcon: "search",
enabled: notSearch && noSelectedNotes },
isHoisted ? null : { title: 'Hoist note <kbd data-kb-action="ToggleNoteHoisting"></kbd>', cmd: "hoist", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-kb-action="ToggleNoteHoisting"></kbd>', cmd: "unhoist", uiIcon: "arrow-up" },
{ title: 'Edit branch prefix <kbd data-kb-action="EditBranchPrefix"></kbd>', cmd: "editBranchPrefix", uiIcon: "empty",
enabled: isNotRoot && parentNotSearch && noSelectedNotes},
{ title: "----" },
{ title: "Protect subtree", cmd: "protectSubtree", uiIcon: "check-shield", enabled: noSelectedNotes },
{ title: "Unprotect subtree", cmd: "unprotectSubtree", uiIcon: "shield", enabled: noSelectedNotes },
{ title: "----" },
{ title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "copy",
{ title: 'Copy / clone <kbd data-kb-action="CopyNotesToClipboard"></kbd>', cmd: "copy", uiIcon: "copy",
enabled: isNotRoot && !isHoisted },
{ title: "Clone to ... <kbd>Ctrl+Shift+C</kbd>", cmd: "cloneTo", uiIcon: "empty",
{ title: 'Clone to ... <kbd data-kb-action="CloneNotesTo"></kbd>', cmd: "cloneTo", uiIcon: "empty",
enabled: isNotRoot && !isHoisted },
{ title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "cut",
{ title: 'Cut <kbd data-kb-action="CutNotesToClipboard"></kbd>', cmd: "cut", uiIcon: "cut",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: "Move to ... <kbd>Ctrl+Shift+X</kbd>", cmd: "moveTo", uiIcon: "empty",
{ title: 'Move to ... <kbd data-kb-action="MoveNotesTo"></kbd>', cmd: "moveTo", uiIcon: "empty",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "paste",
enabled: !clipboard.isEmpty() && notSearch && noSelectedNotes },
{ title: "Paste after", cmd: "pasteAfter", uiIcon: "paste",
enabled: !clipboard.isEmpty() && isNotRoot && parentNotSearch && noSelectedNotes },
{ title: 'Paste into <kbd data-kb-action="PasteNotesFromClipboard"></kbd>', cmd: "pasteInto", uiIcon: "paste",
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
{ title: 'Paste after', cmd: "pasteAfter", uiIcon: "paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
{ title: "Duplicate note here", cmd: "duplicateNote", uiIcon: "empty",
enabled: noSelectedNotes && parentNotSearch && (!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) },
enabled: noSelectedNotes && parentNotSearch && isNotRoot && !isHoisted && (!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) },
{ title: "----" },
{ title: "Export", cmd: "export", uiIcon: "empty",
enabled: notSearch && noSelectedNotes },
{ title: "Import into note", cmd: "importIntoNote", uiIcon: "empty",
enabled: notSearch && noSelectedNotes },
{ title: "----" },
{ title: "Collapse subtree <kbd>Alt+-</kbd>", cmd: "collapseSubtree", uiIcon: "align-justify", enabled: noSelectedNotes },
{ title: "Force note sync", cmd: "forceNoteSync", uiIcon: "recycle", enabled: noSelectedNotes },
{ title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: "empty", enabled: noSelectedNotes && notSearch }
{ title: "Advanced", uiIcon: "empty", enabled: true, items: [
{ title: 'Collapse subtree <kbd data-kb-action="CollapseSubtree"></kbd>', cmd: "collapseSubtree", uiIcon: "align-justify", enabled: noSelectedNotes },
{ title: "Force note sync", cmd: "forceNoteSync", uiIcon: "recycle", enabled: noSelectedNotes },
{ title: 'Sort alphabetically <kbd data-kb-action="SortChildNotes"></kbd>', cmd: "sortAlphabetically", uiIcon: "empty", enabled: noSelectedNotes && notSearch }
] },
].filter(row => row !== null);
}
@@ -93,7 +97,7 @@ class TreeContextMenu {
if (cmd === 'openInTab') {
const notePath = await treeUtils.getNotePath(this.node);
noteDetailService.openInTab(notePath);
noteDetailService.openInTab(notePath, false);
}
else if (cmd.startsWith("insertNoteAfter")) {
const parentNoteId = this.node.data.parentNoteId;
@@ -177,6 +181,9 @@ class TreeContextMenu {
treeService.duplicateNote(this.node.data.noteId, branch.parentNoteId);
}
else if (cmd === "searchInSubtree") {
searchNotesService.searchInSubtree(this.node.data.noteId);
}
else {
ws.logError("Unknown command: " + cmd);
}
@@ -3,149 +3,10 @@ import treeChangesService from "./branches.js";
import treeService from "./tree.js";
import hoistedNoteService from "./hoisted_note.js";
import clipboard from "./clipboard.js";
import treeCache from "./tree_cache.js";
import utils from "./utils.js";
import keyboardActionService from "./keyboard_actions.js";
const keyBindings = {
"del": node => {
treeChangesService.deleteNodes(treeService.getSelectedOrActiveNodes(node));
},
"ctrl+up": node => {
const beforeNode = node.getPrevSibling();
if (beforeNode !== null) {
treeChangesService.moveBeforeNode([node], beforeNode);
}
return false;
},
"ctrl+down": node => {
const afterNode = node.getNextSibling();
if (afterNode !== null) {
treeChangesService.moveAfterNode([node], afterNode);
}
return false;
},
"ctrl+left": node => {
treeChangesService.moveNodeUpInHierarchy(node);
return false;
},
"ctrl+right": node => {
const toNode = node.getPrevSibling();
if (toNode !== null) {
treeChangesService.moveToNode([node], toNode);
}
return false;
},
"shift+up": () => {
const node = treeService.getFocusedNode();
if (!node) {
return;
}
if (node.isActive()) {
node.setSelected(true);
}
node.navigate($.ui.keyCode.UP, false).then(() => {
const currentNode = treeService.getFocusedNode();
if (currentNode.isSelected()) {
node.setSelected(false);
}
currentNode.setSelected(true);
});
return false;
},
"shift+down": () => {
const node = treeService.getFocusedNode();
if (!node) {
return;
}
if (node.isActive()) {
node.setSelected(true);
}
node.navigate($.ui.keyCode.DOWN, false).then(() => {
const currentNode = treeService.getFocusedNode();
if (currentNode.isSelected()) {
node.setSelected(false);
}
currentNode.setSelected(true);
});
return false;
},
"f2": async node => {
const editBranchPrefixDialog = await import("../dialogs/branch_prefix.js");
editBranchPrefixDialog.showDialog(node);
},
"alt+-": node => {
treeService.collapseTree(node);
},
"alt+s": node => {
treeService.sortAlphabetically(node.data.noteId);
return false;
},
"ctrl+a": node => {
for (const child of node.getParent().getChildren()) {
child.setSelected(true);
}
return false;
},
"ctrl+c": node => {
clipboard.copy(treeService.getSelectedOrActiveNodes(node));
return false;
},
"ctrl+x": node => {
clipboard.cut(treeService.getSelectedOrActiveNodes(node));
return false;
},
"ctrl+v": node => {
clipboard.pasteInto(node);
return false;
},
"return": node => {
noteDetailService.focusOnTitle();
return false;
},
"backspace": async node => {
if (!await hoistedNoteService.isRootNode(node)) {
node.getParent().setActive().then(treeService.clearSelectedNodes);
}
},
"ctrl+h": node => {
hoistedNoteService.getHoistedNoteId().then(async hoistedNoteId => {
if (node.data.noteId === hoistedNoteId) {
hoistedNoteService.unhoist();
}
else {
const note = await treeCache.getNote(node.data.noteId);
if (note.type !== 'search') {
hoistedNoteService.setHoistedNoteId(node.data.noteId);
}
}
});
return false;
},
const fixedKeyBindings = {
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
// after opening context menu, standard shortcuts don't work, but they are detected here
// so we essentially takeover the standard handling with our implementation.
@@ -171,4 +32,149 @@ const keyBindings = {
}
};
export default keyBindings;
const templates = {
"DeleteNotes": node => {
treeChangesService.deleteNodes(treeService.getSelectedOrActiveNodes(node));
},
"MoveNoteUp": node => {
const beforeNode = node.getPrevSibling();
if (beforeNode !== null) {
treeChangesService.moveBeforeNode([node], beforeNode);
}
return false;
},
"MoveNoteDown": node => {
const afterNode = node.getNextSibling();
if (afterNode !== null) {
treeChangesService.moveAfterNode([node], afterNode);
}
return false;
},
"MoveNoteUpInHierarchy": node => {
treeChangesService.moveNodeUpInHierarchy(node);
return false;
},
"MoveNoteDownInHierarchy": node => {
const toNode = node.getPrevSibling();
if (toNode !== null) {
treeChangesService.moveToNode([node], toNode);
}
return false;
},
"AddNoteAboveToSelection": () => {
const node = treeService.getFocusedNode();
if (!node) {
return;
}
if (node.isActive()) {
node.setSelected(true);
}
const prevSibling = node.getPrevSibling();
if (prevSibling) {
prevSibling.setActive(true, {noEvents: true});
if (prevSibling.isSelected()) {
node.setSelected(false);
}
prevSibling.setSelected(true);
}
return false;
},
"AddNoteBelowToSelection": () => {
const node = treeService.getFocusedNode();
if (!node) {
return;
}
if (node.isActive()) {
node.setSelected(true);
}
const nextSibling = node.getNextSibling();
if (nextSibling) {
nextSibling.setActive(true, {noEvents: true});
if (nextSibling.isSelected()) {
node.setSelected(false);
}
nextSibling.setSelected(true);
}
return false;
},
"CollapseSubtree": node => {
treeService.collapseTree(node);
},
"SortChildNotes": node => {
treeService.sortAlphabetically(node.data.noteId);
return false;
},
"SelectAllNotesInParent": node => {
for (const child of node.getParent().getChildren()) {
child.setSelected(true);
}
return false;
},
"CopyNotesToClipboard": node => {
clipboard.copy(treeService.getSelectedOrActiveNodes(node));
return false;
},
"CutNotesToClipboard": node => {
clipboard.cut(treeService.getSelectedOrActiveNodes(node));
return false;
},
"PasteNotesFromClipboard": node => {
clipboard.pasteInto(node);
return false;
},
"EditNoteTitle": node => {
noteDetailService.focusOnTitle();
return false;
},
"ActivateParentNote": async node => {
if (!await hoistedNoteService.isRootNode(node)) {
node.getParent().setActive().then(treeService.clearSelectedNodes);
}
}
};
async function getKeyboardBindings() {
const bindings = Object.assign({}, fixedKeyBindings);
for (const actionName in templates) {
const action = await keyboardActionService.getAction(actionName);
for (const shortcut of action.effectiveShortcuts || []) {
const normalizedShortcut = utils.normalizeShortcut(shortcut);
bindings[normalizedShortcut] = templates[actionName];
}
}
return bindings;
}
export default {
getKeyboardBindings
};
+30 -6
View File
@@ -137,10 +137,7 @@ function bindGlobalShortcut(keyboardShortcut, handler) {
function bindElShortcut($el, keyboardShortcut, handler) {
if (isDesktop()) {
if (isMac()) {
// use CMD (meta) instead of CTRL for all shortcuts
keyboardShortcut = keyboardShortcut.replace("ctrl", "meta");
}
keyboardShortcut = normalizeShortcut(keyboardShortcut);
$el.bind('keydown', keyboardShortcut, e => {
handler(e);
@@ -151,6 +148,18 @@ function bindElShortcut($el, keyboardShortcut, handler) {
}
}
/**
* Normalize to the form expected by the jquery.hotkeys.js
*/
function normalizeShortcut(shortcut) {
return shortcut
.toLowerCase()
.replace("enter", "return")
.replace("delete", "del")
.replace("ctrl+alt", "alt+ctrl")
.replace("meta+alt", "alt+meta"); // alt needs to be first;
}
function isMobile() {
return window.device === "mobile"
// window.device is not available in setup
@@ -214,6 +223,20 @@ async function clearBrowserCache() {
}
}
/**
* @param url - should be without initial slash!!!
*/
function getUrlForDownload(url) {
if (isElectron()) {
// electron needs absolute URL so we extract current host, port, protocol
return getHost() + '/' + url;
}
else {
// web server can be deployed on subdomain so we need to use relative path
return url;
}
}
export default {
reloadApp,
parseDate,
@@ -230,7 +253,6 @@ export default {
escapeHtml,
stopWatch,
formatLabel,
getHost,
download,
toObject,
randomString,
@@ -245,5 +267,7 @@ export default {
getMimeTypeClass,
closeActiveDialog,
isHtmlEmpty,
clearBrowserCache
clearBrowserCache,
getUrlForDownload,
normalizeShortcut
};

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