Compare commits

...

78 Commits

Author SHA1 Message Date
zadam
a0395e9866 release 0.43.1 2020-06-23 10:11:17 +02:00
zadam
89aa4fbc73 electron 9.0.5 2020-06-23 10:10:59 +02:00
zadam
74a7802088 fix custom resource handler, closes #1125 2020-06-22 23:13:53 +02:00
zadam
a89b6711d1 refactored code to not depend on external elements, #1120 2020-06-22 22:28:45 +02:00
zadam
b2549b2834 Merge remote-tracking branch 'origin/stable' into stable 2020-06-22 22:00:22 +02:00
zadam
959c4cbe64 removed icon tooltip again 2020-06-22 22:00:08 +02:00
Shon Ramamurthy
d03d3603d2 Add optional support for note title tooltips under note tree widget (#1120)
* Add support for note title tooltips under note tree widget

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

* Revert DB Upgrade

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

* Simplify boolean option comparison

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

* Display tooltip only on center-pane overlap - Experimental

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

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

* Remove option to set tooltip

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

* Revert further changes related to the options menu

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

* Remove debug logging

Remove debug logging and unnecessary line breaks.

* Move note-title tooltip handler under note_tree.js

With this change, we move the definition for the note-title tooltip
handler inside 'note_tree.js'. Registration is done inside
'side_pane_toggles.js' as we would need the handler to detect the
'center-pane' element first before detecting collisions.
2020-06-22 21:58:58 +02:00
zadam
e1c2573778 add tooltip to fancytree node icon, #1120 2020-06-21 12:47:24 +02:00
zadam
2af2b45062 fix keyboard shortcut for print this note, #1118 2020-06-20 00:04:31 +02:00
zadam
eabe4775bd fix click handler of links inside included note 2020-06-19 23:20:52 +02:00
zadam
da9b321aa0 allow mouse interactions in ckeditor widgets, fixes #1117 2020-06-19 22:24:57 +02:00
zadam
c18d8d2d1b add back lost edited notes widget, closes #1115 2020-06-18 08:56:23 +02:00
zadam
5f2361ebd5 release 0.43.0-beta 2020-06-15 23:26:12 +02:00
zadam
9791dab97d recent changes sorting fixes, closes #1099 2020-06-15 23:22:11 +02:00
zadam
85d986534d fix enforcing node's http requests for sync 2020-06-15 18:24:43 +02:00
zadam
00faf758e8 fixes and polish 2020-06-15 17:56:53 +02:00
zadam
6ba2e5cf73 add word count widget to the demo document (plus cleanup of external links) 2020-06-14 16:29:29 +02:00
zadam
fb3876d28b promoted attr selection using autocomplete will trigger change event to save it, closes #699 2020-06-14 16:04:00 +02:00
zadam
fb975849b9 small API additions 2020-06-14 14:30:57 +02:00
zadam
16fef78344 add API method to force refresh of included notes, closes #1106 2020-06-14 10:50:08 +02:00
zadam
e0b4b369dc transaction handling fixes 2020-06-14 00:35:53 +02:00
zadam
0df7851214 fix sync 2020-06-13 22:34:15 +02:00
zadam
5d47c2b23e opening transactions only on write operations which enforces exclusive lock only there to improve concurrency, custom handling of sync request timeouts, #1093, #1018 2020-06-13 10:23:36 +02:00
zadam
d09b021487 add time limit to frontend update 2020-06-11 00:13:56 +02:00
zadam
910bda860c fix delete note function just work one time, closes #1101 2020-06-10 23:43:59 +02:00
zadam
2d92b4931a fix ctrl+click opening the link twice/thrice, closes #1094 2020-06-10 00:10:27 +02:00
zadam
212b719ee9 better detection of changes in attributes and how they affect notes 2020-06-09 22:59:22 +02:00
zadam
1db892d22f return the ability to hide archived notes, closes #1095 2020-06-08 23:15:49 +02:00
zadam
535dcb6d12 release 0.42.7 2020-06-08 10:43:12 +02:00
zadam
4426362799 cleanup sqlite to make the distributed archives smaller 2020-06-08 10:42:40 +02:00
zadam
2c609e8136 promoted attributes widget is now auto-updating, fixes #700 2020-06-08 00:29:52 +02:00
zadam
11b73b79ed refresh promoted attributes when change detected 2020-06-07 23:57:10 +02:00
zadam
e70c862e72 fix import 2020-06-07 23:55:55 +02:00
zadam
b3e66d5a83 fixed command line anonymization 2020-06-07 10:45:41 +02:00
zadam
e8cd821e57 futrther improvements to anonymization 2020-06-07 10:20:48 +02:00
zadam
be7ac74235 better fallback for resolving filenames of binary attachments 2020-06-05 10:40:35 +02:00
zadam
58fa0832f6 fix focusing title after creating a note 2020-06-04 21:44:34 +02:00
zadam
1502b9ce66 prevent attribute inheritance cycle via template, closes #1077 2020-06-04 12:27:41 +02:00
zadam
7307ca385f release 0.42.6 2020-06-03 14:30:07 +02:00
zadam
c1fd9825aa fix backup 2020-06-03 12:16:16 +02:00
zadam
9de7d3fc53 fix unloading protected session after clicking on a button, closes #1078 2020-06-03 11:47:30 +02:00
zadam
3c5db844ba fix tree focusing issues 2020-06-03 11:06:45 +02:00
zadam
e7330c1104 more anonymization 2020-06-03 09:55:05 +02:00
zadam
ec4586b164 fix reference link implementation, closes #1069 2020-06-02 23:54:33 +02:00
zadam
91e5f24798 fix db anonymization 2020-06-02 23:13:55 +02:00
zadam
38723e0189 release 0.42.5 2020-05-31 23:33:30 +02:00
zadam
8c88ce6f65 fix moving/cloning notes broken in 0.42.4, closes #1066 2020-05-31 22:33:02 +02:00
zadam
4d22959e28 release 0.42.4 2020-05-31 10:33:12 +02:00
zadam
50a28d8c51 the node you start dragging should be included even if not selected 2020-05-31 10:32:35 +02:00
zadam
e25b633ec4 better error logging in backup 2020-05-31 10:24:59 +02:00
zadam
75bd395669 fix extra refresh because of duplicated sync event, closes #1063 2020-05-30 22:35:18 +02:00
zadam
5e353a5612 improved drag & drop 2020-05-30 10:30:21 +02:00
zadam
6b359b7796 return 401 when auth request is out of sync, closes #1056 2020-05-29 22:06:36 +02:00
zadam
13f9d037dc safer backup to file using VACUUM INTO + possibility to explicitly ask for backup now 2020-05-29 21:55:08 +02:00
zadam
1911d64c1c fix long filename overflowing, closes #1052 2020-05-29 20:36:48 +02:00
zadam
d4c3f1b3f2 upgrade to ckeditor 19.1.0 2020-05-27 22:08:06 +02:00
zadam
3db84daf94 fix hiding autocompletes after closing tab, closes #1034 2020-05-22 19:30:21 +02:00
zadam
2526715aa4 release 0.42.3 2020-05-20 08:54:55 +02:00
zadam
04c573e212 set default executors 2020-05-20 08:54:37 +02:00
zadam
58f4f5d1e6 fix deadlock after "cut to note", closes #1030 2020-05-19 22:58:08 +02:00
zadam
fa5d982a55 fix exporting root note, closes #1024 2020-05-17 21:07:54 +02:00
zadam
108afe8896 fix incorrect processing of sync rows, closes #1019 2020-05-14 13:08:06 +02:00
zadam
37da0adb8a release 0.42.2 2020-05-12 16:46:45 +02:00
zadam
4f50864ec8 better UX when deleting notes - focus in note tree is moved to the next/previous note 2020-05-12 13:40:42 +02:00
zadam
30b9ef8604 fix tab title of deleted note 2020-05-12 12:45:32 +02:00
zadam
b063b4c528 read only view images should not overflow 2020-05-12 12:28:59 +02:00
zadam
e08b0141a4 when expanding/collpasing, set the flag also to the tree cache 2020-05-12 10:52:07 +02:00
zadam
9d8b8e26a1 attach extension to download file if note present 2020-05-12 10:28:31 +02:00
zadam
cb70109ee7 fix creating new promoted attributes, closes #1008
(cherry picked from commit 2e0fb8aaf1)
2020-05-12 00:00:59 +02:00
zadam
e541abbd60 disable hiding the body to not hide the noscript element 2020-05-11 22:44:10 +02:00
Naveen M V
940a70adc5 Fix regex bug (#1005)
(cherry picked from commit 9a662f76da)
2020-05-11 22:06:39 +02:00
zadam
88e8eb7e9c fixed context menu positioning when scaling is active 2020-05-11 20:08:55 +02:00
zadam
b365c186a1 fixed clicking on links in read only view 2020-05-11 19:38:14 +02:00
zadam
64c9734f05 transform setup.js to the webpacked version in the build 2020-05-08 20:50:53 +02:00
zadam
48c843c087 fix setup on server edition 2020-05-08 10:24:57 +02:00
zadam
0e4eec10b9 added "restore this revision" button 2020-05-07 23:34:13 +02:00
zadam
a3661cb763 fix display of buttons for revisions when there is none 2020-05-07 23:14:21 +02:00
zadam
115879ec4a fix note revisions displaying wrong tooltip 2020-05-07 23:02:46 +02:00
97 changed files with 1311 additions and 1466 deletions

4
.idea/dataSources.xml generated
View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="SQLite - document.db" uuid="d0fd879f-1e1d-4d5c-9c21-0e5cf9ab2976">
<data-source source="LOCAL" name="document.db" uuid="b0b03187-36c8-4ec1-bdab-fd4273cd692e">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/../trilium-data/document.db</jdbc-url>
<jdbc-url>jdbc:sqlite:$USER_HOME$/trilium-data/document.db</jdbc-url>
</data-source>
</component>
</project>

View File

@@ -1,692 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataSource name="document.db">
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.18">
<root id="1">
<ServerVersion>3.16.1</ServerVersion>
</root>
<schema id="2" parent="1" name="main">
<Current>1</Current>
</schema>
<collation id="3" parent="1" name="BINARY"/>
<collation id="4" parent="1" name="NOCASE"/>
<collation id="5" parent="1" name="RTRIM"/>
<table id="6" parent="2" name="api_tokens"/>
<table id="7" parent="2" name="attributes"/>
<table id="8" parent="2" name="branches"/>
<table id="9" parent="2" name="note_contents"/>
<table id="10" parent="2" name="note_revision_contents"/>
<table id="11" parent="2" name="note_revisions"/>
<table id="12" parent="2" name="notes"/>
<table id="13" parent="2" name="options"/>
<table id="14" parent="2" name="recent_notes"/>
<table id="15" parent="2" name="source_ids"/>
<table id="16" parent="2" name="sqlite_master">
<System>1</System>
</table>
<table id="17" parent="2" name="sqlite_sequence">
<System>1</System>
</table>
<table id="18" parent="2" name="sync"/>
<column id="19" parent="6" name="apiTokenId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="20" parent="6" name="token">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="21" parent="6" name="utcDateCreated">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="22" parent="6" name="isDeleted">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="23" parent="6" name="hash">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="24" parent="6" name="sqlite_autoindex_api_tokens_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>apiTokenId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="25" parent="6">
<ColNames>apiTokenId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName>
</key>
<column id="26" parent="7" name="attributeId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="27" parent="7" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="28" parent="7" name="type">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="29" parent="7" name="name">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="30" parent="7" name="value">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="31" parent="7" name="position">
<Position>6</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="32" parent="7" name="utcDateCreated">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="33" parent="7" name="utcDateModified">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="34" parent="7" name="isDeleted">
<Position>9</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="35" parent="7" name="deleteId">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<DefaultExpression>NULL</DefaultExpression>
</column>
<column id="36" parent="7" name="hash">
<Position>11</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="37" parent="7" name="isInheritable">
<Position>12</Position>
<DataType>int|0s</DataType>
<DefaultExpression>0</DefaultExpression>
</column>
<index id="38" parent="7" name="sqlite_autoindex_attributes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>attributeId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="39" parent="7" name="IDX_attributes_noteId_index">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="40" parent="7" name="IDX_attributes_name_value">
<ColNames>name
value</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="41" parent="7" name="IDX_attributes_value_index">
<ColNames>value</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="42" parent="7">
<ColNames>attributeId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_attributes_1</UnderlyingIndexName>
</key>
<column id="43" parent="8" name="branchId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="44" parent="8" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="45" parent="8" name="parentNoteId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="46" parent="8" name="notePosition">
<Position>4</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="47" parent="8" name="prefix">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="48" 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">
<Position>7</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="50" parent="8" name="deleteId">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<DefaultExpression>NULL</DefaultExpression>
</column>
<column id="51" parent="8" name="utcDateModified">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="52" parent="8" name="utcDateCreated">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="53" parent="8" name="hash">
<Position>11</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="54" parent="8" name="sqlite_autoindex_branches_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="55" parent="8" name="IDX_branches_noteId_parentNoteId">
<ColNames>noteId
parentNoteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="56" parent="8" name="IDX_branches_parentNoteId">
<ColNames>parentNoteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="57" parent="8">
<ColNames>branchId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
</key>
<column id="58" parent="9" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="59" parent="9" name="content">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<DefaultExpression>NULL</DefaultExpression>
</column>
<column id="60" 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">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="62" parent="9" name="sqlite_autoindex_note_contents_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="63" parent="9">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_contents_1</UnderlyingIndexName>
</key>
<column id="64" parent="10" name="noteRevisionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="65" parent="10" name="content">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="66" 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">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="68" parent="10" name="sqlite_autoindex_note_revision_contents_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="69" parent="10">
<ColNames>noteRevisionId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revision_contents_1</UnderlyingIndexName>
</key>
<column id="70" parent="11" name="noteRevisionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="71" parent="11" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="72" parent="11" name="title">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="73" parent="11" name="contentLength">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="74" 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">
<Position>6</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="76" parent="11" name="utcDateLastEdited">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="77" parent="11" name="utcDateCreated">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="78" parent="11" name="utcDateModified">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="79" parent="11" name="dateLastEdited">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="80" parent="11" name="dateCreated">
<Position>11</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="81" 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">
<Position>13</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="83" 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">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="85" parent="11" name="IDX_note_revisions_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="86" parent="11" name="IDX_note_revisions_utcDateLastEdited">
<ColNames>utcDateLastEdited</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="87" parent="11" name="IDX_note_revisions_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="88" parent="11" name="IDX_note_revisions_dateLastEdited">
<ColNames>dateLastEdited</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="89" parent="11" name="IDX_note_revisions_dateCreated">
<ColNames>dateCreated</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="90" parent="11">
<ColNames>noteRevisionId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
</key>
<column id="91" parent="12" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="92" 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">
<Position>3</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="94" 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">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text&apos;</DefaultExpression>
</column>
<column id="96" 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">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="98" parent="12" name="isDeleted">
<Position>8</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="99" parent="12" name="deleteId">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<DefaultExpression>NULL</DefaultExpression>
</column>
<column id="100" parent="12" name="isErased">
<Position>10</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="101" parent="12" name="dateCreated">
<Position>11</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="102" parent="12" name="dateModified">
<Position>12</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="103" parent="12" name="utcDateCreated">
<Position>13</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="104" parent="12" name="utcDateModified">
<Position>14</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="105" parent="12" name="sqlite_autoindex_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="106" parent="12" name="IDX_notes_title">
<ColNames>title</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="107" parent="12" name="IDX_notes_type">
<ColNames>type</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="108" parent="12" name="IDX_notes_isDeleted">
<ColNames>isDeleted</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="109" parent="12" name="IDX_notes_dateCreated">
<ColNames>dateCreated</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="110" parent="12" name="IDX_notes_dateModified">
<ColNames>dateModified</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="111" parent="12" name="IDX_notes_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="112" parent="12" name="IDX_notes_utcDateModified">
<ColNames>utcDateModified</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="113" parent="12">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
</key>
<column id="114" parent="13" name="name">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="115" parent="13" name="value">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="116" parent="13" name="isSynced">
<Position>3</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="117" parent="13" name="hash">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="118" parent="13" name="utcDateCreated">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="119" parent="13" name="utcDateModified">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="120" parent="13" name="sqlite_autoindex_options_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>name</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="121" parent="13">
<ColNames>name</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
</key>
<column id="122" parent="14" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="123" parent="14" name="notePath">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="124" parent="14" name="hash">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="125" parent="14" name="utcDateCreated">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="126" parent="14" name="isDeleted">
<Position>5</Position>
<DataType>INT|0s</DataType>
</column>
<index id="127" parent="14" name="sqlite_autoindex_recent_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="128" parent="14">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
</key>
<column id="129" parent="15" name="sourceId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="130" parent="15" name="utcDateCreated">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="131" parent="15" name="sqlite_autoindex_source_ids_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>sourceId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="132" parent="15" name="IDX_source_ids_utcDateCreated">
<ColNames>utcDateCreated</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="133" parent="15">
<ColNames>sourceId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
</key>
<column id="134" parent="16" name="type">
<Position>1</Position>
<DataType>text|0s</DataType>
</column>
<column id="135" parent="16" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
</column>
<column id="136" parent="16" name="tbl_name">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="137" parent="16" name="rootpage">
<Position>4</Position>
<DataType>integer|0s</DataType>
</column>
<column id="138" parent="16" name="sql">
<Position>5</Position>
<DataType>text|0s</DataType>
</column>
<column id="139" parent="17" name="name">
<Position>1</Position>
</column>
<column id="140" parent="17" name="seq">
<Position>2</Position>
</column>
<column id="141" parent="18" name="id">
<Position>1</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="142" parent="18" name="entityName">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="143" parent="18" name="entityId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="144" parent="18" name="sourceId">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="145" parent="18" name="isSynced">
<Position>5</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="146" parent="18" name="utcSyncDate">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="147" parent="18" name="IDX_sync_entityName_entityId">
<ColNames>entityName
entityId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="148" parent="18" name="IDX_sync_utcSyncDate">
<ColNames>utcSyncDate</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="149" parent="18">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
</database-model>
</dataSource>

View File

@@ -1,2 +0,0 @@
#n:main
!<md> [0, 0, null, null, -2147483648, -2147483648]

View File

@@ -1,4 +1,4 @@
FROM node:12.16.2-alpine
FROM node:12.16.3-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.16.2
NODE_VERSION=12.16.3
if [ "$1" != "DONTCOPY" ]
then

View File

@@ -33,5 +33,9 @@ find $DIR/libraries -name "*.map" -type f -delete
rm -r $DIR/src/public/app
rm -r $DIR/node_modules/sqlite3/build
rm -r $DIR/node_modules/sqlite3/deps
sed -i -e 's/app\/desktop.js/app-dist\/desktop.js/g' $DIR/src/views/desktop.ejs
sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' $DIR/src/views/mobile.ejs
sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' $DIR/src/views/mobile.ejs
sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' $DIR/src/views/setup.ejs

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

88
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.42.0-beta",
"version": "0.43.0-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1263,7 +1263,7 @@
"dependencies": {
"file-type": {
"version": "3.9.0",
"resolved": "http://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
"integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek="
}
}
@@ -1539,7 +1539,7 @@
},
"uuid": {
"version": "2.0.3",
"resolved": "http://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
"integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho="
}
}
@@ -1573,7 +1573,7 @@
"dependencies": {
"semver": {
"version": "4.3.6",
"resolved": "http://registry.npmjs.org/semver/-/semver-4.3.6.tgz",
"resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz",
"integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto="
}
}
@@ -1593,7 +1593,7 @@
},
"bl": {
"version": "1.2.2",
"resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
"integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
"requires": {
"readable-stream": "^2.3.5",
@@ -1853,12 +1853,12 @@
"dependencies": {
"file-type": {
"version": "3.9.0",
"resolved": "http://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
"integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek="
},
"uuid": {
"version": "2.0.3",
"resolved": "http://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
"integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho="
}
}
@@ -1973,7 +1973,7 @@
},
"readable-stream": {
"version": "1.1.14",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
@@ -2148,7 +2148,7 @@
},
"chalk": {
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"requires": {
"ansi-styles": "^2.2.1",
@@ -2465,7 +2465,7 @@
},
"commander": {
"version": "2.8.1",
"resolved": "http://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
"integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=",
"requires": {
"graceful-readlink": ">= 1.0.0"
@@ -3128,7 +3128,7 @@
},
"readable-stream": {
"version": "1.1.14",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
@@ -3345,9 +3345,9 @@
}
},
"electron": {
"version": "9.0.0-beta.22",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0-beta.22.tgz",
"integrity": "sha512-dfqAf+CXXTKcNDj7DU7mYsmx+oZQcXOvJnZ8ZsgAHjrE9Tv8zsYUgCP3JlO4Z8CIazgleKXYmgh6H2stdK7fEA==",
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.5.tgz",
"integrity": "sha512-bnL9H48LuQ250DML8xUscsKiuSu+xv5umXbpBXYJ0BfvYVmFfNbG3jCfhrsH7aP6UcQKVxOG1R/oQExd0EFneQ==",
"dev": true,
"requires": {
"@electron/get": "^1.0.1",
@@ -4957,7 +4957,7 @@
},
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"getpass": {
@@ -5221,7 +5221,7 @@
},
"got": {
"version": "5.7.1",
"resolved": "http://registry.npmjs.org/got/-/got-5.7.1.tgz",
"resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz",
"integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=",
"requires": {
"create-error-class": "^3.0.1",
@@ -5869,7 +5869,7 @@
},
"into-stream": {
"version": "3.1.0",
"resolved": "http://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz",
"resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz",
"integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=",
"requires": {
"from2": "^2.1.1",
@@ -6021,7 +6021,7 @@
},
"is-obj": {
"version": "1.0.1",
"resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
},
"is-object": {
@@ -6621,7 +6621,7 @@
},
"load-json-file": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
"requires": {
"graceful-fs": "^4.1.2",
@@ -7130,7 +7130,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"minipass": {
@@ -7230,7 +7230,7 @@
},
"mkdirp": {
"version": "0.5.1",
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"requires": {
"minimist": "0.0.8"
@@ -7238,7 +7238,7 @@
"dependencies": {
"minimist": {
"version": "0.0.8",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
}
}
@@ -7432,7 +7432,7 @@
},
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"got": {
@@ -7468,7 +7468,7 @@
},
"p-cancelable": {
"version": "0.4.1",
"resolved": "http://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz",
"integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ=="
},
"p-event": {
@@ -7592,7 +7592,7 @@
"dependencies": {
"file-type": {
"version": "3.9.0",
"resolved": "http://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
"integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek="
}
}
@@ -7617,7 +7617,7 @@
"dependencies": {
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"pify": {
@@ -7674,7 +7674,7 @@
},
"get-stream": {
"version": "2.3.1",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz",
"integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=",
"requires": {
"object-assign": "^4.0.1",
@@ -7704,7 +7704,7 @@
"dependencies": {
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
}
}
@@ -7744,7 +7744,7 @@
},
"pify": {
"version": "2.3.0",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
},
"prepend-http": {
@@ -7849,7 +7849,7 @@
},
"readable-stream": {
"version": "1.1.14",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
@@ -8227,7 +8227,7 @@
},
"onetime": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k="
},
"open": {
@@ -8379,7 +8379,7 @@
},
"p-is-promise": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz",
"integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4="
},
"p-limit": {
@@ -8860,7 +8860,7 @@
},
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
}
}
@@ -9144,7 +9144,7 @@
"dependencies": {
"file-type": {
"version": "3.9.0",
"resolved": "http://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
"integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek="
}
}
@@ -9169,7 +9169,7 @@
"dependencies": {
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"pify": {
@@ -9207,7 +9207,7 @@
},
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
}
}
@@ -9259,7 +9259,7 @@
},
"get-stream": {
"version": "2.3.1",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz",
"integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=",
"requires": {
"object-assign": "^4.0.1",
@@ -9289,7 +9289,7 @@
"dependencies": {
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
}
}
@@ -9477,7 +9477,7 @@
},
"query-string": {
"version": "5.1.1",
"resolved": "http://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz",
"integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==",
"requires": {
"decode-uri-component": "^0.2.0",
@@ -9616,7 +9616,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@@ -10484,7 +10484,7 @@
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
@@ -10509,7 +10509,7 @@
},
"strip-dirs": {
"version": "1.1.1",
"resolved": "http://registry.npmjs.org/strip-dirs/-/strip-dirs-1.1.1.tgz",
"resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-1.1.1.tgz",
"integrity": "sha1-lgu9EoeETzl1pFWKoQOoJV4kVqA=",
"requires": {
"chalk": "^1.0.0",
@@ -10767,7 +10767,7 @@
},
"through": {
"version": "2.3.8",
"resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"through2": {
@@ -10786,7 +10786,7 @@
},
"readable-stream": {
"version": "1.0.34",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
"requires": {
"core-util-is": "~1.0.0",

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.42.1",
"version": "0.43.1",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -78,7 +78,7 @@
"yazl": "^2.5.1"
},
"devDependencies": {
"electron": "9.0.0-beta.22",
"electron": "9.0.5",
"electron-builder": "22.6.0",
"electron-packager": "14.2.1",
"electron-rebuild": "1.10.1",

View File

@@ -1,7 +1,24 @@
const anonymizationService = require('./services/anonymization');
const backupService = require('./services/backup');
const sqlInit = require('./services/sql_init');
require('./entities/entity_constructor');
anonymizationService.anonymize().then(filePath => {
console.log("Anonymized file has been saved to:", filePath);
sqlInit.dbReady.then(async () => {
try {
console.log("Starting anonymization...");
process.exit(0);
});
const resp = await backupService.anonymize();
if (resp.success) {
console.log("Anonymized file has been saved to: " + resp.anonymizedFilePath);
process.exit(0);
} else {
console.log("Anonymization failed.");
}
}
catch (e) {
console.error(e.message, e.stack);
}
process.exit(1);
});

View File

@@ -28,17 +28,6 @@ app.use((req, res, next) => {
next();
});
app.use((req, res, next) => {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
cls.init(() => {
cls.namespace.set("Hi");
next();
});
});
app.use(bodyParser.json({limit: '500mb'}));
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
@@ -120,4 +109,4 @@ require('./services/scheduler');
module.exports = {
app,
sessionParser
};
};

View File

@@ -105,8 +105,7 @@ class Attribute extends Entity {
// cannot be static!
updatePojo(pojo) {
delete pojo.isOwned;
delete pojo.__note;
delete pojo.__note; // FIXME: probably note necessary anymore
}
createClone(type, name, value) {
@@ -124,4 +123,4 @@ class Attribute extends Entity {
}
}
module.exports = Attribute;
module.exports = Attribute;

View File

@@ -411,10 +411,6 @@ class Note extends Entity {
}
});
for (const attr of filteredAttributes) {
attr.isOwned = attr.noteId === this.noteId;
}
this.__attributeCache = filteredAttributes;
}
@@ -545,12 +541,13 @@ class Note extends Entity {
/**
* @return {Promise<Attribute>}
*/
async addAttribute(type, name, value = "") {
async addAttribute(type, name, value = "", isInheritable = false) {
const attr = new Attribute({
noteId: this.noteId,
type: type,
name: name,
value: value
value: value,
isInheritable: isInheritable
});
await attr.save();
@@ -560,12 +557,12 @@ class Note extends Entity {
return attr;
}
async addLabel(name, value = "") {
return await this.addAttribute(LABEL, name, value);
async addLabel(name, value = "", isInheritable = false) {
return await this.addAttribute(LABEL, name, value, isInheritable);
}
async addRelation(name, targetNoteId) {
return await this.addAttribute(RELATION, name, targetNoteId);
async addRelation(name, targetNoteId, isInheritable = false) {
return await this.addAttribute(RELATION, name, targetNoteId, isInheritable);
}
/**
@@ -946,4 +943,4 @@ class Note extends Entity {
}
}
module.exports = Note;
module.exports = Note;

View File

@@ -8,6 +8,7 @@ import contextMenu from "./services/context_menu.js";
import DesktopMainWindowLayout from "./layouts/desktop_main_window_layout.js";
import glob from "./services/glob.js";
import DesktopExtraWindowLayout from "./layouts/desktop_extra_window_layout.js";
import zoomService from './services/zoom.js';
glob.setupGlobs();
@@ -133,9 +134,11 @@ if (utils.isElectron()) {
return;
}
const zoomLevel = zoomService.getCurrentZoom();
contextMenu.show({
x: params.x,
y: params.y,
x: params.x / zoomLevel,
y: params.y / zoomLevel,
items,
selectMenuItemHandler: ({command, spellingSuggestion}) => {
if (command === 'replaceMisspelling') {
@@ -144,4 +147,4 @@ if (utils.isElectron()) {
}
});
});
}
}

View File

@@ -59,8 +59,8 @@ function AttributesModel() {
});
};
async function showAttributes(attributes) {
const ownedAttributes = attributes.filter(attr => attr.isOwned);
async function showAttributes(noteId, attributes) {
const ownedAttributes = attributes.filter(attr => attr.noteId === noteId);
for (const attr of ownedAttributes) {
attr.labelValue = attr.type === 'label' ? attr.value : '';
@@ -86,7 +86,7 @@ function AttributesModel() {
addLastEmptyRow();
const inheritedAttributes = attributes.filter(attr => !attr.isOwned);
const inheritedAttributes = attributes.filter(attr => attr.noteId !== noteId);
self.inheritedAttributes(inheritedAttributes);
}
@@ -96,7 +96,7 @@ function AttributesModel() {
const attributes = await server.get('notes/' + noteId + '/attributes');
await showAttributes(attributes);
await showAttributes(noteId, attributes);
// attribute might not be rendered immediatelly so could not focus
setTimeout(() => $(".attribute-type-select:last").trigger('focus'), 1000);
@@ -152,10 +152,10 @@ function AttributesModel() {
attr.value = treeService.getNoteIdFromNotePath(attr.selectedPath);
}
else if (attr.type === 'label-definition') {
attr.value = attr.labelDefinition;
attr.value = JSON.stringify(attr.labelDefinition);
}
else if (attr.type === 'relation-definition') {
attr.value = attr.relationDefinition;
attr.value = JSON.stringify(attr.relationDefinition);
}
delete attr.labelValue;
@@ -166,7 +166,7 @@ function AttributesModel() {
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
await showAttributes(attributes);
await showAttributes(noteId, attributes);
toastService.showMessage("Attributes have been saved.");
};
@@ -311,4 +311,4 @@ $dialog.on('focus', '.label-value', function (e) {
$el: $(this),
open: true
})
});
});

View File

@@ -39,13 +39,14 @@ export async function showDialog(noteIds) {
}
async function cloneNotesTo(notePath) {
const targetNoteId = treeService.getNoteIdFromNotePath(notePath);
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
const targetBranchId = await treeCache.getBranchId(parentNoteId, noteId);
for (const cloneNoteId of clonedNoteIds) {
await branchService.cloneNoteTo(cloneNoteId, targetNoteId, $clonePrefix.val());
await branchService.cloneNoteTo(cloneNoteId, targetBranchId, $clonePrefix.val());
const clonedNote = await treeCache.getNote(cloneNoteId);
const targetNote = await treeCache.getNote(targetNoteId);
const targetNote = await treeCache.getBranch(targetBranchId).getNote();
toastService.showMessage(`Note "${clonedNote.title}" has been cloned into ${targetNote.title}`);
}
@@ -64,4 +65,4 @@ $form.on('submit', () => {
}
return false;
});
});

View File

@@ -32,10 +32,11 @@ export async function showDialog(branchIds) {
noteAutocompleteService.showRecentNotes($noteAutoComplete);
}
async function moveNotesTo(parentNoteId) {
await branchService.moveToParentNote(movedBranchIds, parentNoteId);
async function moveNotesTo(parentBranchId) {
await branchService.moveToParentNote(movedBranchIds, parentBranchId);
const parentNote = await treeCache.getNote(parentNoteId);
const parentBranch = treeCache.getBranch(parentBranchId);
const parentNote = await parentBranch.getNote();
toastService.showMessage(`Selected notes have been moved into ${parentNote.title}`);
}
@@ -46,13 +47,12 @@ $form.on('submit', () => {
if (notePath) {
$dialog.modal('hide');
const noteId = treeService.getNoteIdFromNotePath(notePath);
moveNotesTo(noteId);
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
treeCache.getBranchId(parentNoteId, noteId).then(branchId => moveNotesTo(branchId));
}
else {
console.error("No path to move to.");
}
return false;
});
});

View File

@@ -37,15 +37,18 @@ export async function showNoteRevisionsDialog(noteId, noteRevisionId) {
async function loadNoteRevisions(noteId, noteRevId) {
$list.empty();
$content.empty();
$titleButtons.empty();
note = appContext.tabManager.getActiveTabNote();
revisionItems = await server.get(`notes/${noteId}/revisions`);
for (const item of revisionItems) {
$list.append($('<a class="dropdown-item" tabindex="0">')
.text(item.dateLastEdited.substr(0, 16) + ` (${item.contentLength} bytes)`)
.attr('data-note-revision-id', item.noteRevisionId))
.attr('title', 'This revision was last edited on ' + item.dateLastEdited);
$list.append(
$('<a class="dropdown-item" tabindex="0">')
.text(item.dateLastEdited.substr(0, 16) + ` (${item.contentLength} bytes)`)
.attr('data-note-revision-id', item.noteRevisionId)
.attr('title', 'This revision was last edited on ' + item.dateLastEdited)
);
}
$listDropdown.dropdown('show');
@@ -60,6 +63,8 @@ async function loadNoteRevisions(noteId, noteRevId) {
$title.text("No revisions for this note yet...");
noteRevisionId = null;
}
$eraseAllRevisionsButton.toggle(revisionItems.length > 0);
}
$dialog.on('shown.bs.modal', () => {
@@ -77,6 +82,21 @@ async function setContentPane() {
$title.html(revisionItem.title);
const $restoreRevisionButton = $('<button class="btn btn-sm" type="button">Restore this revision</button>');
$restoreRevisionButton.on('click', async () => {
const confirmDialog = await import('../dialogs/confirm.js');
const text = 'Do you want to restore this revision? This will overwrite current title/content of the note with this revision.';
if (await confirmDialog.confirm(text)) {
await server.put(`notes/${revisionItem.noteId}/restore-revision/${revisionItem.noteRevisionId}`);
$dialog.modal('hide');
toastService.showMessage('Note revision has been restored.');
}
});
const $eraseRevisionButton = $('<button class="btn btn-sm" type="button">Delete this revision</button>');
$eraseRevisionButton.on('click', async () => {
@@ -93,6 +113,8 @@ async function setContentPane() {
});
$titleButtons
.append($restoreRevisionButton)
.append(' &nbsp; ')
.append($eraseRevisionButton)
.append(' &nbsp; ');

View File

@@ -17,12 +17,18 @@ const TPL = `
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button><br/><br/>
<h4>Debugging</h4>
<h4>Anonymize database</h4>
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and some non-sensitive metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
<button id="anonymize-button" class="btn">Save anonymized database</button><br/><br/>
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
<h4>Backup database</h4>
<p>Trilium has automatic backup (daily, weekly, monthly), but you can also trigger backup manually here.</p>
<button id="backup-database-button" class="btn">Backup database now</button><br/><br/>
<h4>Vacuum database</h4>
@@ -37,6 +43,7 @@ export default class AdvancedOptions {
this.$forceFullSyncButton = $("#force-full-sync-button");
this.$fillSyncRowsButton = $("#fill-sync-rows-button");
this.$anonymizeButton = $("#anonymize-button");
this.$backupDatabaseButton = $("#backup-database-button");
this.$vacuumDatabaseButton = $("#vacuum-database-button");
this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button");
@@ -53,21 +60,32 @@ export default class AdvancedOptions {
});
this.$anonymizeButton.on('click', async () => {
await server.post('anonymization/anonymize');
const resp = await server.post('database/anonymize');
toastService.showMessage("Created anonymized database");
if (!resp.success) {
toastService.showError("Could not create anonymized database, check backend logs for details");
}
else {
toastService.showMessage(`Created anonymized database in ${resp.anonymizedFilePath}`, 10000);
}
});
this.$backupDatabaseButton.on('click', async () => {
const {backupFile} = await server.post('database/backup-database');
toastService.showMessage("Database has been backed up to " + backupFile, 10000);
});
this.$vacuumDatabaseButton.on('click', async () => {
await server.post('cleanup/vacuum-database');
await server.post('database/vacuum-database');
toastService.showMessage("Database has been vacuumed");
});
this.$findAndFixConsistencyIssuesButton.on('click', async () => {
await server.post('cleanup/find-and-fix-consistency-issues');
await server.post('database/find-and-fix-consistency-issues');
toastService.showMessage("Consistency issues should be fixed.");
});
}
}
}

View File

@@ -16,18 +16,18 @@ export async function showDialog(ancestorNoteId) {
ancestorNoteId = hoistedNoteService.getHoistedNoteId();
}
const result = await server.get('recent-changes/' + ancestorNoteId);
const recentChangesRows = await server.get('recent-changes/' + ancestorNoteId);
// preload all notes into cache
await treeCache.getNotes(result.map(r => r.noteId), true);
await treeCache.getNotes(recentChangesRows.map(r => r.noteId), true);
$content.empty();
if (result.length === 0) {
if (recentChangesRows.length === 0) {
$content.append("No changes yet ...");
}
const groupedByDate = groupByDate(result);
const groupedByDate = groupByDate(recentChangesRows);
for (const [dateDay, dayChanges] of groupedByDate) {
const $changesList = $('<ul>');
@@ -95,10 +95,10 @@ export async function showDialog(ancestorNoteId) {
}
}
function groupByDate(result) {
function groupByDate(rows) {
const groupedByDate = new Map();
for (const row of result) {
for (const row of rows) {
const dateDay = row.date.substr(0, 10);
if (!groupedByDate.has(dateDay)) {

View File

@@ -23,8 +23,12 @@ class Attribute {
}
/** @returns {NoteShort} */
async getNote() {
return await this.treeCache.getNote(this.noteId);
getNote() {
return this.treeCache.notes[this.noteId];
}
get targetNoteId() { // alias
return this.type === 'relation' ? this.value : undefined;
}
get jsonValue() {
@@ -39,6 +43,34 @@ class Attribute {
get toString() {
return `Attribute(attributeId=${this.attributeId}, type=${this.type}, name=${this.name}, value=${this.value})`;
}
/**
* @return {boolean} - returns true if this attribute has the potential to influence the note in the argument.
* That can happen in multiple ways:
* 1. attribute is owned by the note
* 2. attribute is owned by the template of the note
* 3. attribute is owned by some note's ancestor and is inheritable
*/
isAffecting(affectedNote) {
const attrNote = this.getNote();
const owningNotes = [affectedNote, ...affectedNote.getTemplateNotes()];
for (const owningNote of owningNotes) {
if (owningNote.noteId === attrNote.noteId) {
return true;
}
}
if (this.isInheritable) {
for (const owningNote of owningNotes) {
if (owningNote.hasAncestor(attrNote)) {
return true;
}
}
}
return false;
}
}
export default Attribute;
export default Attribute;

View File

@@ -170,6 +170,16 @@ class NoteShort {
* @returns {Attribute[]} all note's attributes, including inherited ones
*/
getAttributes(type, name) {
return this.__filterAttrs(this.__getCachedAttributes([]), type, name);
}
__getCachedAttributes(path) {
// notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates
// when template instance is a parent of template itself
if (path.includes(this.noteId)) {
return [];
}
if (!(this.noteId in noteAttributeCache)) {
const ownedAttributes = this.getOwnedAttributes();
@@ -177,11 +187,13 @@ class NoteShort {
ownedAttributes
];
const newPath = [...path, this.noteId];
for (const templateAttr of ownedAttributes.filter(oa => oa.type === 'relation' && oa.name === 'template')) {
const templateNote = this.treeCache.notes[templateAttr.value];
if (templateNote) {
attrArrs.push(templateNote.getAttributes());
if (templateNote && templateNote.noteId !== this.noteId) {
attrArrs.push(templateNote.__getCachedAttributes(newPath));
}
}
@@ -189,7 +201,7 @@ class NoteShort {
for (const parentNote of this.getParentNotes()) {
// these virtual parent-child relationships are also loaded into frontend tree cache
if (parentNote.type !== 'search') {
attrArrs.push(parentNote.getInheritableAttributes());
attrArrs.push(parentNote.__getInheritableAttributes(newPath));
}
}
}
@@ -197,7 +209,7 @@ class NoteShort {
noteAttributeCache.attributes[this.noteId] = attrArrs.flat();
}
return this.__filterAttrs(noteAttributeCache.attributes[this.noteId], type, name);
return noteAttributeCache.attributes[this.noteId];
}
__filterAttrs(attributes, type, name) {
@@ -212,8 +224,8 @@ class NoteShort {
}
}
getInheritableAttributes() {
const attrs = this.getAttributes();
__getInheritableAttributes(path) {
const attrs = this.__getCachedAttributes(path);
return attrs.filter(attr => attr.isInheritable);
}
@@ -425,6 +437,35 @@ class NoteShort {
return targets;
}
/**
* @returns {NoteShort[]}
*/
getTemplateNotes() {
const relations = this.getRelations('template');
return relations.map(rel => this.treeCache.notes[rel.value]);
}
hasAncestor(ancestorNote) {
if (this.noteId === ancestorNote.noteId) {
return true;
}
for (const templateNote of this.getTemplateNotes()) {
if (templateNote.hasAncestor(ancestorNote)) {
return true;
}
}
for (const parentNote of this.getParentNotes()) {
if (parentNote.hasAncestor(ancestorNote)) {console.log(parentNote);
return true;
}
}
return false;
}
/**
* Clear note's attributes cache to force fresh reload for next attribute request.
* Cache is note instance scoped.
@@ -443,6 +484,15 @@ class NoteShort {
.map(attributeId => this.treeCache.attributes[attributeId]);
}
/**
* Return note complement which is most importantly note's content
*
* @return {Promise<NoteComplement>}
*/
async getNoteComplement() {
return await this.treeCache.getNoteComplement(this.noteId);
}
get toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`;
}
@@ -460,4 +510,4 @@ class NoteShort {
}
}
export default NoteShort;
export default NoteShort;

View File

@@ -24,7 +24,7 @@ import NoteRevisionsWidget from "../widgets/collapsible_widgets/note_revisions.j
import SimilarNotesWidget from "../widgets/collapsible_widgets/similar_notes.js";
import WhatLinksHereWidget from "../widgets/collapsible_widgets/what_links_here.js";
import SidePaneToggles from "../widgets/side_pane_toggles.js";
import appContext from "../services/app_context.js";
import EditedNotesWidget from "../widgets/collapsible_widgets/edited_notes.js";
const RIGHT_PANE_CSS = `
<style>
@@ -117,6 +117,7 @@ export default class DesktopMainWindowLayout {
.hideInZenMode())
.child(new FlexContainer('row')
.collapsible()
.filling()
.child(new SidePaneContainer('left')
.hideInZenMode()
.child(new GlobalButtonsWidget())
@@ -143,6 +144,7 @@ export default class DesktopMainWindowLayout {
.hideInZenMode()
.child(new NoteInfoWidget())
.child(new TabCachingWidget(() => new CalendarWidget()))
.child(new TabCachingWidget(() => new EditedNotesWidget()))
.child(new TabCachingWidget(() => new AttributesWidget()))
.child(new TabCachingWidget(() => new LinkMapWidget()))
.child(new TabCachingWidget(() => new NoteRevisionsWidget()))
@@ -153,4 +155,4 @@ export default class DesktopMainWindowLayout {
.child(new SidePaneToggles().hideInZenMode())
);
}
}
}

View File

@@ -4,7 +4,7 @@ import DialogCommandExecutor from "./dialog_command_executor.js";
import Entrypoints from "./entrypoints.js";
import options from "./options.js";
import utils from "./utils.js";
import ZoomService from "./zoom.js";
import zoomService from "./zoom.js";
import TabManager from "./tab_manager.js";
import treeService from "./tree.js";
import Component from "../widgets/component.js";
@@ -17,6 +17,7 @@ class AppContext extends Component {
super();
this.isMainWindow = isMainWindow;
this.executors = [];
}
setLayout(layout) {
@@ -73,7 +74,7 @@ class AppContext extends Component {
}
if (utils.isElectron()) {
this.child(new ZoomService());
this.child(zoomService);
}
this.triggerEvent('initialRenderComplete');
@@ -134,4 +135,4 @@ $(window).on('hashchange', function() {
}
});
export default appContext;
export default appContext;

View File

@@ -45,7 +45,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
}
}
async function moveToParentNote(branchIdsToMove, newParentNoteId) {
async function moveToParentNote(branchIdsToMove, newParentBranchId) {
branchIdsToMove = filterRootNote(branchIdsToMove);
for (const branchIdToMove of branchIdsToMove) {
@@ -56,7 +56,7 @@ async function moveToParentNote(branchIdsToMove, newParentNoteId) {
continue;
}
const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentNoteId}`);
const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
if (!resp.success) {
alert(resp.message);
@@ -198,8 +198,8 @@ ws.subscribeToMessages(async message => {
}
});
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
async function cloneNoteTo(childNoteId, parentBranchId, prefix) {
const resp = await server.put(`notes/${childNoteId}/clone-to/${parentBranchId}`, {
prefix: prefix
});
@@ -225,4 +225,4 @@ export default {
moveNodeUpInHierarchy,
cloneNoteAfter,
cloneNoteTo
};
};

View File

@@ -1,7 +1,6 @@
import ScriptContext from "./script_context.js";
import server from "./server.js";
import toastService from "./toast.js";
import treeCache from "./tree_cache.js";
async function getAndExecuteBundle(noteId, originEntity = null) {
const bundle = await server.get('script/bundle/' + noteId);
@@ -77,4 +76,4 @@ export default {
getAndExecuteBundle,
executeStartupBundles,
getWidgetBundlesByParent
}
}

View File

@@ -33,13 +33,13 @@ async function pasteAfter(afterBranchId) {
}
}
async function pasteInto(parentNoteId) {
async function pasteInto(parentBranchId) {
if (isClipboardEmpty()) {
return;
}
if (clipboardMode === 'cut') {
await branchService.moveToParentNote(clipboardBranchIds, parentNoteId);
await branchService.moveToParentNote(clipboardBranchIds, parentBranchId);
clipboardBranchIds = [];
clipboardMode = null;
@@ -50,7 +50,7 @@ async function pasteInto(parentNoteId) {
for (const clipboardBranch of clipboardBranches) {
const clipboardNote = await clipboardBranch.getNote();
await branchService.cloneNoteTo(clipboardNote.noteId, parentNoteId);
await branchService.cloneNoteTo(clipboardNote.noteId, parentBranchId);
}
// copy will keep clipboardBranchIds and clipboardMode so it's possible to paste into multiple places
@@ -89,4 +89,4 @@ export default {
cut,
copy,
isClipboardEmpty
}
}

View File

@@ -403,6 +403,13 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @method
*/
this.waitUntilSynced = ws.waitForMaxKnownSyncId;
/**
* This will refresh all currently opened notes which have included note specified in the parameter
*
* @param includedNoteId - noteId of the included note
*/
this.refreshIncludedNote = includedNoteId => appContext.triggerEvent('refreshIncludedNote', {noteId: includedNoteId});
}
export default FrontendScriptApi;
export default FrontendScriptApi;

View File

@@ -49,11 +49,7 @@ function setupGlobs() {
let message = "Uncaught error: ";
if (string.includes("Cannot read property 'defaultView' of undefined")) {
// ignore this specific error which is very common but we don't know where it comes from
// and it seems to be harmless
return true;
} else if (string.includes("script error")) {
if (string.includes("script error")) {
message += 'No details available';
} else {
message += [
@@ -61,8 +57,9 @@ function setupGlobs() {
'URL: ' + url,
'Line: ' + lineNo,
'Column: ' + columnNo,
'Error object: ' + JSON.stringify(error)
].join(' - ');
'Error object: ' + JSON.stringify(error),
'Stack: ' + error && error.stack
].join(', ');
}
ws.logError(message);
@@ -91,4 +88,4 @@ function setupGlobs() {
export default {
setupGlobs
}
}

View File

@@ -81,24 +81,29 @@ function goToLink(e) {
}
else if (e.which === 1) {
const activeTabContext = appContext.tabManager.getActiveTabContext();
activeTabContext.setNote(notePath)
activeTabContext.setNote(notePath);
}
else {
return false;
}
}
else {
const address = $link.attr('href');
if (e.which === 1) {
const address = $link.attr('href');
if (address && address.startsWith('http')) {
window.open(address, '_blank');
if (address && address.startsWith('http')) {
window.open(address, '_blank');
}
}
else {
return false;
}
}
return true;
}
function newTabContextMenu(e) {
function linkContextMenu(e) {
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
@@ -113,7 +118,7 @@ function newTabContextMenu(e) {
x: e.pageX,
y: e.pageY,
items: [
{title: "Open note in new tab", command: "openNoteInNewTab", uiIcon: "arrow-up-right"},
{title: "Open note in new tab", command: "openNoteInNewTab", uiIcon: "empty"},
{title: "Open note in new window", command: "openNoteInNewWindow", uiIcon: "window-open"}
],
selectMenuItemHandler: ({command}) => {
@@ -132,7 +137,7 @@ function newTabContextMenu(e) {
$(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) {
$(document).on('mousedown', '.note-detail-text a:not(.reference-link)', function (e) {
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
@@ -155,21 +160,26 @@ $(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,.note-detail-text a.reference-link', goToLink);
$(document).on('mousedown', '.note-detail-text a.reference-link', goToLink);
$(document).on('mousedown', '.note-detail-readonly-text a.reference-link', goToLink);
$(document).on('mousedown', '.note-detail-readonly-text a', goToLink);
$(document).on('mousedown', 'a.ck-link-actions__preview', goToLink);
$(document).on('click', 'section.include-note a', 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);
$(document).on('contextmenu', ".note-paths-widget a", newTabContextMenu);
$(document).on('contextmenu', 'a.ck-link-actions__preview', linkContextMenu);
$(document).on('contextmenu', '.note-detail-text a', linkContextMenu);
$(document).on('contextmenu', '.note-detail-readonly-text a', linkContextMenu);
$(document).on('contextmenu', "a[data-action='note']", linkContextMenu);
$(document).on('contextmenu', ".note-detail-render a", linkContextMenu);
$(document).on('contextmenu', ".note-paths-widget a", linkContextMenu);
$(document).on('contextmenu', "section.include-note a", linkContextMenu);
export default {
getNotePathFromUrl,
createNoteLink,
goToLink
};
};

View File

@@ -4,7 +4,7 @@ export default class LoadResults {
this.noteIdToSourceId = {};
this.sourceIdToNoteIds = {};
this.branches = [];
this.attributes = [];
@@ -54,8 +54,9 @@ export default class LoadResults {
this.attributes.push({attributeId, sourceId});
}
getAttributes() {
getAttributes(sourceId = 'none') {
return this.attributes
.filter(row => row.sourceId !== sourceId)
.map(row => this.treeCache.attributes[row.attributeId])
.filter(attr => !!attr);
}
@@ -103,10 +104,10 @@ export default class LoadResults {
/**
* @return {boolean} true if there are changes which could affect the attributes (including inherited ones)
* notably changes in note itself should not have any effect on attributes
*/
hasAttributeRelatedChanges() {
return Object.keys(this.noteIdToSourceId).length === 0
&& this.branches.length === 0
return this.branches.length === 0
&& this.attributes.length === 0;
}
@@ -119,4 +120,4 @@ export default class LoadResults {
&& this.contentNoteIdToSourceId.length === 0
&& this.options.length === 0;
}
}
}

View File

@@ -34,7 +34,7 @@ export default class MainTreeExecutors extends Component {
return;
}
const {note} = await noteCreateService.createNote(activeNote.noteId, {
await noteCreateService.createNote(activeNote.noteId, {
isProtected: activeNote.isProtected,
saveSelection: false
});
@@ -56,4 +56,4 @@ export default class MainTreeExecutors extends Component {
saveSelection: false
});
}
}
}

View File

@@ -1,29 +0,0 @@
export default class Mutex {
constructor() {
this.queue = [];
this.pending = false;
}
isLocked() {
return this.pending;
}
acquire() {
const ticket = new Promise(resolve => this.queue.push(resolve));
if (!this.pending) {
this.dispatchNext();
}
return ticket;
}
dispatchNext() {
if (this.queue.length > 0) {
this.pending = true;
this.queue.shift()(this.dispatchNext.bind(this));
} else {
this.pending = false;
}
}
}

View File

@@ -10,7 +10,7 @@ let protectedSessionDeferred = null;
async function leaveProtectedSession() {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
utils.reloadApp();
protectedSessionHolder.resetProtectedSession();
}
}
@@ -113,4 +113,4 @@ export default {
enterProtectedSession,
leaveProtectedSession,
setupProtectedSession
};
};

View File

@@ -8,8 +8,8 @@ async function syncNow() {
toastService.showMessage("Sync finished successfully.");
}
else {
if (result.message.length > 50) {
result.message = result.message.substr(0, 50);
if (result.message.length > 100) {
result.message = result.message.substr(0, 100);
}
toastService.showError("Sync failed: " + result.message);
@@ -25,4 +25,4 @@ async function forceNoteSync(noteId) {
export default {
syncNow,
forceNoteSync
};
};

View File

@@ -73,7 +73,7 @@ class TabContext extends Component {
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
if (triggerSwitchEvent) {
this.triggerEvent('tabNoteSwitched', {
await this.triggerEvent('tabNoteSwitched', {
tabContext: this,
notePath: this.notePath
});
@@ -127,4 +127,4 @@ class TabContext extends Component {
}
}
export default TabContext;
export default TabContext;

View File

@@ -203,7 +203,7 @@ export default class TabManager extends Component {
if (activate) {
this.activateTab(tabContext.tabId, false);
this.triggerEvent('tabNoteSwitchedAndActivated', {
await this.triggerEvent('tabNoteSwitchedAndActivated', {
tabContext,
notePath: tabContext.notePath // resolved note path
});
@@ -238,7 +238,7 @@ export default class TabManager extends Component {
}
this.tabsUpdate.scheduleUpdate();
this.setCurrentNotePathToHash();
}
@@ -249,6 +249,9 @@ export default class TabManager extends Component {
return;
}
// close dangling autocompletes after closing the tab
$(".aa-input").autocomplete("close");
await this.triggerEvent('beforeTabRemove', {tabId});
if (this.tabContexts.length <= 1) {
@@ -267,9 +270,6 @@ export default class TabManager extends Component {
this.children = this.children.filter(tc => tc.tabId !== tabId);
// remove dangling autocompletes after closing the tab
$(".algolia-autocomplete").remove();
this.triggerEvent('tabRemoved', {tabId});
this.tabsUpdate.scheduleUpdate();
@@ -346,4 +346,4 @@ export default class TabManager extends Component {
}
}
}
}
}

View File

@@ -22,7 +22,7 @@ async function resolveNotePath(notePath) {
*
* @return {string[]}
*/
async function getRunPath(notePath) {
async function getRunPath(notePath, logErrors = true) {
utils.assertArguments(notePath);
notePath = notePath.split("-")[0].trim();
@@ -66,10 +66,14 @@ async function getRunPath(notePath) {
}
if (!parents.some(p => p.noteId === parentNoteId)) {
console.debug(utils.now(), "Did not find parent " + parentNoteId + " for child " + childNoteId);
if (logErrors) {
console.log(utils.now(), "Did not find parent " + parentNoteId + " for child " + childNoteId);
}
if (parents.length > 0) {
console.debug(utils.now(), "Available parents:", parents);
if (logErrors) {
console.log(utils.now(), "Available parents:", parents);
}
const someNotePath = await getSomeNotePath(parents[0]);
@@ -86,7 +90,10 @@ async function getRunPath(notePath) {
break;
}
else {
console.log("No parents so no run path.");
if (logErrors) {
console.log("No parents so no run path.");
}
return;
}
}
@@ -162,6 +169,13 @@ function getNoteIdFromNotePath(notePath) {
}
function getNoteIdAndParentIdFromNotePath(notePath) {
if (notePath === 'root') {
return {
noteId: 'root',
parentNoteId: 'none'
};
}
let parentNoteId = 'root';
let noteId = '';
@@ -286,4 +300,4 @@ export default {
getNotePathTitle,
getHashValueFromAddress,
parseNotePath
};
};

View File

@@ -54,6 +54,10 @@ class TreeCache {
if (attr.type === 'relation' && attr.name === 'template' && !(attr.value in existingNotes) && !noteIds.has(attr.value)) {
missingNoteIds.push(attr.value);
}
if (!(attr.noteId in existingNotes) && !noteIds.has(attr.noteId)) {
missingNoteIds.push(attr.noteId);
}
}
if (missingNoteIds.length > 0) {
@@ -272,6 +276,9 @@ class TreeCache {
return child.parentToBranch[parentNoteId];
}
/**
* @return {Promise<NoteComplement>}
*/
async getNoteComplement(noteId) {
if (!this.noteComplementPromises[noteId]) {
this.noteComplementPromises[noteId] = server.get('notes/' + noteId).then(row => new NoteComplement(row));
@@ -283,4 +290,4 @@ class TreeCache {
const treeCache = new TreeCache();
export default treeCache;
export default treeCache;

View File

@@ -316,6 +316,24 @@ function dynamicRequire(moduleName) {
}
}
function timeLimit(promise, limitMs) {
return new Promise((res, rej) => {
let resolved = false;
promise.then(result => {
resolved = true;
res(result);
});
setTimeout(() => {
if (!resolved) {
rej(new Error('Process exceeded time limit ' + limitMs));
}
}, limitMs);
});
}
export default {
reloadApp,
parseDate,
@@ -355,5 +373,6 @@ export default {
normalizeShortcut,
copySelectionToClipboard,
isCKEditorInitialized,
dynamicRequire
};
dynamicRequire,
timeLimit
};

View File

@@ -25,7 +25,8 @@ function logError(message) {
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({
type: 'log-error',
error: message
error: message,
stack: new Error().stack
}));
}
}
@@ -37,6 +38,21 @@ function subscribeToMessages(messageHandler) {
// used to serialize sync operations
let consumeQueuePromise = null;
// most sync events are sent twice - once immediatelly after finishing the transaction and once during the scheduled ping
// but we want to process only once
const processedSyncIds = new Set();
function logRows(syncRows) {
const filteredRows = syncRows.filter(row =>
!processedSyncIds.has(row.id)
&& row.entityName !== 'recent_notes'
&& (row.entityName !== 'options' || row.entityId !== 'openTabs'));
if (filteredRows.length > 0) {
console.debug(utils.now(), "Sync data: ", filteredRows);
}
}
async function handleMessage(event) {
const message = JSON.parse(event.data);
@@ -51,13 +67,7 @@ async function handleMessage(event) {
$outstandingSyncsCount.html(message.outstandingSyncs);
if (syncRows.length > 0) {
const filteredRows = syncRows.filter(row =>
row.entityName !== 'recent_notes'
&& (row.entityName !== 'options' || row.entityId !== 'openTabs'));
if (filteredRows.length > 0) {
console.debug(utils.now(), "Sync data: ", filteredRows);
}
logRows(syncRows);
syncDataQueue.push(...syncRows);
@@ -133,13 +143,21 @@ async function runSafely(syncHandler, syncData) {
}
}
/**
* TODO: we should rethink the fact that each sync row is sent twice (once at the end of transaction, once periodically)
* and we keep both lastProcessedSyncId and processedSyncIds
* it even seems incorrect that when transaction sync rows are received, we incorrectly increase lastProcessedSyncId
* and then some syncs might lost (or are *all* sync rows sent from transactions?)
*/
async function consumeSyncData() {
if (syncDataQueue.length > 0) {
const allSyncData = syncDataQueue;
const allSyncRows = syncDataQueue;
syncDataQueue = [];
const nonProcessedSyncRows = allSyncRows.filter(sync => !processedSyncIds.has(sync.id));
try {
await processSyncRows(allSyncData);
await utils.timeLimit(processSyncRows(nonProcessedSyncRows), 5000);
}
catch (e) {
logError(`Encountered error ${e.message}: ${e.stack}, reloading frontend.`);
@@ -148,7 +166,11 @@ async function consumeSyncData() {
utils.reloadApp();
}
lastProcessedSyncId = Math.max(lastProcessedSyncId, allSyncData[allSyncData.length - 1].id);
for (const syncRow of nonProcessedSyncRows) {
processedSyncIds.add(syncRow.id);
}
lastProcessedSyncId = Math.max(lastProcessedSyncId, allSyncRows[allSyncRows.length - 1].id);
}
checkSyncIdListeners();
@@ -170,7 +192,7 @@ function connectWebSocket() {
async function sendPing() {
if (Date.now() - lastPingTs > 30000) {
console.log(utils.now(), "Lost websocket connection to the backend");
console.log(utils.now(), "Lost websocket connection to the backend. If you keep having this issue repeatedly, you might want to check your reverse proxy (nginx, apache) configuration and allow/unblock WebSocket.");
}
if (ws.readyState === ws.OPEN) {
@@ -212,18 +234,18 @@ subscribeToMessages(message => {
async function processSyncRows(syncRows) {
const missingNoteIds = [];
syncRows.forEach(({entityName, entity}) => {
if (entityName === 'branches' && !(entity.parentNoteId in treeCache.notes)) {
for (const {entityName, entity} of syncRows) {
if (entityName === 'branches' && !(entity.parentNoteId in treeCache.notes)) {
missingNoteIds.push(entity.parentNoteId);
}
else if (entityName === 'attributes'
&& entity.type === 'relation'
&& entity.name === 'template'
&& !(entity.noteId in treeCache.notes)) {
}
else if (entityName === 'attributes'
&& entity.type === 'relation'
&& entity.name === 'template'
&& !(entity.noteId in treeCache.notes)) {
missingNoteIds.push(entity.value);
}
});
missingNoteIds.push(entity.value);
}
}
if (missingNoteIds.length > 0) {
await treeCache.reloadNotes(missingNoteIds);
@@ -231,16 +253,16 @@ async function processSyncRows(syncRows) {
const loadResults = new LoadResults(treeCache);
syncRows.filter(sync => sync.entityName === 'notes').forEach(sync => {
for (const sync of syncRows.filter(sync => sync.entityName === 'notes')) {
const note = treeCache.notes[sync.entityId];
if (note) {
note.update(sync.entity);
loadResults.addNote(sync.entityId, sync.sourceId);
}
});
}
syncRows.filter(sync => sync.entityName === 'branches').forEach(sync => {
for (const sync of syncRows.filter(sync => sync.entityName === 'branches')) {
let branch = treeCache.branches[sync.entityId];
const childNote = treeCache.notes[sync.entity.noteId];
const parentNote = treeCache.notes[sync.entity.parentNoteId];
@@ -286,9 +308,9 @@ async function processSyncRows(syncRows) {
}
}
}
});
}
syncRows.filter(sync => sync.entityName === 'note_reordering').forEach(sync => {
for (const sync of syncRows.filter(sync => sync.entityName === 'note_reordering')) {
for (const branchId in sync.positions) {
const branch = treeCache.branches[branchId];
@@ -298,10 +320,10 @@ async function processSyncRows(syncRows) {
}
loadResults.addNoteReordering(sync.entityId, sync.sourceId);
});
}
// missing reloading the relation target note
syncRows.filter(sync => sync.entityName === 'attributes').forEach(sync => {
for (const sync of syncRows.filter(sync => sync.entityName === 'attributes')) {
let attribute = treeCache.attributes[sync.entityId];
const sourceNote = treeCache.notes[sync.entity.noteId];
const targetNote = sync.entity.type === 'relation' && treeCache.notes[sync.entity.value];
@@ -337,27 +359,27 @@ async function processSyncRows(syncRows) {
}
}
}
});
}
syncRows.filter(sync => sync.entityName === 'note_contents').forEach(sync => {
for (const sync of syncRows.filter(sync => sync.entityName === 'note_contents')) {
delete treeCache.noteComplementPromises[sync.entityId];
loadResults.addNoteContent(sync.entityId, sync.sourceId);
});
}
syncRows.filter(sync => sync.entityName === 'note_revisions').forEach(sync => {
for (const sync of syncRows.filter(sync => sync.entityName === 'note_revisions')) {
loadResults.addNoteRevision(sync.entityId, sync.noteId, sync.sourceId);
});
}
syncRows.filter(sync => sync.entityName === 'options').forEach(sync => {
for (const sync of syncRows.filter(sync => sync.entityName === 'options')) {
if (sync.entity.name === 'openTabs') {
return; // only noise
continue; // only noise
}
options.set(sync.entity.name, sync.entity.value);
loadResults.addOption(sync.entity.name);
});
}
if (!loadResults.isEmpty()) {
if (loadResults.hasAttributeRelatedChanges()) {
@@ -374,4 +396,4 @@ export default {
subscribeToMessages,
waitForSyncId,
waitForMaxKnownSyncId
};
};

View File

@@ -5,31 +5,35 @@ import utils from "../services/utils.js";
const MIN_ZOOM = 0.5;
const MAX_ZOOM = 2.0;
export default class ZoomService extends Component {
class ZoomService extends Component {
constructor() {
super();
this.setZoomFactor(options.getFloat('zoomFactor'));
if (utils.isElectron()) {
options.initializedPromise.then(() => {
this.setZoomFactor(options.getFloat('zoomFactor'));
});
}
}
setZoomFactor(zoomFactor) {
zoomFactor = parseFloat(zoomFactor);
const webFrame = utils.dynamicRequire('electron').webFrame;
webFrame.setZoomFactor(zoomFactor);
}
async setZoomFactorAndSave(zoomFactor) {
if (zoomFactor >= MIN_ZOOM && zoomFactor <= MAX_ZOOM) {
this.setZoomFactor(zoomFactor);
await options.save('zoomFactor', zoomFactor);
}
else {
console.log(`Zoom factor ${zoomFactor} outside of the range, ignored.`);
}
}
getCurrentZoom() {
return utils.dynamicRequire('electron').webFrame.getZoomFactor();
}
@@ -45,4 +49,8 @@ export default class ZoomService extends Component {
setZoomFactorAndSaveEvent({zoomFactor}) {
this.setZoomFactorAndSave(zoomFactor);
}
}
}
const zoomService = new ZoomService();
export default zoomService;

View File

@@ -30,6 +30,11 @@ class BasicWidget extends Component {
return this;
}
filling() {
this.css('flex-grow', '1');
return this;
}
hideInZenMode() {
this.class('hide-in-zen-mode');
return this;
@@ -109,4 +114,4 @@ class BasicWidget extends Component {
cleanup() {}
}
export default BasicWidget;
export default BasicWidget;

View File

@@ -4,7 +4,6 @@ const TPL = `
<table class="note-info-widget-table">
<style>
.note-info-widget-table {
table-layout: fixed;
width: 100%;
}
@@ -22,22 +21,23 @@ const TPL = `
<tr>
<th>Note ID:</th>
<td nowrap colspan="3" class="note-info-note-id"></td>
<td class="note-info-note-id"></td>
</tr>
<tr>
<th>Created:</th>
<td nowrap colspan="3" style="overflow: hidden; text-overflow: ellipsis;" class="note-info-date-created"></td>
<td class="note-info-date-created"></td>
</tr>
<tr>
<th>Modified:</th>
<td nowrap colspan="3" style="overflow: hidden; text-overflow: ellipsis;" class="note-info-date-modified"></td>
<td class="note-info-date-modified"></td>
</tr>
<tr>
<th>Type:</th>
<td class="note-info-type"></td>
<th>MIME:</th>
<td class="note-info-mime"></td>
<td>
<span class="note-info-type"></span>
<span class="note-info-mime"></span>
</td>
</tr>
</table>
`;
@@ -60,18 +60,21 @@ export default class NoteInfoWidget extends CollapsibleWidget {
this.$noteId.text(note.noteId);
this.$dateCreated
.text(noteComplement.dateCreated)
.text(noteComplement.dateCreated.substr(0, 16))
.attr("title", noteComplement.dateCreated);
this.$dateModified
.text(noteComplement.dateModified)
.text(noteComplement.dateModified.substr(0, 16))
.attr("title", noteComplement.dateCreated);
this.$type.text(note.type);
this.$mime
.text(note.mime)
.attr("title", note.mime);
if (note.mime) {
this.$mime.text('(' + note.mime + ')');
}
else {
this.$mime.empty();
}
}
entitiesReloadedEvent({loadResults}) {

View File

@@ -1,13 +1,11 @@
import utils from '../services/utils.js';
import Mutex from "../services/mutex.js";
/**
* Abstract class for all components in the Trilium's frontend.
*
* Contains also event implementation with following properties:
* - event / command distribution is synchronous which among others mean that events are well ordered - event
* which was sent out first will also be processed first by the component since it was added to the mutex queue
* as the first one
* which was sent out first will also be processed first by the component
* - execution of the event / command is asynchronous - each component executes the event on its own without regard for
* other components.
* - although the execution is async, we are collecting all the promises and therefore it is possible to wait until the
@@ -19,7 +17,6 @@ export default class Component {
/** @type Component[] */
this.children = [];
this.initialized = Promise.resolve();
this.mutex = new Mutex();
}
setParent(parent) {
@@ -79,18 +76,8 @@ export default class Component {
return false;
}
let release;
await fun.call(this, data);
try {
release = await this.mutex.acquire();
await fun.call(this, data);
return true;
} finally {
if (release) {
release();
}
}
return true;
}
}
}

View File

@@ -35,7 +35,7 @@ const TPL = `
<a class="dropdown-item sync-now-button" title="Trigger sync">
<span class="bx bx-refresh"></span>
Sync (<span id="outstanding-syncs-count">0</span>)
Sync now (<span id="outstanding-syncs-count">0</span>)
</a>
<a class="dropdown-item" data-trigger-command="openNewWindow">
@@ -116,4 +116,4 @@ export default class GlobalMenuWidget extends BasicWidget {
return this.$widget;
}
}
}

View File

@@ -270,13 +270,30 @@ export default class NoteDetailWidget extends TabAwareWidget {
}
async entitiesReloadedEvent({loadResults}) {
// FIXME: we should test what happens when the loaded note is deleted
if (loadResults.isNoteContentReloaded(this.noteId, this.componentId)
|| (loadResults.isNoteReloaded(this.noteId, this.componentId) && (this.type !== await this.getWidgetType() || this.mime !== this.note.mime))) {
this.handleEvent('noteTypeMimeChanged', {noteId: this.noteId});
}
else {
const attrs = loadResults.getAttributes();
const label = attrs.find(attr =>
attr.type === 'label'
&& ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'bookZoomLevel'].includes(attr.name)
&& attr.isAffecting(this.note));
const relation = attrs.find(attr =>
attr.type === 'relation'
&& ['template', 'renderNote'].includes(attr.name)
&& attr.isAffecting(this.note));
if (label || relation) {
// probably incorrect event
// calling this.refresh() is not enough since the event needs to be propagated to children as well
this.handleEvent('noteTypeMimeChanged', {noteId: this.noteId});
}
}
}
beforeUnloadEvent() {
@@ -308,9 +325,10 @@ export default class NoteDetailWidget extends TabAwareWidget {
return;
}
await noteCreateService.createNote(note.noteId, {
// without await as this otherwise causes deadlock through component mutex
noteCreateService.createNote(note.noteId, {
isProtected: note.isProtected,
saveSelection: true
});
}
}
}

View File

@@ -100,4 +100,4 @@ export default class NoteTitleWidget extends TabAwareWidget {
beforeUnloadEvent() {
this.spacedUpdate.updateNowIfNecessary();
}
}
}

View File

@@ -64,6 +64,72 @@ const TPL = `
width: 320px;
border-radius: 10px 0 10px 10px;
}
ul.fancytree-container {
outline: none !important;
background-color: inherit !important;
}
.fancytree-custom-icon {
font-size: 1.3em;
}
span.fancytree-title {
color: inherit !important;
background: inherit !important;
outline: none !important;
}
span.fancytree-node.protected > span.fancytree-custom-icon {
filter: drop-shadow(2px 2px 2px var(--main-text-color));
}
span.fancytree-node.multiple-parents .fancytree-title::after {
content: " *"
}
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
font-weight: bold;
}
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
.ui-fancytree > li > ul {
padding-left: 5px;
}
span.fancytree-active .fancytree-title {
font-weight: bold;
border-color: var(--main-border-color) !important;
border-radius: 5px;
}
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
border-style: dashed !important;
}
span.fancytree-focused .fancytree-title, span.fancytree-focused.fancytree-selected .fancytree-title {
color: var(--active-item-text-color) !important;
background-color: var(--active-item-background-color) !important;
border-color: var(--main-background-color) !important; /* invisible border */
border-radius: 5px;
}
span.fancytree-selected .fancytree-title {
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
border-color: var(--main-background-color) !important; /* invisible border */
border-radius: 5px;
font-style: italic;
}
span.fancytree-node:hover span.fancytree-title {
border-color: var(--main-border-color) !important;
border-radius: 5px;
}
span.fancytree-node.archived {
opacity: 0.6;
}
</style>
<button class="btn btn-sm icon-button bx bx-cog tree-settings-button" title="Tree settings"></button>
@@ -183,9 +249,39 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.initialized = this.initFancyTree();
this.setupNoteTitleTooltip();
return this.$widget;
}
setupNoteTitleTooltip() {
// the following will dynamically set tree item's tooltip if the whole item's text is not currently visible
// if the whole text is visible then no tooltip is show since that's unnecessarily distracting
// see https://github.com/zadam/trilium/pull/1120 for discussion
// code inspired by https://gist.github.com/jtsternberg/c272d7de5b967cec2d3d
const isEnclosing = ($container, $sub) => {
const conOffset = $container.offset();
const conDistanceFromTop = conOffset.top + $container.outerHeight(true);
const conDistanceFromLeft = conOffset.left + $container.outerWidth(true);
const subOffset = $sub.offset();
const subDistanceFromTop = subOffset.top + $sub.outerHeight(true);
const subDistanceFromLeft = subOffset.left + $sub.outerWidth(true);
return conDistanceFromTop > subDistanceFromTop
&& conOffset.top < subOffset.top
&& conDistanceFromLeft > subDistanceFromLeft
&& conOffset.left < subOffset.left;
};
this.$tree.on("mouseenter", "span.fancytree-title", e => {
e.currentTarget.title = isEnclosing(this.$tree, $(e.currentTarget))
? ""
: e.currentTarget.innerText;
});
}
get hideArchivedNotes() {
return options.is("hideArchivedNotes_" + this.treeName);
}
@@ -206,6 +302,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const treeData = [await this.prepareRootNode()];
this.$tree.fancytree({
titlesTabbable: true,
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: utils.isMobile() ? ["dnd5", "clones"] : ["hotkeys", "dnd5", "clones"],
@@ -251,8 +348,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.triggerCommand('setActiveScreen', {screen:'detail'});
}
},
expand: (event, data) => this.setExpandedToServer(data.node.data.branchId, true),
collapse: (event, data) => this.setExpandedToServer(data.node.data.branchId, false),
expand: (event, data) => this.setExpanded(data.node.data.branchId, true),
collapse: (event, data) => this.setExpanded(data.node.data.branchId, false),
hotkeys: utils.isMobile() ? undefined : { keydown: await this.getHotKeys() },
dnd5: {
autoExpandMS: 600,
@@ -265,6 +362,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const notes = this.getSelectedOrActiveNodes(node).map(node => ({
noteId: node.data.noteId,
branchId: node.data.branchId,
title: node.title
}));
@@ -304,17 +402,28 @@ export default class NoteTreeWidget extends TabAwareWidget {
});
}
else {
const jsonStr = dataTransfer.getData("text");
let notes = null;
try {
notes = JSON.parse(jsonStr);
}
catch (e) {
console.error(`Cannot parse ${jsonStr} into notes for drop`);
return;
}
// This function MUST be defined to enable dropping of items on the tree.
// data.hitMode is 'before', 'after', or 'over'.
const selectedBranchIds = this.getSelectedOrActiveNodes().map(node => node.data.branchId);
const selectedBranchIds = notes.map(note => note.branchId);
if (data.hitMode === "before") {
branchService.moveBeforeBranch(selectedBranchIds, node.data.branchId);
} else if (data.hitMode === "after") {
branchService.moveAfterBranch(selectedBranchIds, node.data.branchId);
} else if (data.hitMode === "over") {
branchService.moveToParentNote(selectedBranchIds, node.data.noteId);
branchService.moveToParentNote(selectedBranchIds, node.data.branchId);
} else {
throw new Error("Unknown hitMode=" + data.hitMode);
}
@@ -497,7 +606,17 @@ export default class NoteTreeWidget extends TabAwareWidget {
const noteList = [];
const hideArchivedNotes = this.hideArchivedNotes;
for (const branch of this.getChildBranches(parentNote)) {
if (hideArchivedNotes) {
const note = await branch.getNote();
if (note.hasLabel('archived')) {
continue;
}
}
const node = await this.prepareNode(branch);
noteList.push(node);
@@ -521,6 +640,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
childBranches = childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId));
}
// we're not checking hideArchivedNotes since that would mean we need to lazy load the child notes
// which would seriously slow down everything.
// we check this flag only once user chooses to expand the parent. This has the negative consequence that
// note may appear as folder but not contain any children when all of them are archived
return childBranches;
}
@@ -571,13 +695,18 @@ export default class NoteTreeWidget extends TabAwareWidget {
/** @return {FancytreeNode[]} */
getSelectedOrActiveNodes(node = null) {
const notes = this.getSelectedNodes(true);
const nodes = this.getSelectedNodes(true);
if (notes.length === 0) {
notes.push(node ? node : this.getActiveNode());
// the node you start dragging should be included even if not selected
if (node && !nodes.find(n => n.key === node.key)) {
nodes.push(node);
}
return notes;
if (nodes.length === 0) {
nodes.push(this.getActiveNode());
}
return nodes;
}
async setExpandedStatusForSubtree(node, isExpanded) {
@@ -634,27 +763,30 @@ export default class NoteTreeWidget extends TabAwareWidget {
const activeContext = appContext.tabManager.getActiveTabContext();
if (activeContext && activeContext.notePath) {
this.tree.setFocus();
this.tree.setFocus(true);
const node = await this.expandToNote(activeContext.notePath);
await node.makeVisible({scrollIntoView: true});
node.setFocus();
node.setFocus(true);
}
}
/** @return {FancytreeNode} */
async getNodeFromPath(notePath, expand = false, expandOpts = {}) {
async getNodeFromPath(notePath, expand = false, logErrors = true) {
utils.assertArguments(notePath);
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
/** @var {FancytreeNode} */
/** @const {FancytreeNode} */
let parentNode = null;
const runPath = await treeService.getRunPath(notePath);
const runPath = await treeService.getRunPath(notePath, logErrors);
if (!runPath) {
console.error("Could not find run path for notePath:", notePath);
if (logErrors) {
console.error("Could not find run path for notePath:", notePath);
}
return;
}
@@ -673,7 +805,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
if (expand) {
await parentNode.setExpanded(true, expandOpts);
await parentNode.setExpanded(true);
// although previous line should set the expanded status, it seems to happen asynchronously
// so we need to make sure it is set properly before calling updateNode which uses this flag
const branch = treeCache.getBranch(parentNode.data.branchId);
branch.isExpanded = true;
}
this.updateNode(parentNode);
@@ -686,7 +823,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
foundChildNode = this.findChildNode(parentNode, childNoteId);
if (!foundChildNode) {
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteId}, requested path is ${notePath}`);
if (logErrors) {
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteId}, requested path is ${notePath}`);
}
return;
}
}
@@ -713,8 +853,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
/** @return {FancytreeNode} */
async expandToNote(notePath, expandOpts) {
return this.getNodeFromPath(notePath, true, expandOpts);
async expandToNote(notePath, logErrors = true) {
return this.getNodeFromPath(notePath, true, logErrors);
}
updateNode(node) {
@@ -729,6 +869,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
node.icon = this.getIcon(note, isFolder);
node.extraClasses = this.getExtraClasses(note);
node.title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
if (node.isExpanded() !== branch.isExpanded) {
node.setExpanded(branch.isExpanded, {noEvents: true});
}
node.renderTitle();
}
@@ -760,8 +905,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.toggleInt(this.isEnabled());
const oldActiveNode = this.getActiveNode();
let oldActiveNodeFocused = false;
if (oldActiveNode) {
oldActiveNodeFocused = oldActiveNode.hasFocus();
oldActiveNode.setActive(false);
oldActiveNode.setFocus(false);
}
@@ -774,8 +922,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
await this.expandToNote(this.tabContext.notePath);
}
newActiveNode.setActive(true, {noEvents: true});
newActiveNode.setActive(true, {noEvents: true, noFocus: !oldActiveNodeFocused});
newActiveNode.makeVisible({scrollIntoView: true});
}
}
@@ -804,7 +951,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
async entitiesReloadedEvent({loadResults}) {
const activeNode = this.getActiveNode();
const activeNodeFocused = activeNode && activeNode.hasFocus();
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
const activeNoteId = activeNode ? activeNode.data.noteId : null;
const noteIdsToUpdate = new Set();
@@ -912,7 +1062,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
if (activeNotePath) {
let node = await this.expandToNote(activeNotePath);
let node = await this.expandToNote(activeNotePath, false);
if (node && node.data.noteId !== activeNoteId) {
// if the active note has been moved elsewhere then it won't be found by the path
@@ -924,17 +1074,33 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
if (node) {
node.setActive(true, {noEvents: true});
node.setActive(true, {noEvents: true, noFocus: true});
}
else {
// this is used when original note has been deleted and we want to move the focus to the note above/below
node = await this.expandToNote(nextNotePath, false);
if (node) {
await appContext.tabManager.getActiveTabContext().setNote(nextNotePath);
}
}
const newActiveNode = this.getActiveNode();
// return focus if the previously active node was also focused
if (newActiveNode && activeNodeFocused) {
await newActiveNode.setFocus(true);
}
}
}
async setExpandedToServer(branchId, isExpanded) {
async setExpanded(branchId, isExpanded) {
utils.assertArguments(branchId);
const expandedNum = isExpanded ? 1 : 0;
const branch = treeCache.getBranch(branchId);
branch.isExpanded = isExpanded;
await server.put('branches/' + branchId + '/expanded/' + expandedNum);
await server.put(`branches/${branchId}/expanded/${isExpanded ? 1 : 0}`);
}
async reloadTreeFromCache() {
@@ -951,7 +1117,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
if (activeNotePath) {
const node = await this.getNodeFromPath(activeNotePath, true);
await node.setActive(true, {noEvents: true});
await node.setActive(true, {noEvents: true, noFocus: true});
}
}
@@ -994,7 +1160,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
return false;
}
};
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts) {
hotKeyMap[utils.normalizeShortcut(shortcut)] = node => {
@@ -1019,83 +1185,83 @@ export default class NoteTreeWidget extends TabAwareWidget {
async deleteNotesCommand({node}) {
const branchIds = this.getSelectedOrActiveBranchIds(node);
await branchService.deleteNotes(branchIds);
this.clearSelectedNodes();
}
moveNoteUpCommand({node}) {
const beforeNode = node.getPrevSibling();
if (beforeNode !== null) {
branchService.moveBeforeBranch([node.data.branchId], beforeNode.data.branchId);
}
}
moveNoteDownCommand({node}) {
const afterNode = node.getNextSibling();
if (afterNode !== null) {
branchService.moveAfterBranch([node.data.branchId], afterNode.data.branchId);
}
}
moveNoteUpInHierarchyCommand({node}) {
branchService.moveNodeUpInHierarchy(node);
}
moveNoteDownInHierarchyCommand({node}) {
const toNode = node.getPrevSibling();
if (toNode !== null) {
branchService.moveToParentNote([node.data.branchId], toNode.data.noteId);
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId);
}
}
addNoteAboveToSelectionCommand() {
const node = this.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);
}
}
addNoteBelowToSelectionCommand() {
const node = this.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);
}
}
@@ -1133,7 +1299,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
pasteNotesFromClipboardCommand({node}) {
clipboard.pasteInto(node.data.noteId);
clipboard.pasteInto(node.data.branchId);
}
pasteNotesAfterFromClipboard({node}) {
@@ -1179,4 +1345,4 @@ export default class NoteTreeWidget extends TabAwareWidget {
noteCreateService.duplicateNote(node.data.noteId, branch.parentNoteId);
}
}
}

View File

@@ -19,6 +19,7 @@ const TPL = `
.promoted-attributes td, .promoted-attributes th {
padding: 5px;
min-width: 50px; /* otherwise checkboxes can collapse into 0 width (if there are only checkboxes) */
}
</style>
@@ -98,7 +99,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
const $labelCell = $("<th>").append(valueAttr.name);
const $input = $("<input>")
.prop("tabindex", definitionAttr.position)
.prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
.prop("attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
.prop("attribute-type", valueAttr.type)
.prop("attribute-name", valueAttr.name)
.prop("value", valueAttr.value)
@@ -148,6 +149,8 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
cb(filtered);
}
}]);
$input.on('autocomplete:selected', e => this.promotedAttributeChanged(e))
});
}
else if (definition.labelType === 'number') {
@@ -228,7 +231,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
.prop("title", "Remove this attribute")
.on('click', async () => {
if (valueAttr.attributeId) {
await server.remove("notes/" + this.noteId + "/attributes/" + valueAttr.attributeId);
await server.remove("notes/" + this.noteId + "/attributes/" + valueAttr.attributeId, this.componentId);
}
$tr.remove();
@@ -262,8 +265,14 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
type: $attr.prop("attribute-type"),
name: $attr.prop("attribute-name"),
value: value
});
}, this.componentId);
$attr.prop("attribute-id", result.attributeId);
}
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
this.refresh();
}
}
}

View File

@@ -602,18 +602,23 @@ export default class TabRowWidget extends BasicWidget {
}
updateTab($tab, note) {
if (!note || !$tab.length) {
if (!$tab.length) {
return;
}
this.updateTitle($tab, note.title);
for (const clazz of Array.from($tab[0].classList)) { // create copy to safely iterate over while removing classes
if (clazz !== 'note-tab') {
$tab.removeClass(clazz);
}
}
if (!note) {
this.updateTitle($tab, 'New tab');
return;
}
this.updateTitle($tab, note.title);
$tab.addClass(note.getCssClass());
$tab.addClass(utils.getNoteTypeClass(note.type));
$tab.addClass(utils.getMimeTypeClass(note.mime));
@@ -636,4 +641,4 @@ export default class TabRowWidget extends BasicWidget {
this.updateTab($tab, tabContext.note);
}
}
}
}

View File

@@ -62,4 +62,12 @@ export default class AbstractTextTypeWidget extends TypeWidget {
$el.text(title);
}
}
refreshIncludedNote($container, noteId) {
if ($container) {
$container.find(`section[data-note-id="${noteId}"]`).each((_, el) => {
this.loadIncludedNote(noteId, $(el));
});
}
}
}

View File

@@ -23,6 +23,7 @@ const mentionSetup = {
row.text = row.name = row.noteTitle;
row.id = '@' + row.text;
row.link = '#' + row.path;
row.notePath = row.path;
}
res(rows);
@@ -256,4 +257,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.textEditor.model.insertContent(imageElement, this.textEditor.model.document.selection);
} );
}
}
async refreshIncludedNoteEvent({noteId}) {
this.refreshIncludedNote(this.$editor, noteId);
}
}

View File

@@ -5,17 +5,23 @@ import TypeWidget from "./type_widget.js";
const TPL = `
<div class="note-detail-file note-detail-printable">
<style>
.file-table td {
overflow-wrap: anywhere;
}
</style>
<table class="file-table">
<tr>
<th nowrap>Note ID:</th>
<th>Note ID:</th>
<td class="file-note-id"></td>
<th nowrap>Original file name:</th>
<th>Original file name:</th>
<td class="file-filename"></td>
</tr>
<tr>
<th nowrap>File type:</th>
<th>File type:</th>
<td class="file-filetype"></td>
<th nowrap>File size:</th>
<th>File size:</th>
<td class="file-filesize"></td>
</tr>
</table>
@@ -94,7 +100,7 @@ export default class FileTypeWidget extends TypeWidget {
toastService.showError("Upload of a new file revision failed.");
}
});
return this.$widget;
}
@@ -130,4 +136,4 @@ export default class FileTypeWidget extends TypeWidget {
getFileUrl() {
return utils.getUrlForDownload("api/notes/" + this.noteId + "/download");
}
}
}

View File

@@ -22,6 +22,10 @@ const TPL = `
.note-detail-readonly-text p:first-child, .note-detail-text::before {
margin-top: 0;
}
.note-detail-readonly-text img {
max-width: 100%;
}
</style>
<div class="alert alert-warning no-print">
@@ -77,4 +81,8 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
this.loadIncludedNote(noteId, $(el));
});
}
}
async refreshIncludedNoteEvent({noteId}) {
this.refreshIncludedNote(this.$content, noteId);
}
}

View File

@@ -68,6 +68,8 @@
.note-detail-image {
text-align: center;
height: 100%;
overflow: auto;
}
.note-detail-image-view {
@@ -92,4 +94,4 @@
max-height: 300px;
overflow: auto;
margin: 10px;
}
}

View File

@@ -113,71 +113,6 @@ span.fancytree-node.muted { opacity: 0.6; }
width: 100% !important;
}
ul.fancytree-container {
outline: none !important;
background-color: inherit !important;
}
.fancytree-custom-icon {
font-size: 1.3em;
}
span.fancytree-title {
color: inherit !important;
background: inherit !important;
}
span.fancytree-node.protected > span.fancytree-custom-icon {
filter: drop-shadow(2px 2px 2px var(--main-text-color));
}
span.fancytree-node.multiple-parents .fancytree-title::after {
content: " *"
}
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
font-weight: bold;
}
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
.ui-fancytree > li > ul {
padding-left: 5px;
}
span.fancytree-active .fancytree-title {
font-weight: bold;
border-color: var(--main-border-color) !important;
border-radius: 5px;
}
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
border-style: dashed !important;
}
span.fancytree-focused .fancytree-title, span.fancytree-focused.fancytree-selected .fancytree-title {
color: var(--active-item-text-color) !important;
background-color: var(--active-item-background-color) !important;
border-color: var(--main-background-color) !important; /* invisible border */
border-radius: 5px;
}
span.fancytree-selected .fancytree-title {
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
border-color: var(--main-background-color) !important; /* invisible border */
border-radius: 5px;
font-style: italic;
}
span.fancytree-node:hover span.fancytree-title {
border-color: var(--main-border-color) !important;
border-radius: 5px;
}
span.fancytree-node.archived {
opacity: 0.6;
}
.ui-autocomplete {
max-height: 300px;
overflow-y: auto;
@@ -873,4 +808,4 @@ body {
.hidden-int, .hidden-ext {
display: none !important;
}
}

View File

@@ -1,11 +0,0 @@
"use strict";
const anonymization = require('../../services/anonymization');
async function anonymize() {
await anonymization.anonymize();
}
module.exports = {
anonymize
};

View File

@@ -199,4 +199,4 @@ module.exports = {
getEffectiveNoteAttributes,
createRelation,
deleteRelation
};
};

View File

@@ -14,25 +14,33 @@ const TaskContext = require('../../services/task_context');
*/
async function moveBranchToParent(req) {
const {branchId, parentNoteId} = req.params;
const {branchId, parentBranchId} = req.params;
const parentBranch = await repository.getBranch(parentBranchId);
const branchToMove = await repository.getBranch(branchId);
if (branchToMove.parentNoteId === parentNoteId) {
if (!parentBranch || !branchToMove) {
return [400, `One or both branches ${branchId}, ${parentBranchId} have not been found`];
}
if (branchToMove.parentNoteId === parentBranch.noteId) {
return { success: true }; // no-op
}
const validationResult = await treeService.validateParentChild(parentNoteId, branchToMove.noteId, branchId);
const validationResult = await treeService.validateParentChild(parentBranch.noteId, branchToMove.noteId, branchId);
if (!validationResult.success) {
return [200, validationResult];
}
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentNoteId]);
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentBranch.noteId]);
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 10;
const newBranch = branchToMove.createClone(parentNoteId, newNotePos);
newBranch.isExpanded = true;
// expanding so that the new placement of the branch is immediately visible
parentBranch.isExpanded = true;
await parentBranch.save();
const newBranch = branchToMove.createClone(parentBranch.noteId, newNotePos);
await newBranch.save();
branchToMove.isDeleted = true;
@@ -117,6 +125,7 @@ async function setExpanded(req) {
if (branchId !== 'root') {
await sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
// we don't sync expanded label
// also this does not trigger updates to the frontend, this would trigger too many reloads
}
}
@@ -178,4 +187,4 @@ module.exports = {
setExpandedForSubtree,
deleteBranch,
setPrefix
};
};

View File

@@ -3,10 +3,10 @@
const cloningService = require('../../services/cloning');
async function cloneNoteToParent(req) {
const {noteId, parentNoteId} = req.params;
const {noteId, parentBranchId} = req.params;
const {prefix} = req.body;
return await cloningService.cloneNoteToParent(noteId, parentNoteId, prefix);
return await cloningService.cloneNoteToParent(noteId, parentBranchId, prefix);
}
async function cloneNoteAfter(req) {
@@ -18,4 +18,4 @@ async function cloneNoteAfter(req) {
module.exports = {
cloneNoteToParent,
cloneNoteAfter
};
};

View File

@@ -2,8 +2,19 @@
const sql = require('../../services/sql');
const log = require('../../services/log');
const backupService = require('../../services/backup');
const consistencyChecksService = require('../../services/consistency_checks');
async function anonymize() {
return await backupService.anonymize();
}
async function backupDatabase() {
return {
backupFile: await backupService.backupNow("now")
};
}
async function vacuumDatabase() {
await sql.execute("VACUUM");
@@ -15,6 +26,8 @@ async function findAndFixConsistencyIssues() {
}
module.exports = {
backupDatabase,
vacuumDatabase,
findAndFixConsistencyIssues
};
findAndFixConsistencyIssues,
anonymize
};

View File

@@ -11,6 +11,14 @@ async function exportBranch(req, res) {
const {branchId, type, format, version, taskId} = req.params;
const branch = await repository.getBranch(branchId);
if (!branch) {
const message = `Cannot export branch ${branchId} since it does not exist.`;
log.error(message);
res.status(500).send(message);
return;
}
const taskContext = new TaskContext(taskId, 'export');
try {
@@ -39,4 +47,4 @@ async function exportBranch(req, res) {
module.exports = {
exportBranch
};
};

View File

@@ -1,6 +1,5 @@
"use strict";
const noteService = require('../../services/notes');
const protectedSessionService = require('../../services/protected_session');
const repository = require('../../services/repository');
const utils = require('../../services/utils');
@@ -45,7 +44,9 @@ async function downloadNoteFile(noteId, res, contentDisposition = true) {
if (contentDisposition) {
// (one) reason we're not using the originFileName (available as label) is that it's not
// available for older note revisions and thus would be inconsistent
res.setHeader('Content-Disposition', utils.getContentDisposition(note.title || "untitled"));
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
}
res.setHeader('Content-Type', note.mime);
@@ -70,4 +71,4 @@ module.exports = {
openFile,
downloadFile,
downloadNoteFile
};
};

View File

@@ -16,7 +16,7 @@ const ApiToken = require('../../entities/api_token');
async function loginSync(req) {
if (!await sqlInit.schemaExists()) {
return [400, { message: "DB schema does not exist, can't sync." }];
return [500, { message: "DB schema does not exist, can't sync." }];
}
const timestampStr = req.body.timestamp;
@@ -27,7 +27,7 @@ async function loginSync(req) {
// login token is valid for 5 minutes
if (Math.abs(timestamp.getTime() - now.getTime()) > 5 * 60 * 1000) {
return [400, { message: 'Auth request time is out of sync, please check that both client and server have correct time.' }];
return [401, { message: 'Auth request time is out of sync, please check that both client and server have correct time.' }];
}
const syncVersion = req.body.syncVersion;
@@ -68,7 +68,7 @@ async function loginToProtectedSession(req) {
const protectedSessionId = protectedSessionService.setDataKey(decryptedDataKey);
// this is set here so that event handlers have access to the protected session
cls.namespace.set('protectedSessionId', protectedSessionId);
cls.set('protectedSessionId', protectedSessionId);
await eventService.emit(eventService.ENTER_PROTECTED_SESSION);
@@ -102,4 +102,4 @@ module.exports = {
loginSync,
loginToProtectedSession,
token
};
};

View File

@@ -3,6 +3,7 @@
const repository = require('../../services/repository');
const noteCacheService = require('../../services/note_cache');
const protectedSessionService = require('../../services/protected_session');
const noteRevisionService = require('../../services/note_revisions');
const utils = require('../../services/utils');
const path = require('path');
@@ -37,13 +38,7 @@ async function getNoteRevision(req) {
* @return {string}
*/
function getRevisionFilename(noteRevision) {
let filename = noteRevision.title || "untitled";
if (noteRevision.type === 'text') {
filename += '.html';
} else if (['relation-map', 'search'].includes(noteRevision.type)) {
filename += '.json';
}
let filename = utils.formatDownloadTitle(noteRevision.title, noteRevision.type, noteRevision.mime);
const extension = path.extname(filename);
const date = noteRevision.dateCreated
@@ -109,6 +104,20 @@ async function eraseNoteRevision(req) {
}
}
async function restoreNoteRevision(req) {
const noteRevision = await repository.getNoteRevision(req.params.noteRevisionId);
if (noteRevision && !noteRevision.isErased) {
const note = await noteRevision.getNote();
await noteRevisionService.createNoteRevision(note);
note.title = noteRevision.title;
await note.setContent(await noteRevision.getContent());
await note.save();
}
}
async function getEditedNotesOnDate(req) {
const date = utils.sanitizeSql(req.params.date);
@@ -141,5 +150,6 @@ module.exports = {
downloadNoteRevision,
getEditedNotesOnDate,
eraseAllNoteRevisions,
eraseNoteRevision
};
eraseNoteRevision,
restoreNoteRevision
};

View File

@@ -8,69 +8,55 @@ const noteCacheService = require('../../services/note_cache');
async function getRecentChanges(req) {
const {ancestorNoteId} = req.params;
const noteRows = await sql.getRows(
`
SELECT * FROM (
SELECT note_revisions.noteId,
note_revisions.noteRevisionId,
note_revisions.dateLastEdited AS date
FROM note_revisions
ORDER BY note_revisions.dateLastEdited DESC
)
UNION ALL SELECT * FROM (
SELECT
notes.noteId,
NULL AS noteRevisionId,
dateModified AS date
FROM notes
ORDER BY dateModified DESC
)
ORDER BY date DESC`);
let recentChanges = [];
const recentChanges = [];
const noteRevisions = await sql.getRows(`
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
note_revisions.title,
note_revisions.utcDateCreated AS utcDate,
note_revisions.dateCreated AS date
FROM
note_revisions
JOIN notes USING(noteId)`);
for (const noteRow of noteRows) {
if (!noteCacheService.isInAncestor(noteRow.noteId, ancestorNoteId)) {
continue;
}
if (noteRow.noteRevisionId) {
recentChanges.push(await sql.getRow(`
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
note_revisions.title,
note_revisions.dateCreated AS date
FROM
note_revisions
JOIN notes USING(noteId)
WHERE noteRevisionId = ?`, [noteRow.noteRevisionId]));
}
else {
recentChanges.push(await sql.getRow(`
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.dateModified AS date
FROM
notes
WHERE noteId = ?`, [noteRow.noteId]));
}
if (recentChanges.length >= 200) {
break;
for (const noteRevision of noteRevisions) {
if (noteCacheService.isInAncestor(noteRevision.noteId, ancestorNoteId)) {
recentChanges.push(noteRevision);
}
}
const notes = await sql.getRows(`
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateCreated AS utcDate,
notes.dateCreated AS date
FROM
notes`);
for (const note of notes) {
if (noteCacheService.isInAncestor(note.noteId, ancestorNoteId)) {
recentChanges.push(note);
}
}
recentChanges.sort((a, b) => a.utcDate > b.utcDate ? -1 : 1);
recentChanges = recentChanges.slice(0, Math.min(500, recentChanges.length));
console.log(recentChanges);
for (const change of recentChanges) {
if (change.current_isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
@@ -102,4 +88,4 @@ async function getRecentChanges(req) {
module.exports = {
getRecentChanges
};
};

View File

@@ -55,6 +55,8 @@ async function checkSync() {
}
async function syncNow() {
log.info("Received request to trigger sync now.");
return await syncService.sync();
}
@@ -168,4 +170,4 @@ module.exports = {
getStats,
syncFinished,
queueSector
};
};

View File

@@ -2,67 +2,76 @@ const repository = require('../services/repository');
const log = require('../services/log');
const fileUploadService = require('./api/files.js');
const scriptService = require('../services/script');
const cls = require('../services/cls');
async function handleRequest(req, res) {
// express puts content after first slash into 0 index element
const path = req.params.path + req.params[0];
const attrs = await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')");
for (const attr of attrs) {
const regex = new RegExp(attr.value);
let match;
try {
match = path.match(regex);
}
catch (e) {
log.error(`Testing path for label ${attr.attributeId}, regex=${attr.value} failed with error ` + e.stack);
continue;
}
if (!match) {
continue;
}
if (attr.name === 'customRequestHandler') {
const note = await attr.getNote();
log.info(`Handling custom request "${path}" with note ${note.noteId}`);
try {
await scriptService.executeNote(note, {
pathParams: match.slice(1),
req,
res
});
}
catch (e) {
log.error(`Custom handler ${note.noteId} failed with ${e.message}`);
res.status(500).send(e.message);
}
}
else if (attr.name === 'customResourceProvider') {
await fileUploadService.downloadNoteFile(attr.noteId, res);
}
else {
throw new Error("Unrecognized attribute name " + attr.name);
}
return; // only first handler is executed
}
const message = `No handler matched for custom ${path} request.`;
log.info(message);
res.status(404).send(message);
}
function register(router) {
// explicitly no CSRF middleware since it's meant to allow integration from external services
router.all('/custom/:path*', async (req, res, next) => {
// express puts content after first slash into 0 index element
const path = req.params.path + req.params[0];
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
const attrs = await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')");
for (const attr of attrs) {
const regex = new RegExp(attr.value);
let match;
try {
match = path.match(regex);
}
catch (e) {
log.error(`Testing path for label ${attr.attributeId}, regex=${attr.value} failed with error ` + e.stack);
continue;
}
if (!match) {
continue;
}
if (attr.name === 'customRequestHandler') {
const note = await attr.getNote();
log.info(`Handling custom request "${path}" with note ${note.noteId}`);
try {
await scriptService.executeNote(note, {
pathParams: match.slice(1),
req,
res
});
}
catch (e) {
log.error(`Custom handler ${note.noteId} failed with ${e.message}`);
res.status(500).send(e.message);
}
}
else if (attr.name === 'customResourceProvider') {
await fileUploadService.downloadNoteFile(attr.noteId, res);
}
else {
throw new Error("Unrecognized attribute name " + attr.name);
}
return; // only first handler is executed
}
const message = `No handler matched for custom ${path} request.`;
log.info(message);
res.status(404).send(message);
cls.init(() => handleRequest(req, res));
});
}
module.exports = {
register
};
};

View File

@@ -24,8 +24,7 @@ const exportRoute = require('./api/export');
const importRoute = require('./api/import');
const setupApiRoute = require('./api/setup');
const sqlRoute = require('./api/sql');
const anonymizationRoute = require('./api/anonymization');
const cleanupRoute = require('./api/cleanup');
const databaseRoute = require('./api/database');
const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script');
@@ -82,9 +81,12 @@ function apiRoute(method, path, routeHandler) {
function route(method, path, middleware, routeHandler, resultHandler, transactional = true) {
router[method](path, ...middleware, async (req, res, next) => {
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
const result = await cls.init(async () => {
cls.namespace.set('sourceId', req.headers['trilium-source-id']);
cls.namespace.set('localNowDateTime', req.headers['`trilium-local-now-datetime`']);
cls.set('sourceId', req.headers['trilium-source-id']);
cls.set('localNowDateTime', req.headers['`trilium-local-now-datetime`']);
protectedSessionService.setProtectedSessionId(req);
if (transactional) {
@@ -123,7 +125,7 @@ function register(app) {
apiRoute(POST, '/api/tree/load', treeApiRoute.load);
apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix);
apiRoute(PUT, '/api/branches/:branchId/move-to/:parentNoteId', branchesApiRoute.moveBranchToParent);
apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent);
apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote);
apiRoute(PUT, '/api/branches/:branchId/move-after/:afterBranchId', branchesApiRoute.moveBranchAfterNote);
apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded);
@@ -145,13 +147,14 @@ function register(app) {
apiRoute(GET, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision);
apiRoute(DELETE, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.eraseNoteRevision);
route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision);
apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision);
apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap);
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateNote);
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentNoteId', cloningApiRoute.cloneNoteToParent);
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentBranchId', cloningApiRoute.cloneNoteToParent);
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
route(GET, '/api/notes/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch);
@@ -219,12 +222,15 @@ function register(app) {
apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema);
apiRoute(POST, '/api/sql/execute', sqlRoute.execute);
apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
route(POST, '/api/database/anonymize', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false);
// backup requires execution outside of transaction
route(POST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false);
// VACUUM requires execution outside of transaction
route(POST, '/api/cleanup/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.vacuumDatabase, apiResultHandler, false);
route(POST, '/api/database/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.vacuumDatabase, apiResultHandler, false);
route(POST, '/api/cleanup/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.findAndFixConsistencyIssues, apiResultHandler, false);
route(POST, '/api/database/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.findAndFixConsistencyIssues, apiResultHandler, false);
apiRoute(POST, '/api/script/exec', scriptRoute.exec);
apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run);
@@ -266,4 +272,4 @@ function register(app) {
module.exports = {
register
};
};

View File

@@ -6,9 +6,8 @@ const utils = require('../services/utils');
async function setupPage(req, res) {
if (await sqlInit.isDbInitialized()) {
const windowService = require('../services/window');
if (utils.isElectron()) {
const windowService = require('../services/window');
await windowService.createMainWindow();
windowService.closeSetupWindow();
}

View File

@@ -1,38 +0,0 @@
"use strict";
const dataDir = require('./data_dir');
const dateUtils = require('./date_utils');
const fs = require('fs-extra');
const sqlite = require('sqlite');
async function anonymize() {
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
}
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
fs.copySync(dataDir.DOCUMENT_PATH, anonymizedFile);
const db = await sqlite.open(anonymizedFile, {Promise});
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'");
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',
'passwordVerificationSalt', 'passwordDerivedKeySalt')`);
await db.run("VACUUM");
await db.close();
return anonymizedFile;
}
module.exports = {
anonymize
};

View File

@@ -27,9 +27,9 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'label', name: 'customRequestHandler', isDangerous: true },
{ type: 'label', name: 'customResourceProvider', isDangerous: true },
{ type: 'label', name: 'bookZoomLevel', isDangerous: false },
{ type: 'label', name: 'widget', isDangerous: true },
// relation names
{ type: 'relation', name: 'runOnNoteView', isDangerous: true },
{ type: 'relation', name: 'runOnNoteCreation', isDangerous: true },
{ type: 'relation', name: 'runOnNoteTitleChange', isDangerous: true },
{ type: 'relation', name: 'runOnNoteChange', isDangerous: true },
@@ -115,13 +115,24 @@ function isAttributeType(type) {
}
function isAttributeDangerous(type, name) {
return BUILTIN_ATTRIBUTES.some(attr =>
attr.type === attr.type &&
return BUILTIN_ATTRIBUTES.some(attr =>
attr.type === attr.type &&
attr.name.toLowerCase() === name.trim().toLowerCase() &&
attr.isDangerous
);
}
function getBuiltinAttributeNames() {
return BUILTIN_ATTRIBUTES
.map(attr => attr.name)
.concat([
'internalLink',
'imageLink',
'includeNoteLink',
'relationMapLink'
]);
}
module.exports = {
getNotesWithLabel,
getNotesWithLabels,
@@ -131,5 +142,6 @@ module.exports = {
createAttribute,
getAttributeNames,
isAttributeType,
isAttributeDangerous
};
isAttributeDangerous,
getBuiltinAttributeNames
};

View File

@@ -7,7 +7,11 @@ const dataDir = require('./data_dir');
const log = require('./log');
const sqlInit = require('./sql_init');
const syncMutexService = require('./sync_mutex');
const attributeService = require('./attributes');
const cls = require('./cls');
const utils = require('./utils');
const sqlite = require('sqlite');
const sqlite3 = require('sqlite3');
async function regularBackup() {
await periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
@@ -28,28 +32,110 @@ async function periodBackup(optionName, fileName, periodInSeconds) {
}
}
const COPY_ATTEMPT_COUNT = 50;
async function copyFile(backupFile) {
const sql = require('./sql');
try {
fs.unlinkSync(backupFile);
} catch (e) {
} // unlink throws exception if the file did not exist
let success = false;
let attemptCount = 0
for (; attemptCount < COPY_ATTEMPT_COUNT && !success; attemptCount++) {
try {
await sql.executeWithoutTransaction(`VACUUM INTO '${backupFile}'`);
success = true;
} catch (e) {
log.info(`Copy DB attempt ${attemptCount + 1} failed with "${e.message}", retrying...`);
}
// we re-try since VACUUM is very picky and it can't run if there's any other query currently running
// which is difficult to guarantee so we just re-try
}
return attemptCount !== COPY_ATTEMPT_COUNT;
}
async function backupNow(name) {
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
await syncMutexService.doExclusively(async () => {
return await syncMutexService.doExclusively(async () => {
const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`;
fs.copySync(dataDir.DOCUMENT_PATH, backupFile);
const success = await copyFile(backupFile);
log.info("Created backup at " + backupFile);
if (success) {
log.info("Created backup at " + backupFile);
}
else {
log.error(`Creating backup ${backupFile} failed`);
}
return backupFile;
});
}
async function anonymize() {
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
}
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
const success = await copyFile(anonymizedFile);
if (!success) {
return { success: false };
}
const db = await sqlite.open({
filename: anonymizedFile,
driver: sqlite3.Database
});
await db.run("UPDATE api_tokens SET token = 'API token value'");
await db.run("UPDATE notes SET title = 'title'");
await db.run("UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL");
await db.run("UPDATE note_revisions SET title = 'title'");
await db.run("UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL");
// we want to delete all non-builtin attributes because they can contain sensitive names and values
// on the other hand builtin/system attrs should not contain any sensitive info
const builtinAttrs = attributeService.getBuiltinAttributeNames().map(name => "'" + utils.sanitizeSql(name) + "'").join(', ');
await db.run(`UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN(${builtinAttrs})`);
await db.run(`UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN (${builtinAttrs})`);
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
('documentId', 'documentSecret', 'encryptedDataKey',
'passwordVerificationHash', 'passwordVerificationSalt',
'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')
AND value != ''`);
await db.run("VACUUM");
await db.close();
return {
success: true,
anonymizedFilePath: anonymizedFile
};
}
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
fs.mkdirSync(dataDir.BACKUP_DIR, 0o700);
}
sqlInit.dbReady.then(() => {
setInterval(cls.wrap(regularBackup), 60 * 60 * 1000);
setInterval(cls.wrap(regularBackup), 4 * 60 * 60 * 1000);
// kickoff backup immediately
setTimeout(cls.wrap(regularBackup), 1000);
// kickoff first backup soon after start up
setTimeout(cls.wrap(regularBackup), 5 * 60 * 1000);
});
module.exports = {
backupNow
};
backupNow,
anonymize
};

View File

@@ -1 +1 @@
module.exports = { buildDate:"2020-05-06T23:24:13+02:00", buildRevision: "54ecd2ee75d1177cedadf9fee10319687feee5f0" };
module.exports = { buildDate:"2020-06-23T10:11:17+02:00", buildRevision: "89aa4fbc73fc6c357e04e037d6a1191fa4058d80" };

View File

@@ -9,12 +9,14 @@ const Branch = require('../entities/branch');
const TaskContext = require("./task_context.js");
const utils = require('./utils');
async function cloneNoteToParent(noteId, parentNoteId, prefix) {
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentNoteId)) {
async function cloneNoteToParent(noteId, parentBranchId, prefix) {
const parentBranch = await repository.getBranch(parentBranchId);
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentBranch.noteId)) {
return { success: false, message: 'Note is deleted.' };
}
const validationResult = await treeService.validateParentChild(parentNoteId, noteId);
const validationResult = await treeService.validateParentChild(parentBranch.noteId, noteId);
if (!validationResult.success) {
return validationResult;
@@ -22,12 +24,13 @@ async function cloneNoteToParent(noteId, parentNoteId, prefix) {
const branch = await new Branch({
noteId: noteId,
parentNoteId: parentNoteId,
parentNoteId: parentBranch.noteId,
prefix: prefix,
isExpanded: 0
}).save();
await sql.execute("UPDATE branches SET isExpanded = 1 WHERE noteId = ?", [parentNoteId]);
parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user
await parentBranch.save();
return { success: true, branchId: branch.branchId };
}
@@ -111,4 +114,4 @@ module.exports = {
ensureNoteIsAbsentFromParent,
toggleNoteInParent,
cloneNoteAfter
};
};

View File

@@ -9,6 +9,14 @@ function wrap(callback) {
return async () => await init(callback);
}
function get(key) {
return namespace.get(key);
}
function set(key, value) {
namespace.set(key, value);
}
function getSourceId() {
return namespace.get('sourceId');
}
@@ -52,6 +60,8 @@ function setEntityToCache(entityName, entityId, entity) {
module.exports = {
init,
wrap,
get,
set,
namespace,
getSourceId,
getLocalNowDateTime,
@@ -62,4 +72,4 @@ module.exports = {
addSyncRow,
getEntityFromCache,
setEntityToCache
};
};

View File

@@ -3,7 +3,6 @@
const html = require('html');
const repository = require('../repository');
const dateUtils = require('../date_utils');
const zip = require('tar-stream');
const path = require('path');
const mimeTypes = require('mime-types');
const mdService = require('./md');
@@ -444,4 +443,4 @@ ${content}
module.exports = {
exportToZip
};
};

View File

@@ -47,7 +47,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
return noteIdMap[origNoteId];
}
function getMeta(filePath) {
if (!metaFile) {
return {};
@@ -425,7 +425,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
}
for (const noteId in createdNoteIds) { // now the noteIds are unique
await noteService.scanForLinks(await repository.getNotes(noteId));
await noteService.scanForLinks(await repository.getNote(noteId));
if (!metaFile) {
// if there's no meta file then the notes are created based on the order in that tar file but that
@@ -459,4 +459,4 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
module.exports = {
importTar
};
};

View File

@@ -454,7 +454,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
});
for (const noteId in createdNoteIds) { // now the noteIds are unique
await noteService.scanForLinks(await repository.getNotes(noteId));
await noteService.scanForLinks(await repository.getNote(noteId));
if (!metaFile) {
// if there's no meta file then the notes are created based on the order in that tar file but that
@@ -481,4 +481,4 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
module.exports = {
importZip
};
};

View File

@@ -32,7 +32,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
{
actionName: "scrollToActiveNote",
defaultShortcuts: ["CommandOrControl+."],
scope: "window" // FIXME - how do we find what note tree should be updated?
scope: "window"
},
{
actionName: "searchNotes",
@@ -55,7 +55,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
actionName: "collapseTree",
defaultShortcuts: ["Alt+C"],
description: "Collapses the complete note tree",
scope: "note-tree"
scope: "window"
},
{
actionName: "collapseSubtree",
@@ -327,7 +327,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
{
actionName: "printActiveNote",
defaultShortcuts: [],
scope: "note-detail"
scope: "window"
},
{
actionName: "runActiveNote",
@@ -425,4 +425,4 @@ async function getKeyboardActions() {
module.exports = {
DEFAULT_KEYBOARD_ACTIONS,
getKeyboardActions
};
};

View File

@@ -13,6 +13,7 @@ const Attribute = require('../entities/attribute');
const hoistedNoteService = require('../services/hoisted_note');
const protectedSessionService = require('../services/protected_session');
const log = require('../services/log');
const utils = require('../services/utils');
const noteRevisionService = require('../services/note_revisions');
const attributeService = require('../services/attributes');
const request = require('./request');
@@ -97,7 +98,7 @@ async function createNewNote(params) {
const parentNote = await repository.getNote(params.parentNoteId);
if (!parentNote) {
throw new Error(`Parent note ${params.parentNoteId} not found.`);
throw new Error(`Parent note "${params.parentNoteId}" not found.`);
}
if (!params.title || params.title.trim().length === 0) {
@@ -276,9 +277,9 @@ async function downloadImage(noteId, imageUrl) {
const downloadImagePromises = {};
function replaceUrl(content, url, imageNote) {
const quoted = url.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
const quotedUrl = utils.quoteRegex(url);
return content.replace(new RegExp(`\\s+src=[\"']${quoted}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`);
return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`);
}
async function downloadImages(noteId, content) {
@@ -661,7 +662,9 @@ async function scanForLinks(note) {
const content = await note.getContent();
const newContent = await saveLinks(note, content);
await note.setContent(newContent);
if (content !== newContent) {
await note.setContent(newContent);
}
}
catch (e) {
log.error(`Could not scan for links note ${note.noteId}: ${e.message} ${e.stack}`);

View File

@@ -1,6 +1,6 @@
const dayjs = require("dayjs");
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}\p{Number}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([^\s=*]+|"[^"]+"))?/igu;
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}\p{Number}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([^\s=*"]+|"[^"]+"))?/igu;
const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i;
function calculateSmartValue(v) {

View File

@@ -15,11 +15,11 @@ function setDataKey(decryptedDataKey) {
}
function setProtectedSessionId(req) {
cls.namespace.set('protectedSessionId', req.cookies.protectedSessionId);
cls.set('protectedSessionId', req.cookies.protectedSessionId);
}
function getProtectedSessionId() {
return cls.namespace.get('protectedSessionId');
return cls.get('protectedSessionId');
}
function getDataKey() {
@@ -63,4 +63,4 @@ module.exports = {
decryptString,
decryptNotes,
setProtectedSessionId
};
};

View File

@@ -140,10 +140,6 @@ async function updateEntity(entity) {
await eventService.emit(entity.isDeleted ? eventService.ENTITY_DELETED : eventService.ENTITY_CHANGED, eventPayload);
}
}
if (entity.afterSaving) {
await entity.afterSaving();
}
});
}
@@ -159,4 +155,4 @@ module.exports = {
getOption,
updateEntity,
setEntityConstructor
};
};

View File

@@ -9,12 +9,13 @@ const syncOptions = require('./sync_options');
// this allows to support system proxy
function exec(opts) {
const client = getClient(opts);
// hack for cases where electron.net does not work but we don't want to set proxy
if (opts.proxy === 'noproxy') {
opts.proxy = null;
}
const client = getClient(opts);
const proxyAgent = getProxyAgent(opts);
const parsedTargetUrl = url.parse(opts.url);
@@ -40,7 +41,7 @@ function exec(opts) {
host: parsedTargetUrl.hostname,
port: parsedTargetUrl.port,
path: parsedTargetUrl.path,
timeout: opts.timeout,
timeout: opts.timeout, // works only for node.js client
headers,
agent: proxyAgent
});
@@ -104,13 +105,15 @@ async function getImage(imageUrl) {
host: parsedTargetUrl.hostname,
port: parsedTargetUrl.port,
path: parsedTargetUrl.path,
timeout: opts.timeout,
timeout: opts.timeout, // works only for node client
headers: {},
agent: proxyAgent
});
request.on('error', err => reject(generateError(opts, err)));
request.on('abort', err => reject(generateError(opts, err)));
request.on('response', response => {
if (![200, 201, 204].includes(response.statusCode)) {
reject(generateError(opts, response.statusCode + ' ' + response.statusMessage));
@@ -173,4 +176,4 @@ function generateError(opts, message) {
module.exports = {
exec,
getImage
};
};

View File

@@ -31,7 +31,7 @@ async function executeBundle(bundle, apiParams = {}) {
apiParams.startNote = bundle.note;
}
cls.namespace.set('sourceId', 'script');
cls.set('sourceId', 'script');
// last \r\n is necessary if script contains line comment on its last line
const script = "async function() {\r\n" + bundle.script + "\r\n}";
@@ -187,4 +187,4 @@ module.exports = {
executeNoteNoException,
executeScript,
getScriptBundleForFrontend
};
};

View File

@@ -6,6 +6,7 @@ const optionService = require('./options');
const syncOptions = require('./sync_options');
const request = require('./request');
const appInfo = require('./app_info');
const utils = require('./utils');
async function hasSyncServerSchemaAndSeed() {
const response = await requestToSyncServer('GET', '/api/setup/status');
@@ -43,13 +44,15 @@ async function sendSeedToSyncServer() {
}
async function requestToSyncServer(method, path, body = null) {
return await request.exec({
const timeout = await syncOptions.getSyncTimeout();
return utils.timeLimit(request.exec({
method,
url: await syncOptions.getSyncServerHost() + path,
body,
proxy: await syncOptions.getSyncProxy(),
timeout: await syncOptions.getSyncTimeout()
});
timeout: timeout
}), timeout);
}
async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, password) {
@@ -115,4 +118,4 @@ module.exports = {
sendSeedToSyncServer,
setupSyncFromSyncServer,
getSyncSeedOptions
};
};

View File

@@ -9,7 +9,7 @@ function setDbConnection(connection) {
dbConnection = connection;
}
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `uncaughtException`, `SIGTERM`].forEach(eventType => {
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
process.on(eventType, () => {
if (dbConnection) {
// closing connection is especially important to fold -wal file into the main DB file
@@ -64,15 +64,15 @@ async function upsert(tableName, primaryKey, rec) {
}
async function beginTransaction() {
return await execute("BEGIN");
return await dbConnection.run("BEGIN");
}
async function commit() {
return await execute("COMMIT");
return await dbConnection.run("COMMIT");
}
async function rollback() {
return await execute("ROLLBACK");
return await dbConnection.run("ROLLBACK");
}
async function getRow(query, params = []) {
@@ -150,15 +150,25 @@ async function getColumn(query, params = []) {
}
async function execute(query, params = []) {
await startTransactionIfNecessary();
return await wrap(async db => db.run(query, ...params), query);
}
async function executeWithoutTransaction(query, params = []) {
await dbConnection.run(query, ...params);
}
async function executeMany(query, params) {
await startTransactionIfNecessary();
// essentially just alias
await getManyRows(query, params);
}
async function executeScript(query) {
await startTransactionIfNecessary();
return await wrap(async db => db.exec(query), query);
}
@@ -195,60 +205,68 @@ async function wrap(func, query) {
}
}
// true if transaction is active globally.
// cls.namespace.get('isTransactional') OTOH indicates active transaction in active CLS
let transactionActive = false;
// resolves when current transaction ends with either COMMIT or ROLLBACK
let transactionPromise = null;
let transactionPromiseResolve = null;
async function transactional(func) {
if (cls.namespace.get('isInTransaction')) {
return await func();
async function startTransactionIfNecessary() {
if (!cls.get('isTransactional')
|| cls.get('isInTransaction')) {
return;
}
while (transactionActive) {
await transactionPromise;
}
let ret = null;
const thisError = new Error(); // to capture correct stack trace in case of exception
// first set semaphore (atomic operation and only then start transaction
transactionActive = true;
transactionPromise = new Promise(async (resolve, reject) => {
try {
await beginTransaction();
transactionPromise = new Promise(res => transactionPromiseResolve = res);
cls.set('isInTransaction', true);
cls.namespace.set('isInTransaction', true);
await beginTransaction();
}
ret = await func();
await commit();
require('./ws.js').sendPingToAllClients();
transactionActive = false;
resolve();
setTimeout(() => require('./ws').sendPingToAllClients(), 50);
}
catch (e) {
if (transactionActive) {
log.error("Error executing transaction, executing rollback. Inner stack: " + e.stack + "\nOutside stack: " + thisError.stack);
await rollback();
transactionActive = false;
}
reject(e);
}
finally {
cls.namespace.set('isInTransaction', false);
}
});
if (transactionActive) {
await transactionPromise;
async function transactional(func) {
// if the CLS is already transactional then the whole transaction is handled by higher level transactional() call
if (cls.get('isTransactional')) {
return await func();
}
return ret;
cls.set('isTransactional', true); // this signals that transaction will be needed if there's a write operation
try {
const ret = await func();
if (cls.get('isInTransaction')) {
await commit();
// note that sync rows sent from this action will be sent again by scheduled periodic ping
require('./ws.js').sendPingToAllClients();
}
return ret;
}
catch (e) {
if (cls.get('isInTransaction')) {
await rollback();
}
throw e;
}
finally {
cls.namespace.set('isTransactional', false);
if (cls.namespace.get('isInTransaction')) {
transactionActive = false;
cls.namespace.set('isInTransaction', false);
// resolving even for rollback since this is just semaphore for allowing another write transaction to proceed
transactionPromiseResolve();
}
}
}
module.exports = {
@@ -263,8 +281,9 @@ module.exports = {
getMap,
getColumn,
execute,
executeWithoutTransaction,
executeMany,
executeScript,
transactional,
upsert
};
};

View File

@@ -70,7 +70,7 @@ async function sync() {
};
}
else {
log.info("sync failed: " + e.message + e.stack);
log.info("sync failed: " + e.message + "\nstack: " + e.stack);
return {
success: false,
@@ -97,7 +97,6 @@ async function doLogin() {
const hash = utils.hmac(documentSecret, timestamp);
const syncContext = { cookieJar: {} };
const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', {
timestamp: timestamp,
syncVersion: appInfo.syncVersion,
@@ -259,14 +258,18 @@ async function checkContentHash(syncContext) {
}
async function syncRequest(syncContext, method, requestPath, body) {
return await request.exec({
const timeout = await syncOptions.getSyncTimeout();
const opts = {
method,
url: await syncOptions.getSyncServerHost() + requestPath,
cookieJar: syncContext.cookieJar,
timeout: await syncOptions.getSyncTimeout(),
timeout: timeout,
body,
proxy: proxyToggle ? await syncOptions.getSyncProxy() : null
});
};
return await utils.timeLimit(request.exec(opts), timeout);
}
const primaryKeys = {
@@ -369,7 +372,7 @@ sqlInit.dbReady.then(async () => {
setInterval(cls.wrap(sync), 60000);
// kickoff initial sync immediately
setTimeout(cls.wrap(sync), 1000);
setTimeout(cls.wrap(sync), 3000);
setInterval(cls.wrap(updatePushStats), 1000);
});
@@ -380,4 +383,4 @@ module.exports = {
getSyncRecords,
stats,
getMaxSyncId
};
};

View File

@@ -5,6 +5,7 @@ const randtoken = require('rand-token').generator({source: 'crypto'});
const unescape = require('unescape');
const escape = require('escape-html');
const sanitize = require("sanitize-filename");
const mimeTypes = require('mime-types');
function newEntityId() {
return randomString(12);
@@ -166,10 +167,72 @@ function isStringNote(type, mime) {
|| STRING_MIME_TYPES.includes(mime);
}
function replaceAll(string, replaceWhat, replaceWith) {
const escapedWhat = replaceWhat.replace(/([\/,!\\^${}\[\]().*+?|<>\-&])/g, "\\$&");
function quoteRegex(url) {
return url.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}
return string.replace(new RegExp(escapedWhat, "g"), replaceWith);
function replaceAll(string, replaceWhat, replaceWith) {
const quotedReplaceWhat = quoteRegex(replaceWhat);
return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith);
}
function formatDownloadTitle(filename, type, mime) {
if (!filename) {
filename = "untitled";
}
if (type === 'text') {
return filename + '.html';
} else if (['relation-map', 'search'].includes(type)) {
return filename + '.json';
} else {
if (!mime) {
return filename;
}
mime = mime.toLowerCase();
const filenameLc = filename.toLowerCase();
const extensions = mimeTypes.extensions[mime];
if (!extensions || extensions.length === 0) {
return filename;
}
for (const ext of extensions) {
if (filenameLc.endsWith('.' + ext)) {
return filename;
}
}
if (mime === 'application/octet-stream') {
// we didn't find any good guess for this one, it will be better to just return
// the current name without fake extension. It's possible that the title still preserves to correct
// extension too
return filename;
}
return filename + '.' + extensions[0];
}
}
function timeLimit(promise, limitMs) {
return new Promise((res, rej) => {
let resolved = false;
promise.then(result => {
resolved = true;
res(result);
});
setTimeout(() => {
if (!resolved) {
rej(new Error('Process exceeded time limit ' + limitMs));
}
}, limitMs);
});
}
module.exports = {
@@ -198,5 +261,8 @@ module.exports = {
sanitizeFilenameForHeader,
getContentDisposition,
isStringNote,
replaceAll
};
quoteRegex,
replaceAll,
formatDownloadTitle,
timeLimit
};

View File

@@ -165,6 +165,5 @@ module.exports = {
createMainWindow,
createSetupWindow,
closeSetupWindow,
createExtraWindow,
registerGlobalShortcuts
};

View File

@@ -36,7 +36,7 @@ function init(httpServer, sessionParser) {
const message = JSON.parse(messageJson);
if (message.type === 'log-error') {
log.error('JS Error: ' + message.error);
log.info('JS Error: ' + message.error + '\r\nStack: ' + message.stack);
}
else if (message.type === 'ping') {
lastAcceptedSyncIds[ws.id] = message.lastSyncId;
@@ -141,4 +141,4 @@ module.exports = {
syncPullInProgress,
syncPullFinished,
sendPingToAllClients
};
};

View File

@@ -5,7 +5,7 @@
<link rel="shortcut icon" href="favicon.ico">
<title>Trilium Notes</title>
</head>
<body class="desktop theme-<%= theme %>" style="display: none; --main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;">
<body class="desktop theme-<%= theme %>" style="--main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;">
<noscript>Trilium requires JavaScript to be enabled.</noscript>
<div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div>
@@ -83,9 +83,5 @@
<link rel="stylesheet" type="text/css" href="libraries/boxicons/css/boxicons.min.css">
<script>
$("body").show();
</script>
</body>
</html>

View File

@@ -210,7 +210,7 @@
<script src="libraries/knockout.min.js"></script>
<script src="app-dist/setup.js" crossorigin type="module"></script>
<script src="app/setup.js" crossorigin type="module"></script>
<link href="stylesheets/themes.css" rel="stylesheet">
</body>
</html>