Compare commits

...

154 Commits

Author SHA1 Message Date
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
a9702aa6a2 fix empty checkbox visibility, closes #775 2019-12-21 12:37:24 +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
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
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
c141f4b2c0 tab row styling change 2019-11-17 10:22:26 +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
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
13c0411533 refactoring of note creation APIs WIP 2019-11-16 11:09:52 +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
146 changed files with 10637 additions and 6233 deletions

View File

@@ -1,6 +1,6 @@
<?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>
</root>
@@ -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>
</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>

View File

@@ -1,4 +1,4 @@
FROM node:12.13.0-alpine
FROM node:12.13.1-alpine
# Create app directory
WORKDIR /usr/src/app

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.13.1
if [ "$1" != "DONTCOPY" ]
then

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

Binary file not shown.

View File

@@ -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`);

View File

@@ -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';

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);

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,

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>

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}
}

View File

@@ -142,6 +142,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;
@@ -220,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);
}
/**
@@ -261,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();
}
}
@@ -282,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);
}
/**
@@ -290,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);
}
/**
@@ -298,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);
}
/**
@@ -321,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);
}
/**
@@ -330,6 +367,7 @@ class Note extends Entity {
*/
invalidateAttributeCache() {
this.__attributeCache = null;
this.__ownedAttributeCache = null;
}
/** @returns {Promise&lt;void>} */
@@ -339,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
@@ -362,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);
@@ -405,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
@@ -419,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);
@@ -427,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.
*
@@ -454,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) {
@@ -488,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)) {
@@ -500,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)
@@ -546,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.
*
@@ -699,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]);
}
/**
@@ -776,6 +910,16 @@ class Note extends Entity {
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;
@@ -815,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;
}
}

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>

View File

@@ -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.
*

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

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>

View File

@@ -1220,7 +1220,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line220">line 220</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line227">line 227</a>
</li></ul></dd>
@@ -1278,7 +1278,7 @@
<h4 class="name" id="getAttributes"><span class="type-signature">(async) </span>getAttributes<span class="signature">(name<span class="signature-attributes">opt</span>)</span><span class="type-signature"> &rarr; {Promise.&lt;Array.&lt;<a href="Attribute.html">Attribute</a>>>}</span></h4>
<h4 class="name" id="getAttributes"><span class="type-signature">(async) </span>getAttributes<span class="signature">(type<span class="signature-attributes">opt</span>, name<span class="signature-attributes">opt</span>)</span><span class="type-signature"> &rarr; {Promise.&lt;Array.&lt;<a href="Attribute.html">Attribute</a>>>}</span></h4>
@@ -1318,6 +1318,39 @@
<tbody>
<tr>
<td class="name"><code>type</code></td>
<td class="type">
<span class="param-type">string</span>
</td>
<td class="attributes">
&lt;optional><br>
</td>
<td class="description last">(optional) attribute type to filter</td>
</tr>
<tr>
<td class="name"><code>name</code></td>
@@ -1346,7 +1379,7 @@
<td class="description last">attribute name to filter</td>
<td class="description last">(optional) attribute name to filter</td>
</tr>
@@ -1387,7 +1420,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line160">line 160</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line161">line 161</a>
</li></ul></dd>
@@ -1415,6 +1448,10 @@
<h5>Returns:</h5>
<div class="param-desc">
all note's attributes, including inherited ones
</div>
<dl>
@@ -1561,7 +1598,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line231">line 231</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line238">line 238</a>
</li></ul></dd>
@@ -2124,7 +2161,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line253">line 253</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line260">line 260</a>
</li></ul></dd>
@@ -2291,7 +2328,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line186">line 186</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line193">line 193</a>
</li></ul></dd>
@@ -2458,7 +2495,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line178">line 178</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line185">line 185</a>
</li></ul></dd>
@@ -2613,7 +2650,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line265">line 265</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line272">line 272</a>
</li></ul></dd>
@@ -2972,7 +3009,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line259">line 259</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line266">line 266</a>
</li></ul></dd>
@@ -3139,7 +3176,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line202">line 202</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line209">line 209</a>
</li></ul></dd>
@@ -3306,7 +3343,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line194">line 194</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line201">line 201</a>
</li></ul></dd>
@@ -3461,7 +3498,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line277">line 277</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line284">line 284</a>
</li></ul></dd>
@@ -3631,7 +3668,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line287">line 287</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line294">line 294</a>
</li></ul></dd>
@@ -3782,7 +3819,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line271">line 271</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line278">line 278</a>
</li></ul></dd>
@@ -3892,7 +3929,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line311">line 311</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line318">line 318</a>
</li></ul></dd>
@@ -4066,7 +4103,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line211">line 211</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line218">line 218</a>
</li></ul></dd>
@@ -4323,7 +4360,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line241">line 241</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line248">line 248</a>
</li></ul></dd>
@@ -4478,7 +4515,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line247">line 247</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line254">line 254</a>
</li></ul></dd>
@@ -4536,7 +4573,7 @@
<h4 class="name" id="invalidateAttributeCache"><span class="type-signature"></span>invalidateAttributeCache<span class="signature">()</span><span class="type-signature"></span></h4>
<h4 class="name" id="invalidate__attributeCache"><span class="type-signature"></span>invalidate__attributeCache<span class="signature">()</span><span class="type-signature"></span></h4>
@@ -4589,7 +4626,7 @@ Cache is note instance scoped.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line302">line 302</a>
<a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line309">line 309</a>
</li></ul></dd>

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 */

View File

@@ -182,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();
}
}
@@ -204,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);
}
/**
@@ -212,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);
}
/**
@@ -220,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);
}
/**
@@ -228,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);
}
/**
@@ -327,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;
}
/**
@@ -349,7 +356,7 @@ class NoteShort {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.archived;
delete dto.attributeCache;
delete dto.__attributeCache;
return dto;
}

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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -8,6 +8,7 @@ const cls = require('./src/services/cls');
const url = require("url");
const port = require('./src/services/port');
const env = require('./src/services/env');
const keyboardActionsService = require('./src/services/keyboard_actions');
const appIconService = require('./src/services/app_icon');
const windowStateKeeper = require('electron-window-state');
@@ -95,21 +96,44 @@ app.on('activate', () => {
}
});
async function registerGlobalShortcuts() {
await sqlInit.dbReady;
const allActions = await keyboardActionsService.getKeyboardActions();
for (const action of allActions) {
if (!action.effectiveShortcuts) {
continue;
}
for (const shortcut of action.effectiveShortcuts) {
if (shortcut.startsWith('global:')) {
const translatedShortcut = shortcut.substr(7);
const result = globalShortcut.register(translatedShortcut, cls.wrap(async () => {
// window may be hidden / not in focus
mainWindow.focus();
mainWindow.webContents.send('globalShortcut', action.actionName);
}));
if (result) {
log.info(`Registered global shortcut ${translatedShortcut} for action ${action.actionName}`);
}
else {
log.info(`Could not register global shortcut ${translatedShortcut}`);
}
}
}
}
}
app.on('ready', async () => {
app.setAppUserModelId('com.github.zadam.trilium');
mainWindow = await createMainWindow();
const result = globalShortcut.register('CommandOrControl+Alt+P', cls.wrap(async () => {
// window may be hidden / not in focus
mainWindow.focus();
mainWindow.webContents.send('create-day-sub-note');
}));
if (!result) {
log.error("Could not register global shortcut CTRL+ALT+P");
}
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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -498,7 +498,7 @@
t._started = false;
onRenderStop();
} else {
setImmediate(step);
requestIdleCallback(step, { timeout: 10 });
}
}

4501
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.37.2",
"version": "0.38.3",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -29,11 +29,11 @@
"csurf": "1.10.0",
"dayjs": "1.8.17",
"debug": "4.1.1",
"ejs": "2.7.1",
"ejs": "2.7.4",
"electron-debug": "3.0.1",
"electron-dl": "1.14.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",
@@ -45,16 +45,16 @@
"http-proxy-agent": "2.1.0",
"https-proxy-agent": "3.0.1",
"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",
@@ -68,7 +68,7 @@
"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",
@@ -79,18 +79,10 @@
"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"
},
"xo": {
"envs": [
"node",
"browser"
]
"lorem-ipsum": "2.0.3"
}
}

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;
}
}

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]);
}
/**
@@ -748,6 +882,16 @@ class Note extends Entity {
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;
@@ -787,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;
}
}

View File

@@ -30,6 +30,7 @@ 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";
window.glob.isDesktop = utils.isDesktop;
window.glob.isMobile = utils.isMobile;
@@ -78,7 +79,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 +109,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 +127,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;
}
@@ -154,7 +145,11 @@ $noteTabContainer.on("click", ".print-note-button", async function () {
loadCSS: "libraries/codemirror/codemirror.css",
debug: true
});
});
}
keyboardActionService.setGlobalActionHandler("PrintActiveNote", printActiveNote);
$noteTabContainer.on("click", ".print-note-button", printActiveNote);
$('[data-toggle="tooltip"]').tooltip({
html: true

View File

@@ -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);

View File

@@ -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);

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);
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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 => {

View File

@@ -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.");
});
}
}

View File

@@ -131,7 +131,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);

View File

@@ -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))
));
});
});
}
}

View File

@@ -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']);
}
}
}

View File

@@ -93,7 +93,7 @@ export default class SidebarOptions {
this.$sidebarMinWidth.val(options.sidebarMinWidth);
this.$sidebarWidthPercent.val(options.sidebarWidthPercent);
if (parseInt(options.showSidebarInNewTab)) {
if (options.showSidebarInNewTab === 'true') {
this.$showSidebarInNewTab.attr("checked", "checked");
}
else {

View File

@@ -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() {

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"));
}
}

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 */

View File

@@ -154,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();
}
}
@@ -176,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);
}
/**
@@ -184,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);
}
/**
@@ -192,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);
}
/**
@@ -200,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);
}
/**
@@ -299,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;
}
/**
@@ -321,7 +328,7 @@ class NoteShort {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.archived;
delete dto.attributeCache;
delete dto.__attributeCache;
return dto;
}

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
}

View File

@@ -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;

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,112 @@ 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");
});
// hide (toggle) everything except for the note content for zen mode
const toggleZenMode = () => {
$(".hide-in-zen-mode").toggle();
$("#container").toggleClass("zen-mode");
};
// hide (toggle) everything except for the note content for distraction free writing
utils.bindGlobalShortcut('alt+t', e => {
$("#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 +146,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 +168,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 +189,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 +197,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 +213,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 +223,58 @@ 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);
});
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 {

View File

@@ -382,6 +382,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;

View File

@@ -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
};

View File

@@ -0,0 +1,16 @@
class Actions {
constructor() {
this.JUMP_TO = "";
}
}
const actions = new Actions();
function bind() {
}
export default {
actions,
bind
};

View File

@@ -6,12 +6,7 @@ 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) {
@@ -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,20 +166,17 @@ $(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,

View File

@@ -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
}
}

View File

@@ -8,6 +8,7 @@ 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");
@@ -34,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) {
@@ -273,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]);
}
}
@@ -419,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);

View File

@@ -185,8 +185,7 @@ class NoteDetailBook {
}
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";
return utils.getUrlForDownload("api/notes/" + note.noteId + "/download");
}
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');

View File

@@ -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() {

View File

@@ -87,8 +87,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() {}

View File

@@ -98,8 +98,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() {}

View File

@@ -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();
}
});
}

View File

@@ -1,6 +1,41 @@
import libraryLoader from "./library_loader.js";
import treeService from './tree.js';
import noteAutocompleteService from './note_autocomplete.js';
import mimeTypesService from './mime_types.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 {
/**
@@ -33,6 +68,16 @@ class NoteDetailText {
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,40 +88,9 @@ 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
}
});
@@ -84,11 +98,14 @@ class NoteDetailText {
}
}
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() {

View File

@@ -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]);

View File

@@ -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
};

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
};

View File

@@ -246,11 +246,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') {

View File

@@ -29,6 +29,7 @@ const tabTemplate = `
</div>`;
const newTabButtonTemplate = `<div class="note-new-tab" title="Add new tab">+</div>`;
const fillerTemplate = `<div class="tab-row-filler"></div>`;
class TabRow {
constructor(el) {
@@ -40,9 +41,10 @@ class TabRow {
this.setupStyleEl();
this.setupEvents();
this.layoutTabs();
this.setupDraggabilly();
this.setupNewButton();
this.setupFiller();
this.layoutTabs();
this.setVisibility();
}
@@ -109,12 +111,17 @@ class TabRow {
const widths = [];
let extraWidthRemaining = totalExtraWidthDueToFlooring;
for (let i = 0; i < numberOfTabs; i += 1) {
const extraWidth = flooredClampedTargetWidth < TAB_CONTENT_MAX_WIDTH && extraWidthRemaining > 0 ? 1 : 0;
widths.push(flooredClampedTargetWidth + extraWidth);
if (extraWidthRemaining > 0) extraWidthRemaining -= 1;
}
if (this.fillerEl) {
this.fillerEl.style.width = extraWidthRemaining + "px";
}
return widths;
}
@@ -129,8 +136,9 @@ class TabRow {
});
const newTabPosition = position;
const fillerPosition = position + 32;
return {tabPositions, newTabPosition};
return {tabPositions, newTabPosition, fillerPosition};
}
layoutTabs() {
@@ -151,13 +159,14 @@ class TabRow {
let styleHTML = '';
const {tabPositions, newTabPosition} = this.getTabPositions();
const {tabPositions, newTabPosition, fillerPosition} = this.getTabPositions();
tabPositions.forEach((position, i) => {
styleHTML += `.note-tab:nth-child(${ i + 1 }) { transform: translate3d(${ position }px, 0, 0)} `;
});
styleHTML += `.note-new-tab { transform: translate3d(${ newTabPosition }px, 0, 0) } `;
styleHTML += `.tab-row-filler { transform: translate3d(${ fillerPosition }px, 0, 0) } `;
this.styleEl.innerHTML = styleHTML;
}
@@ -387,11 +396,18 @@ class TabRow {
this.newTabEl = div.firstElementChild;
this.tabContentEl.appendChild(this.newTabEl);
this.layoutTabs();
this.newTabEl.addEventListener('click', _ => this.emit('newTab'));
}
setupFiller() {
const div = document.createElement('div');
div.innerHTML = fillerTemplate;
this.fillerEl = div.firstElementChild;
this.tabContentEl.appendChild(this.fillerEl);
}
closest(value, array) {
let closest = Infinity;
let closestIndex = -1;

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");
@@ -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);

View File

@@ -65,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();

View File

@@ -133,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);
@@ -161,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;
}

View File

@@ -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);
}

View File

@@ -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
};

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
};

View File

@@ -1,5 +1,6 @@
import utils from './utils.js';
import toastService from "./toast.js";
import server from "./server.js";
const $outstandingSyncsCount = $("#outstanding-syncs-count");
@@ -8,7 +9,8 @@ const outsideSyncMessageHandlers = [];
const messageHandlers = [];
let ws;
let lastSyncId = window.glob.maxSyncIdAtLoad;
let lastAcceptedSyncId = window.glob.maxSyncIdAtLoad;
let lastProcessedSyncId = window.glob.maxSyncIdAtLoad;
let lastPingTs;
let syncDataQueue = [];
@@ -57,21 +59,26 @@ async function handleMessage(event) {
syncDataQueue.push(...syncRows);
// we set lastAcceptedSyncId even before sync processing and send ping so that backend can start sending more updates
lastAcceptedSyncId = Math.max(lastAcceptedSyncId, syncRows[syncRows.length - 1].id);
sendPing();
// first wait for all the preceding consumers to finish
while (consumeQueuePromise) {
await consumeQueuePromise;
}
// it's my turn so start it up
consumeQueuePromise = consumeSyncData();
try {
// it's my turn so start it up
consumeQueuePromise = consumeSyncData();
await consumeQueuePromise;
// finish and set to null to signal somebody else can pick it up
consumeQueuePromise = null;
await consumeQueuePromise;
}
finally {
// finish and set to null to signal somebody else can pick it up
consumeQueuePromise = null;
}
}
checkSyncIdListeners();
}
else if (message.type === 'sync-hash-check-failed') {
toastService.showError("Sync check failed!", 60000);
@@ -84,7 +91,7 @@ async function handleMessage(event) {
let syncIdReachedListeners = [];
function waitForSyncId(desiredSyncId) {
if (desiredSyncId <= lastSyncId) {
if (desiredSyncId <= lastProcessedSyncId) {
return Promise.resolve();
}
@@ -97,16 +104,29 @@ function waitForSyncId(desiredSyncId) {
});
}
function waitForMaxKnownSyncId() {
return waitForSyncId(server.getMaxKnownSyncId());
}
function checkSyncIdListeners() {
syncIdReachedListeners
.filter(l => l.desiredSyncId <= lastSyncId)
.filter(l => l.desiredSyncId <= lastProcessedSyncId)
.forEach(l => l.resolvePromise());
syncIdReachedListeners = syncIdReachedListeners
.filter(l => l.desiredSyncId > lastSyncId);
.filter(l => l.desiredSyncId > lastProcessedSyncId);
syncIdReachedListeners.filter(l => Date.now() > l.start - 60000)
.forEach(l => console.log(`Waiting for syncId ${l.desiredSyncId} while current is ${lastSyncId} for ${Math.floor((Date.now() - l.start) / 1000)}s`));
.forEach(l => console.log(`Waiting for syncId ${l.desiredSyncId} while current is ${lastProcessedSyncId} for ${Math.floor((Date.now() - l.start) / 1000)}s`));
}
async function runSafely(syncHandler, syncData) {
try {
return await syncHandler(syncData);
}
catch (e) {
console.log(`Sync handler failed with ${e.message}: ${e.stack}`);
}
}
async function consumeSyncData() {
@@ -116,51 +136,64 @@ async function consumeSyncData() {
const outsideSyncData = allSyncData.filter(sync => sync.sourceId !== glob.sourceId);
// the update process should be synchronous as a whole but individual handlers can run in parallel
await Promise.all([
...allSyncMessageHandlers.map(syncHandler => syncHandler(allSyncData)),
...outsideSyncMessageHandlers.map(syncHandler => syncHandler(outsideSyncData))
]);
try {
// the update process should be synchronous as a whole but individual handlers can run in parallel
await Promise.all([
...allSyncMessageHandlers.map(syncHandler => runSafely(syncHandler, allSyncData)),
...outsideSyncMessageHandlers.map(syncHandler => runSafely(syncHandler, outsideSyncData))
]);
}
catch (e) {
logError(`Encountered error ${e.message}, reloading frontend.`);
lastSyncId = allSyncData[allSyncData.length - 1].id;
// if there's an error in updating the frontend then the easy option to recover is to reload the frontend completely
utils.reloadApp();
}
lastProcessedSyncId = Math.max(lastProcessedSyncId, allSyncData[allSyncData.length - 1].id);
}
checkSyncIdListeners();
}
function connectWebSocket() {
const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
const loc = window.location;
const webSocketUri = (loc.protocol === "https:" ? "wss:" : "ws:")
+ "//" + loc.host + loc.pathname;
// use wss for secure messaging
const ws = new WebSocket(protocol + "://" + location.host);
ws.onopen = () => console.debug(utils.now(), "Connected to server with WebSocket");
const ws = new WebSocket(webSocketUri);
ws.onopen = () => console.debug(utils.now(), `Connected to server ${webSocketUri} with WebSocket`);
ws.onmessage = handleMessage;
// we're not handling ws.onclose here because reconnection is done in sendPing()
return ws;
}
async function sendPing() {
if (Date.now() - lastPingTs > 30000) {
console.log(utils.now(), "Lost websocket connection to the backend");
}
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'ping',
lastSyncId: lastAcceptedSyncId
}));
}
else if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
console.log(utils.now(), "WS closed or closing, trying to reconnect");
ws = connectWebSocket();
}
}
setTimeout(() => {
ws = connectWebSocket();
lastSyncId = glob.maxSyncIdAtLoad;
lastPingTs = Date.now();
setInterval(async () => {
if (Date.now() - lastPingTs > 30000) {
console.log(utils.now(), "Lost connection to server");
}
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'ping',
lastSyncId: lastSyncId
}));
}
else if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
console.log(utils.now(), "WS closed or closing, trying to reconnect");
ws = connectWebSocket();
}
}, 1000);
setInterval(sendPing, 1000);
}, 0);
subscribeToMessages(message => {
@@ -183,5 +216,6 @@ export default {
subscribeToMessages,
subscribeToAllSyncMessages,
subscribeToOutsideSyncMessages,
waitForSyncId
waitForSyncId,
waitForMaxKnownSyncId
};

View File

@@ -76,12 +76,12 @@ function SetupModel() {
}
// not using server.js because it loads too many dependencies
$.post('/api/setup/new-document', {
$.post('api/setup/new-document', {
username: username,
password: password1,
theme: theme
}).then(() => {
window.location.replace("/");
window.location.replace("./");
});
}
else if (this.setupType() === 'sync-from-server') {
@@ -128,10 +128,10 @@ function SetupModel() {
}
async function checkOutstandingSyncs() {
const { stats, initialized } = await $.get('/api/sync/stats');
const { stats, initialized } = await $.get('api/sync/stats');
if (initialized) {
window.location.replace("/");
window.location.replace("./");
}
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls;

View File

@@ -18,7 +18,7 @@ class SimilarNotesWidget extends StandardWidget {
// remember which title was when we found the similar notes
this.title = this.ctx.note.title;
const similarNotes = await server.get('similar_notes/' + this.ctx.note.noteId);
const similarNotes = await server.get('similar-notes/' + this.ctx.note.noteId);
if (similarNotes.length === 0) {
this.$body.text("No similar notes found ...");

View File

@@ -18,7 +18,7 @@ body {
grid-gap: 0;
}
#container.distraction-free-mode {
#container.zen-mode {
grid-template-areas:
"tab-container" !important;
grid-template-rows: auto
@@ -59,7 +59,7 @@ body {
background-color: var(--header-background-color);
display: flex;
align-items: center;
padding: 4px;
padding-top: 4px;
}
#header button {
@@ -68,18 +68,26 @@ body {
margin-bottom: 2px;
margin-top: 2px;
margin-right: 8px;
border-color: transparent !important;
}
#header button.btn-sm .bx {
position: relative;
top: 1px;
}
#header button:hover {
border-color: var(--button-border-color) !important;
}
#history-navigation {
margin: 0 15px 0 5px;
position: relative;
top: 2px;
}
#global-buttons {
display: flex;
justify-content: space-around;
padding: 10px 0 10px 0;
padding: 3px 0 3px 0;
border: 1px solid var(--main-border-color);
border-radius: 7px;
margin: 5px 15px 5px 5px;
@@ -89,6 +97,11 @@ body {
font-size: inherit;
}
#context-menu-container {
max-height: 100vh;
overflow: auto; /* make it scrollable when exceeding total height of the window */
}
#context-menu-container, #context-menu-container .dropdown-menu {
padding: 3px 0 0;
z-index: 1111;
@@ -111,6 +124,7 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
top: 0;
left: 100%;
margin-top: -6px;
min-width: 15rem;
}
/* rotate caret on hover */
@@ -145,7 +159,7 @@ body {
::-webkit-scrollbar-thumb {
border-radius: 3px;
border: 1px solid var(--main-border-color);
border: 1px solid var(--scrollbar-border-color);
}
::-webkit-scrollbar-corner {
@@ -165,6 +179,13 @@ body {
cursor: pointer;
position: relative;
top: -1px;
border: 1px solid transparent;
padding: 2px;
border-radius: 2px;
}
.refresh-search-button:hover {
border-color: var(--button-border-color);
}
.note-title-row {
@@ -215,7 +236,7 @@ body {
.note-new-tab {
position: absolute;
left: 0;
height: 32px;
height: 33px;
width: 32px;
border: 0;
margin: 0;
@@ -223,6 +244,7 @@ body {
text-align: center;
font-size: 24px;
cursor: pointer;
border-bottom: 1px solid var(--button-border-color);
}
.note-new-tab:hover {
@@ -230,6 +252,14 @@ body {
border-radius: 5px;
}
.tab-row-filler {
position: absolute;
left: 0;
background: linear-gradient(to right, var(--button-border-color), transparent);
height: 1px;
margin-top: 32px;
}
.note-tab-row .note-tab[active] {
z-index: 5;
}
@@ -244,6 +274,7 @@ body {
top: 10px;
animation: note-tab-was-just-added 120ms forwards ease-in-out;
}
.note-tab-row .note-tab .note-tab-wrapper {
position: absolute;
display: flex;
@@ -256,11 +287,14 @@ body {
border-top-right-radius: 8px;
overflow: hidden;
pointer-events: all;
background-image: linear-gradient(to bottom, var(--accented-background-color), var(--main-background-color));
background-color: var(--accented-background-color);
border-bottom: 1px solid var(--button-border-color);
}
.note-tab-row .note-tab[active] .note-tab-wrapper {
background-image: linear-gradient(to bottom, var(--more-accented-background-color), var(--main-background-color));
background-color: var(--main-background-color);
border: 1px solid var(--button-border-color);
border-bottom: 0;
font-weight: bold;
}
@@ -268,6 +302,7 @@ body {
padding-left: 2px;
padding-right: 2px;
}
.note-tab-row .note-tab .note-tab-title {
flex: 1;
vertical-align: top;
@@ -275,12 +310,15 @@ body {
white-space: nowrap;
color: var(--muted-text-color);
}
.note-tab-row .note-tab[is-small] .note-tab-title {
margin-left: 0;
}
.note-tab-row .note-tab[active] .note-tab-title {
color: var(--main-text-color);
}
.note-tab-row .note-tab .note-tab-drag-handle {
position: absolute;
top: 0;
@@ -290,6 +328,7 @@ body {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.note-tab-row .note-tab .note-tab-close {
flex-grow: 0;
flex-shrink: 0;
@@ -348,6 +387,14 @@ body {
.hide-sidebar-button {
color: var(--main-text-color);
background: none;
border: 1px solid transparent;
padding: 2px 8px 2px 8px;
border-radius: 2px;
}
.hide-sidebar-button:hover {
border-color: var(--button-border-color);
}
.note-detail-sidebar {
@@ -380,6 +427,8 @@ body {
border: 0;
background: inherit;
font-weight: bold;
text-transform: uppercase;
color: var(--muted-text-color) !important;
}
.note-detail-sidebar .widget-header-action {
@@ -388,7 +437,9 @@ body {
}
.note-detail-sidebar .widget-help {
color: var(--main-text-color);
color: var(--muted-text-color);
position: relative;
top: 2px;
}
.note-detail-sidebar .widget-help.no-link:hover {

View File

@@ -19,7 +19,7 @@ html, body {
display: flex;
flex-shrink: 0;
justify-content: space-around;
padding: 10px 0 10px 0;
padding: 3px 0 3px 0;
margin: 0 10px 0 16px;
}

View File

@@ -75,6 +75,12 @@ ul.fancytree-container {
content: "\e9f8";
}
/** some common text styling for cssClass label */
span.fancytree-node.underline .fancytree-title { text-decoration: underline; }
span.fancytree-node.dotted .fancytree-title { text-decoration: dotted; }
span.fancytree-node.bold .fancytree-title { font-weight: bold; }
span.fancytree-node.muted { opacity: 0.6; }
.note-title[readonly] {
background: inherit;
}
@@ -144,9 +150,9 @@ ul.fancytree-container {
margin-top: 0;
}
/** we disable shield background when in distraction free mode because I couldn't get it to stay static
/** we disable shield background when in zen mode because I couldn't get it to stay static
(it kept growing with content) */
#container:not(.distraction-free-mode) .note-tab-content.protected {
#container:not(.zen-mode) .note-tab-content.protected {
/* DON'T COLLAPSE THE RULES INTO SINGLE ONE, BACKGROUND WON'T DISPLAY */
background: url('../images/shield.svg') no-repeat;
background-size: contain;
@@ -227,9 +233,13 @@ span.fancytree-node.archived {
.icon-action:hover {
text-decoration: none;
border-color: var(--button-border-color);
}
.icon-action {
border: 1px solid transparent;
border-radius: 3px;
padding: 5px;
cursor: pointer;
font-size: 1.5em;
}
@@ -411,11 +421,31 @@ div.ui-tooltip {
height: 150px;
}
#sql-console-query .CodeMirror-scroll {
min-height: inherit !important;
}
.btn {
border-radius: var(--button-border-radius);
}
.btn:not(.btn-primary):not(.btn-secondary):not(.btn-danger) {
.btn.btn-primary {
border-color: var(--primary-button-border-color);
background-color: var(--primary-button-background-color);
color: var(--primary-button-text-color);
}
.btn:not(:disabled):not(.disabled):active, .btn:not(:disabled):not(.disabled).active {
border-color: var(--primary-button-text-color);
background-color: var(--active-item-background-color);
color: var(--active-item-text-color);
}
.btn.btn-primary kbd {
color: var(--primary-button-text-color);
}
.btn:not(.btn-primary) {
border-color: var(--button-border-color);
background-color: var(--button-background-color);
color: var(--button-text-color);
@@ -423,6 +453,7 @@ div.ui-tooltip {
.btn.active:not(.btn-primary) {
background-color: var(--button-disabled-background-color) !important;
opacity: 0.4;
}
.note-path-list a.current {
@@ -460,9 +491,10 @@ button.icon-button {
margin: auto;
/* setting the display to block since "table" doesn't support scrolling */
display: block;
flex-basis: content;
flex-shrink: 1;
/** flex-basis: content; - use once "content" is implemented by chrome */
flex-shrink: 0;
flex-grow: 0;
max-height: 30%;
overflow: auto;
}
@@ -768,7 +800,7 @@ div[data-notify="container"] {
padding: 2px;
}
#sql-console-tables button {
#sql-console-table-schemas button {
padding: 0.25rem 0.4rem;
font-size: 0.875rem;
line-height: 0.5;
@@ -903,4 +935,13 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
font-weight: bold !important;
color: inherit !important;
vertical-align: baseline !important;
}
.ck-content .todo-list .todo-list__label > input:before {
border: 1px solid var(--muted-text-color) !important;
}
.ck-link-actions .ck-tooltip {
/* force hide the tooltip since it shows misleading "open link in new tab */
display: none !important;
}

View File

@@ -10,26 +10,30 @@
--main-text-color: black;
--main-border-color: #ccc;
--accented-background-color: #f5f5f5;
--more-accented-background-color: #ccc;
--header-background-color: #f8f8f8;
--button-background-color: #eee;
--button-disabled-background-color: #ccc;
--more-accented-background-color: #ddd;
--header-background-color: #fff;
--button-background-color: #fff;
--button-disabled-background-color: #ddd;
--button-border-color: #ddd;
--button-text-color: black;
--button-border-radius: 5px;
--muted-text-color: #444;
--primary-button-background-color: #6c757d;
--primary-button-text-color: white;
--primary-button-border-color: #6c757d;
--muted-text-color: #666;
--input-text-color: black;
--input-background-color: white;
--hover-item-text-color: black;
--hover-item-background-color: #eee;
--active-item-text-color: black;
--active-item-background-color: #ccc;
--active-item-background-color: #ddd;
--menu-text-color: black;
--menu-background-color: white;
--tooltip-background-color: #f8f8f8;
--link-color: blue;
--modal-background-color: white;
--modal-backdrop-color: black;
--scrollbar-border-color: #ddd;
}
body.theme-black {
@@ -43,6 +47,9 @@ body.theme-black {
--button-border-color: #444;
--button-text-color: white;
--button-border-radius: 5px;
--primary-button-background-color: #888;
--primary-button-text-color: white;
--primary-button-border-color: #999;
--muted-text-color: #ccc;
--input-text-color: white;
--input-background-color: black;
@@ -56,6 +63,7 @@ body.theme-black {
--link-color: lightskyblue;
--modal-background-color: black;
--modal-backdrop-color: #444;
--scrollbar-border-color: #888;
}
body.theme-black .CodeMirror {
@@ -65,7 +73,7 @@ body.theme-black .CodeMirror {
body.theme-dark {
--main-background-color: #333;
--main-text-color: white;
--main-border-color: #ddd;
--main-border-color: #aaa;
--accented-background-color: #555;
--more-accented-background-color: #777;
--header-background-color: #333;
@@ -73,6 +81,9 @@ body.theme-dark {
--button-border-color: #444;
--button-text-color: white;
--button-border-radius: 5px;
--primary-button-background-color: #888;
--primary-button-text-color: white;
--primary-button-border-color: #999;
--muted-text-color: #ccc;
--input-text-color: white;
--input-background-color: #333;
@@ -86,6 +97,7 @@ body.theme-dark {
--link-color: lightskyblue;
--modal-background-color: #333;
--modal-backdrop-color: #444;
--scrollbar-border-color: #888;
}
body.theme-dark .CodeMirror {
@@ -184,7 +196,7 @@ body {
--ck-color-todo-list-checkmark-border: var(--main-border-color);
--ck-color-engine-placeholder-text: var(--main-text-color);
--ck-color-engine-placeholder-text: var(--muted-text-color);
}
body {

View File

@@ -0,0 +1,15 @@
"use strict";
const fs = require('fs');
const dateUtils = require('../../services/date_utils');
const {LOG_DIR} = require('../../services/data_dir.js');
async function getBackendLog() {
const file = `${LOG_DIR}/trilium-${dateUtils.localNowDate()}.log`;
return fs.readFileSync(file, 'utf8');
}
module.exports = {
getBackendLog
};

View File

@@ -2,6 +2,7 @@
const sql = require('../../services/sql');
const log = require('../../services/log');
const consistencyChecksService = require('../../services/consistency_checks');
async function vacuumDatabase() {
await sql.execute("VACUUM");
@@ -9,6 +10,11 @@ async function vacuumDatabase() {
log.info("Database has been vacuumed.");
}
async function findAndFixConsistencyIssues() {
await consistencyChecksService.runOnDemandChecks(true);
}
module.exports = {
vacuumDatabase
vacuumDatabase,
findAndFixConsistencyIssues
};

View File

@@ -15,7 +15,7 @@ async function findClippingNote(todayNote, pageUrl) {
const notes = await todayNote.getDescendantNotesWithLabel('pageUrl', pageUrl);
for (const note of notes) {
if (await note.getLabelValue('clipType') === 'clippings') {
if (await note.getOwnedLabelValue('clipType') === 'clippings') {
return note;
}
}
@@ -31,7 +31,12 @@ async function addClipping(req) {
let clippingNote = await findClippingNote(todayNote, pageUrl);
if (!clippingNote) {
clippingNote = (await noteService.createNote(todayNote.noteId, title, '')).note;
clippingNote = (await noteService.createNewNote({
parentNoteId: todayNote.noteId,
title: title,
content: '',
type: 'text'
})).note;
await clippingNote.setLabel('clipType', 'clippings');
await clippingNote.setLabel('pageUrl', pageUrl);
@@ -51,7 +56,12 @@ async function createNote(req) {
const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate());
const {note} = await noteService.createNote(todayNote.noteId, title, content);
const {note} = await noteService.createNewNote({
parentNoteId: todayNote.noteId,
title,
content,
type: 'text'
});
await note.setLabel('clipType', clipType);
@@ -88,7 +98,7 @@ async function addImagesToNote(images, note, content) {
noteId: note.noteId,
type: 'relation',
value: imageNote.noteId,
name: 'image-link'
name: 'imageLink'
}).save();
console.log(`Replacing ${imageId} with ${url}`);

22
src/routes/api/keys.js Normal file
View File

@@ -0,0 +1,22 @@
"use strict";
const keyboardActions = require('../../services/keyboard_actions');
const sql = require('../../services/sql');
async function getKeyboardActions() {
return await keyboardActions.getKeyboardActions();
}
async function getShortcutsForNotes() {
return await sql.getMap(`
SELECT value, noteId
FROM attributes
WHERE isDeleted = 0
AND type = 'label'
AND name = 'keyboardShortcut'`);
}
module.exports = {
getKeyboardActions,
getShortcutsForNotes
};

View File

@@ -112,15 +112,21 @@ async function eraseNoteRevision(req) {
}
async function getEditedNotesOnDate(req) {
const date = req.params.date;
const date = utils.sanitizeSql(req.params.date);
const notes = await repository.getEntities(`
select distinct notes.*
from notes
left join note_revisions using (noteId)
where substr(notes.dateCreated, 0, 11) = ?
or substr(notes.dateModified, 0, 11) = ?
or substr(note_revisions.dateLastEdited, 0, 11) = ?`, [date, date, date]);
SELECT notes.*
FROM notes
WHERE noteId IN (
SELECT noteId FROM notes
WHERE notes.dateCreated LIKE '${date}%'
OR notes.dateModified LIKE '${date}%'
UNION ALL
SELECT noteId FROM note_revisions
WHERE note_revisions.dateLastEdited LIKE '${date}%'
)
ORDER BY isDeleted
LIMIT 50`);
for (const note of notes) {
const notePath = noteCacheService.getNotePath(note.noteId);

View File

@@ -27,36 +27,13 @@ async function getNote(req) {
return note;
}
async function getChildren(req) {
const parentNoteId = req.params.parentNoteId;
const parentNote = await repository.getNote(parentNoteId);
if (!parentNote) {
return [404, `Note ${parentNoteId} has not been found.`];
}
const ret = [];
for (const childNote of await parentNote.getChildNotes()) {
ret.push({
noteId: childNote.noteId,
title: childNote.title,
relations: (await childNote.getRelations()).map(relation => { return {
attributeId: relation.attributeId,
name: relation.name,
targetNoteId: relation.value
}; })
});
}
return ret;
}
async function createNote(req) {
const parentNoteId = req.params.parentNoteId;
const newNote = req.body;
const params = Object.assign({}, req.body); // clone
params.parentNoteId = req.params.parentNoteId;
const { note, branch } = await noteService.createNewNote(parentNoteId, newNote, req);
const { target, targetBranchId } = req.query;
const { note, branch } = await noteService.createNewNoteWithTarget(target, targetBranchId, params);
await treeService.setCssClassesToNotes([note]);
@@ -128,7 +105,9 @@ async function getRelationMap(req) {
noteTitles: {},
relations: [],
// relation name => inverse relation name
inverseRelations: {}
inverseRelations: {
'internalLink': 'internalLink'
}
};
if (noteIds.length === 0) {
@@ -194,7 +173,6 @@ module.exports = {
sortNotes,
protectSubtree,
setNoteTypeMime,
getChildren,
getRelationMap,
changeTitle,
duplicateNote

View File

@@ -5,7 +5,7 @@ const log = require('../../services/log');
const attributes = require('../../services/attributes');
// options allowed to be updated directly in options dialog
const ALLOWED_OPTIONS = [
const ALLOWED_OPTIONS = new Set([
'protectedSessionTimeout',
'noteRevisionSnapshotTimeInterval',
'zoomFactor',
@@ -37,23 +37,32 @@ const ALLOWED_OPTIONS = [
'spellCheckLanguageCode',
'imageMaxWidthHeight',
'imageJpegQuality'
];
]);
async function getOptions() {
return await optionService.getOptionsMap(ALLOWED_OPTIONS);
const optionMap = await optionService.getOptionsMap();
const resultMap = {};
for (const optionName in optionMap) {
if (isAllowed(optionName)) {
resultMap[optionName] = optionMap[optionName];
}
}
return resultMap;
}
async function updateOption(req) {
const {name, value} = req.params;
if (!update(name, value)) {
if (!await update(name, value)) {
return [400, "not allowed option to change"];
}
}
async function updateOptions(req) {
for (const optionName in req.body) {
if (!update(optionName, req.body[optionName])) {
if (!await update(optionName, req.body[optionName])) {
// this should be improved
// it should return 400 instead of current 500, but at least it now rollbacks transaction
throw new Error(`${optionName} is not allowed to change`);
@@ -62,7 +71,7 @@ async function updateOptions(req) {
}
async function update(name, value) {
if (!ALLOWED_OPTIONS.includes(name)) {
if (!isAllowed(name)) {
return false;
}
@@ -81,7 +90,7 @@ async function getUserThemes() {
const ret = [];
for (const note of notes) {
let value = await note.getLabelValue('appTheme');
let value = await note.getOwnedLabelValue('appTheme');
if (!value) {
value = note.title.toLowerCase().replace(/[^a-z0-9]/gi, '-');
@@ -97,6 +106,10 @@ async function getUserThemes() {
return ret;
}
function isAllowed(name) {
return ALLOWED_OPTIONS.has(name) || name.startsWith("keyboardShortcuts");
}
module.exports = {
getOptions,
updateOption,

View File

@@ -17,8 +17,8 @@ async function getRecentChanges() {
FROM
note_revisions
JOIN notes USING(noteId)
ORDER BY
utcDateCreated DESC
ORDER BY
note_revisions.utcDateCreated DESC
LIMIT 1000
)
UNION ALL SELECT * FROM (

View File

@@ -26,10 +26,10 @@ async function uploadImage(req) {
async function saveNote(req) {
const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']);
const {note, branch} = await noteService.createNewNote(parentNote.noteId, {
const {note, branch} = await noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: req.body.title,
content: req.body.content,
target: 'into',
isProtected: false,
type: 'text',
mime: 'text/html'

View File

@@ -17,12 +17,22 @@ async function getSchema() {
}
async function execute(req) {
const query = req.body.query;
const queries = req.body.query.split("\n---");
try {
const results = [];
for (const query of queries) {
if (!query.trim()) {
continue;
}
results.push(await sql.getRows(query));
}
return {
success: true,
rows: await sql.getRows(query)
results
};
}
catch (e) {

View File

@@ -10,6 +10,8 @@ async function getNotesAndBranches(noteIds) {
noteIds = notes.map(n => n.noteId);
// joining child note to filter out not completely synchronised notes which would then cause errors later
// cannot do that with parent because of root note's 'none' parent
const branches = await sql.getManyRows(`
SELECT
branches.branchId,
@@ -19,6 +21,7 @@ async function getNotesAndBranches(noteIds) {
branches.prefix,
branches.isExpanded
FROM branches
JOIN notes AS child ON child.noteId = branches.noteId
WHERE branches.isDeleted = 0
AND (branches.noteId IN (???) OR parentNoteId IN (???))`, noteIds);

View File

@@ -12,10 +12,14 @@ function init(app) {
}
};
const respHeaders = {};
const res = {
statusCode: 200,
getHeader: () => {},
setHeader: () => {},
getHeader: name => respHeaders[name],
setHeader: (name, value) => {
respHeaders[name] = value.toString();
},
status: statusCode => {
res.statusCode = statusCode;
return res;
@@ -24,6 +28,7 @@ function init(app) {
event.sender.send('server-response', {
requestId: arg.requestId,
statusCode: res.statusCode,
headers: respHeaders,
body: obj
});
}

View File

@@ -34,6 +34,8 @@ const dateNotesRoute = require('./api/date_notes');
const linkMapRoute = require('./api/link_map');
const clipperRoute = require('./api/clipper');
const similarNotesRoute = require('./api/similar_notes');
const keysRoute = require('./api/keys');
const backendLogRoute = require('./api/backend_log');
const log = require('../services/log');
const express = require('express');
@@ -42,6 +44,7 @@ const auth = require('../services/auth');
const cls = require('../services/cls');
const sql = require('../services/sql');
const protectedSessionService = require('../services/protected_session');
const syncTableService = require('../services/sync_table');
const csurf = require('csurf');
const csrfMiddleware = csurf({
@@ -50,6 +53,8 @@ const csrfMiddleware = csurf({
});
function apiResultHandler(req, res, result) {
res.setHeader('trilium-max-sync-id', syncTableService.getMaxSyncId());
// if it's an array and first element is integer then we consider this to be [statusCode, response] format
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [statusCode, response] = result;
@@ -127,7 +132,6 @@ function register(app) {
apiRoute(PUT, '/api/notes/:noteId', notesApiRoute.updateNote);
apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote);
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
apiRoute(GET, '/api/notes/:parentNoteId/children', notesApiRoute.getChildren);
apiRoute(PUT, '/api/notes/:noteId/sort', notesApiRoute.sortNotes);
apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectSubtree);
apiRoute(PUT, /\/api\/notes\/(.*)\/type\/(.*)\/mime\/(.*)/, notesApiRoute.setNoteTypeMime);
@@ -211,10 +215,11 @@ function register(app) {
apiRoute(POST, '/api/sql/execute', sqlRoute.execute);
apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
apiRoute(POST, '/api/cleanup/cleanup-unused-images', cleanupRoute.cleanupUnusedImages);
// VACUUM requires execution outside of transaction
route(POST, '/api/cleanup/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.vacuumDatabase, apiResultHandler, false);
route(POST, '/api/cleanup/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.findAndFixConsistencyIssues, apiResultHandler, false);
apiRoute(POST, '/api/script/exec', scriptRoute.exec);
apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run);
apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles);
@@ -242,7 +247,12 @@ function register(app) {
route(POST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler);
route(POST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler);
apiRoute(GET, '/api/similar_notes/:noteId', similarNotesRoute.getSimilarNotes);
apiRoute(GET, '/api/similar-notes/:noteId', similarNotesRoute.getSimilarNotes);
apiRoute(GET, '/api/keyboard-actions', keysRoute.getKeyboardActions);
apiRoute(GET, '/api/keyboard-shortcuts-for-notes', keysRoute.getShortcutsForNotes);
apiRoute(GET, '/api/backend-log', backendLogRoute.getBackendLog);
app.use('', router);
}

View File

@@ -18,7 +18,10 @@ async function anonymize() {
await db.run("UPDATE notes SET title = 'title'");
await db.run("UPDATE note_contents SET content = 'text'");
await db.run("UPDATE note_revisions SET title = 'title', content = 'text'");
await db.run("UPDATE note_revisions SET title = 'title'");
await db.run("UPDATE note_revision_contents SET content = 'title'");
await db.run("UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label'");
await db.run("UPDATE attributes SET name = 'name' WHERE type = 'relation'");
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
('documentSecret', 'encryptedDataKey', 'passwordVerificationHash',

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