mirror of
https://github.com/zadam/trilium.git
synced 2025-10-26 15:56:29 +01:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0395e9866 | ||
|
|
89aa4fbc73 | ||
|
|
74a7802088 | ||
|
|
a89b6711d1 | ||
|
|
b2549b2834 | ||
|
|
959c4cbe64 | ||
|
|
d03d3603d2 | ||
|
|
e1c2573778 | ||
|
|
2af2b45062 | ||
|
|
eabe4775bd | ||
|
|
da9b321aa0 | ||
|
|
c18d8d2d1b | ||
|
|
5f2361ebd5 | ||
|
|
9791dab97d | ||
|
|
85d986534d | ||
|
|
00faf758e8 | ||
|
|
6ba2e5cf73 | ||
|
|
fb3876d28b | ||
|
|
fb975849b9 | ||
|
|
16fef78344 | ||
|
|
e0b4b369dc | ||
|
|
0df7851214 | ||
|
|
5d47c2b23e | ||
|
|
d09b021487 | ||
|
|
910bda860c | ||
|
|
2d92b4931a | ||
|
|
212b719ee9 | ||
|
|
1db892d22f | ||
|
|
535dcb6d12 | ||
|
|
4426362799 | ||
|
|
2c609e8136 | ||
|
|
11b73b79ed | ||
|
|
e70c862e72 | ||
|
|
b3e66d5a83 | ||
|
|
e8cd821e57 | ||
|
|
be7ac74235 | ||
|
|
58fa0832f6 | ||
|
|
1502b9ce66 | ||
|
|
7307ca385f | ||
|
|
c1fd9825aa | ||
|
|
9de7d3fc53 | ||
|
|
3c5db844ba | ||
|
|
e7330c1104 | ||
|
|
ec4586b164 | ||
|
|
91e5f24798 | ||
|
|
38723e0189 | ||
|
|
8c88ce6f65 | ||
|
|
4d22959e28 | ||
|
|
50a28d8c51 | ||
|
|
e25b633ec4 | ||
|
|
75bd395669 | ||
|
|
5e353a5612 | ||
|
|
6b359b7796 | ||
|
|
13f9d037dc | ||
|
|
1911d64c1c | ||
|
|
d4c3f1b3f2 | ||
|
|
3db84daf94 | ||
|
|
2526715aa4 | ||
|
|
04c573e212 | ||
|
|
58f4f5d1e6 | ||
|
|
fa5d982a55 | ||
|
|
108afe8896 | ||
|
|
37da0adb8a | ||
|
|
4f50864ec8 | ||
|
|
30b9ef8604 | ||
|
|
b063b4c528 | ||
|
|
e08b0141a4 | ||
|
|
9d8b8e26a1 | ||
|
|
cb70109ee7 | ||
|
|
e541abbd60 | ||
|
|
940a70adc5 | ||
|
|
88e8eb7e9c | ||
|
|
b365c186a1 | ||
|
|
64c9734f05 | ||
|
|
48c843c087 | ||
|
|
0e4eec10b9 | ||
|
|
a3661cb763 | ||
|
|
115879ec4a |
4
.idea/dataSources.xml
generated
4
.idea/dataSources.xml
generated
@@ -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>
|
||||
@@ -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>""</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>''</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>""</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>""</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>""</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>''</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>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="82" parent="11" name="mime">
|
||||
<Position>13</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="83" parent="11" name="hash">
|
||||
<Position>14</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</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>"note"</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>'text'</DefaultExpression>
|
||||
</column>
|
||||
<column id="96" parent="12" name="mime">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'text/html'</DefaultExpression>
|
||||
</column>
|
||||
<column id="97" parent="12" name="hash">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</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>""</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>""</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>
|
||||
@@ -1,2 +0,0 @@
|
||||
#n:main
|
||||
!<md> [0, 0, null, null, -2147483648, -2147483648]
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:12.16.2-alpine
|
||||
FROM node:12.16.3-alpine
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
db/demo.zip
BIN
db/demo.zip
Binary file not shown.
2
libraries/ckeditor/ckeditor.js
vendored
2
libraries/ckeditor/ckeditor.js
vendored
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
88
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
13
src/app.js
13
src/app.js
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(' ')
|
||||
.append($eraseRevisionButton)
|
||||
.append(' ');
|
||||
|
||||
|
||||
@@ -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.");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,4 +100,4 @@ export default class NoteTitleWidget extends TabAwareWidget {
|
||||
beforeUnloadEvent() {
|
||||
this.spacedUpdate.updateNowIfNecessary();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const anonymization = require('../../services/anonymization');
|
||||
|
||||
async function anonymize() {
|
||||
await anonymization.anonymize();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
anonymize
|
||||
};
|
||||
@@ -199,4 +199,4 @@ module.exports = {
|
||||
getEffectiveNoteAttributes,
|
||||
createRelation,
|
||||
deleteRelation
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -165,6 +165,5 @@ module.exports = {
|
||||
createMainWindow,
|
||||
createSetupWindow,
|
||||
closeSetupWindow,
|
||||
createExtraWindow,
|
||||
registerGlobalShortcuts
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user