mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 00:06:30 +01:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c07785be67 | ||
|
|
80d2457b23 | ||
|
|
5dde2752d2 | ||
|
|
8bf4633cd0 | ||
|
|
bd66b8a1c8 | ||
|
|
be51e533fc | ||
|
|
f47ae12019 | ||
|
|
cab54a458f | ||
|
|
a30734f1bc | ||
|
|
7ad9f7b129 | ||
|
|
40a32e6826 | ||
|
|
ab0486aaf1 | ||
|
|
874593a167 | ||
|
|
03bf33630e | ||
|
|
933cce1b94 | ||
|
|
4a6ff573f8 | ||
|
|
1a737f7d19 | ||
|
|
cb69914f09 | ||
|
|
a372cbb2df | ||
|
|
0ce5caefe8 | ||
|
|
94dabb81f6 | ||
|
|
cd45bcfd03 | ||
|
|
49a53f7a45 | ||
|
|
9fa6c0918c | ||
|
|
e8d089e37e | ||
|
|
a931ce25fa | ||
|
|
b507abb4f7 | ||
|
|
66e7c6de62 | ||
|
|
4ce5ea9886 | ||
|
|
8c54b62f07 | ||
|
|
85eb50ed0f | ||
|
|
5ffd621e9d | ||
|
|
df93cb09da | ||
|
|
bbf04209f0 | ||
|
|
834bfa39c7 | ||
|
|
52b445f70b | ||
|
|
7b9b4fbb0c | ||
|
|
5af0ba1fcb | ||
|
|
85a9748291 | ||
|
|
b4005a7ffe | ||
|
|
82de1c88d4 | ||
|
|
1687ed7e0b | ||
|
|
c8b9c7d936 | ||
|
|
d57057ba28 | ||
|
|
66cee8daa4 | ||
|
|
afd7df0942 | ||
|
|
bd6ae33d32 | ||
|
|
70660a0d68 | ||
|
|
cdad18551a | ||
|
|
592c51d1a5 | ||
|
|
6a57b8a7e7 | ||
|
|
7a94e21c54 |
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<dataSource name="document.db">
|
||||
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.8">
|
||||
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.9">
|
||||
<root id="1">
|
||||
<ServerVersion>3.16.1</ServerVersion>
|
||||
</root>
|
||||
@@ -50,542 +50,616 @@
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<index id="24" parent="6" name="sqlite_autoindex_api_tokens_1">
|
||||
<column id="24" parent="6" name="hash">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="25" 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">
|
||||
<key id="26" parent="6">
|
||||
<ColNames>apiTokenId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="26" parent="7" name="branchId">
|
||||
<column id="27" parent="7" name="branchId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="27" parent="7" name="noteId">
|
||||
<column id="28" parent="7" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="28" parent="7" name="parentNoteId">
|
||||
<column id="29" parent="7" name="parentNoteId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="29" parent="7" name="notePosition">
|
||||
<column id="30" parent="7" name="notePosition">
|
||||
<Position>4</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="30" parent="7" name="prefix">
|
||||
<column id="31" parent="7" name="prefix">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="31" parent="7" name="isExpanded">
|
||||
<column id="32" parent="7" name="isExpanded">
|
||||
<Position>6</Position>
|
||||
<DataType>BOOLEAN|0s</DataType>
|
||||
</column>
|
||||
<column id="32" parent="7" name="isDeleted">
|
||||
<column id="33" parent="7" name="isDeleted">
|
||||
<Position>7</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="33" parent="7" name="dateModified">
|
||||
<column id="34" parent="7" name="dateModified">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="34" parent="7" name="sqlite_autoindex_branches_1">
|
||||
<column id="35" parent="7" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="36" parent="7" name="dateCreated">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'1970-01-01T00:00:00.000Z'</DefaultExpression>
|
||||
</column>
|
||||
<index id="37" parent="7" name="sqlite_autoindex_branches_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>branchId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="35" parent="7" name="IDX_branches_noteId_parentNoteId">
|
||||
<index id="38" parent="7" name="IDX_branches_noteId_parentNoteId">
|
||||
<ColNames>noteId
|
||||
parentNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="36" parent="7" name="IDX_branches_noteId">
|
||||
<index id="39" parent="7" name="IDX_branches_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="37" parent="7">
|
||||
<index id="40" parent="7" name="IDX_branches_parentNoteId">
|
||||
<ColNames>parentNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="41" parent="7">
|
||||
<ColNames>branchId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="38" parent="8" name="id">
|
||||
<column id="42" parent="8" name="id">
|
||||
<Position>1</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<SequenceIdentity>1</SequenceIdentity>
|
||||
</column>
|
||||
<column id="39" parent="8" name="noteId">
|
||||
<column id="43" parent="8" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="40" parent="8" name="comment">
|
||||
<column id="44" parent="8" name="comment">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="41" parent="8" name="dateAdded">
|
||||
<column id="45" parent="8" name="dateCreated">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<key id="42" parent="8">
|
||||
<key id="46" parent="8">
|
||||
<ColNames>id</ColNames>
|
||||
<Primary>1</Primary>
|
||||
</key>
|
||||
<column id="43" parent="9" name="imageId">
|
||||
<column id="47" parent="9" name="imageId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="44" parent="9" name="format">
|
||||
<column id="48" parent="9" name="format">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="45" parent="9" name="checksum">
|
||||
<column id="49" parent="9" name="checksum">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="46" parent="9" name="name">
|
||||
<column id="50" parent="9" name="name">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="47" parent="9" name="data">
|
||||
<column id="51" parent="9" name="data">
|
||||
<Position>5</Position>
|
||||
<DataType>BLOB|0s</DataType>
|
||||
</column>
|
||||
<column id="48" parent="9" name="isDeleted">
|
||||
<column id="52" parent="9" name="isDeleted">
|
||||
<Position>6</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="49" parent="9" name="dateModified">
|
||||
<column id="53" parent="9" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="50" parent="9" name="dateCreated">
|
||||
<column id="54" parent="9" name="dateCreated">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="51" parent="9" name="sqlite_autoindex_images_1">
|
||||
<column id="55" parent="9" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="56" parent="9" name="sqlite_autoindex_images_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="52" parent="9">
|
||||
<key id="57" parent="9">
|
||||
<ColNames>imageId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="53" parent="10" name="labelId">
|
||||
<column id="58" parent="10" name="labelId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="54" parent="10" name="noteId">
|
||||
<column id="59" parent="10" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="55" parent="10" name="name">
|
||||
<column id="60" parent="10" name="name">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="56" parent="10" name="value">
|
||||
<column id="61" parent="10" name="value">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="57" parent="10" name="position">
|
||||
<column id="62" parent="10" name="position">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="58" parent="10" name="dateCreated">
|
||||
<column id="63" parent="10" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="59" parent="10" name="dateModified">
|
||||
<column id="64" parent="10" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="60" parent="10" name="isDeleted">
|
||||
<column id="65" parent="10" name="isDeleted">
|
||||
<Position>8</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="61" parent="10" name="sqlite_autoindex_labels_1">
|
||||
<column id="66" parent="10" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="67" parent="10" name="sqlite_autoindex_labels_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>labelId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="62" parent="10" name="IDX_labels_noteId">
|
||||
<index id="68" parent="10" name="IDX_labels_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="63" parent="10" name="IDX_labels_name_value">
|
||||
<index id="69" parent="10" name="IDX_labels_name_value">
|
||||
<ColNames>name
|
||||
value</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="64" parent="10">
|
||||
<key id="70" parent="10">
|
||||
<ColNames>labelId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="65" parent="11" name="noteImageId">
|
||||
<column id="71" parent="11" name="noteImageId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="66" parent="11" name="noteId">
|
||||
<column id="72" parent="11" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="67" parent="11" name="imageId">
|
||||
<column id="73" parent="11" name="imageId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="68" parent="11" name="isDeleted">
|
||||
<column id="74" parent="11" name="isDeleted">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="69" parent="11" name="dateModified">
|
||||
<column id="75" parent="11" name="dateModified">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="70" parent="11" name="dateCreated">
|
||||
<column id="76" parent="11" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="71" parent="11" name="sqlite_autoindex_note_images_1">
|
||||
<column id="77" parent="11" name="hash">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="78" parent="11" name="sqlite_autoindex_note_images_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteImageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="72" parent="11" name="IDX_note_images_noteId_imageId">
|
||||
<index id="79" parent="11" name="IDX_note_images_noteId_imageId">
|
||||
<ColNames>noteId
|
||||
imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="73" parent="11" name="IDX_note_images_noteId">
|
||||
<index id="80" parent="11" name="IDX_note_images_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="74" parent="11" name="IDX_note_images_imageId">
|
||||
<index id="81" parent="11" name="IDX_note_images_imageId">
|
||||
<ColNames>imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="75" parent="11">
|
||||
<key id="82" parent="11">
|
||||
<ColNames>noteImageId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="76" parent="12" name="noteRevisionId">
|
||||
<column id="83" parent="12" name="noteRevisionId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="77" parent="12" name="noteId">
|
||||
<column id="84" parent="12" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="78" parent="12" name="title">
|
||||
<column id="85" parent="12" name="title">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="79" parent="12" name="content">
|
||||
<column id="86" parent="12" name="content">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="80" parent="12" name="isProtected">
|
||||
<column id="87" parent="12" name="isProtected">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="81" parent="12" name="dateModifiedFrom">
|
||||
<column id="88" parent="12" name="dateModifiedFrom">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="82" parent="12" name="dateModifiedTo">
|
||||
<column id="89" parent="12" name="dateModifiedTo">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="83" parent="12" name="type">
|
||||
<column id="90" parent="12" name="type">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="84" parent="12" name="mime">
|
||||
<column id="91" parent="12" name="mime">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<index id="85" parent="12" name="sqlite_autoindex_note_revisions_1">
|
||||
<column id="92" parent="12" name="hash">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="93" parent="12" name="sqlite_autoindex_note_revisions_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteRevisionId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="86" parent="12" name="IDX_note_revisions_noteId">
|
||||
<index id="94" parent="12" name="IDX_note_revisions_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="87" parent="12" name="IDX_note_revisions_dateModifiedFrom">
|
||||
<index id="95" parent="12" name="IDX_note_revisions_dateModifiedFrom">
|
||||
<ColNames>dateModifiedFrom</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="88" parent="12" name="IDX_note_revisions_dateModifiedTo">
|
||||
<index id="96" parent="12" name="IDX_note_revisions_dateModifiedTo">
|
||||
<ColNames>dateModifiedTo</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="89" parent="12">
|
||||
<key id="97" parent="12">
|
||||
<ColNames>noteRevisionId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="90" parent="13" name="noteId">
|
||||
<column id="98" parent="13" name="noteId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="91" parent="13" name="title">
|
||||
<column id="99" parent="13" name="title">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>"unnamed"</DefaultExpression>
|
||||
</column>
|
||||
<column id="92" parent="13" name="content">
|
||||
<column id="100" parent="13" name="content">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="93" parent="13" name="isProtected">
|
||||
<column id="101" parent="13" name="isProtected">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="94" parent="13" name="isDeleted">
|
||||
<column id="102" parent="13" name="isDeleted">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="95" parent="13" name="dateCreated">
|
||||
<column id="103" parent="13" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="96" parent="13" name="dateModified">
|
||||
<column id="104" parent="13" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="97" parent="13" name="type">
|
||||
<column id="105" parent="13" name="type">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'text'</DefaultExpression>
|
||||
</column>
|
||||
<column id="98" parent="13" name="mime">
|
||||
<column id="106" parent="13" name="mime">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'text/html'</DefaultExpression>
|
||||
</column>
|
||||
<index id="99" parent="13" name="sqlite_autoindex_notes_1">
|
||||
<column id="107" parent="13" name="hash">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="108" parent="13" name="sqlite_autoindex_notes_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="100" parent="13" name="IDX_notes_isDeleted">
|
||||
<ColNames>isDeleted</ColNames>
|
||||
<index id="109" parent="13" name="IDX_notes_type">
|
||||
<ColNames>type</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="101" parent="13">
|
||||
<key id="110" parent="13">
|
||||
<ColNames>noteId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="102" parent="14" name="name">
|
||||
<column id="111" parent="14" name="name">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="103" parent="14" name="value">
|
||||
<column id="112" parent="14" name="value">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="104" parent="14" name="dateModified">
|
||||
<column id="113" parent="14" name="dateModified">
|
||||
<Position>3</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
</column>
|
||||
<column id="105" parent="14" name="isSynced">
|
||||
<column id="114" parent="14" name="isSynced">
|
||||
<Position>4</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<index id="106" parent="14" name="sqlite_autoindex_options_1">
|
||||
<column id="115" parent="14" name="hash">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="116" parent="14" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'1970-01-01T00:00:00.000Z'</DefaultExpression>
|
||||
</column>
|
||||
<index id="117" parent="14" name="sqlite_autoindex_options_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>name</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="107" parent="14">
|
||||
<key id="118" parent="14">
|
||||
<ColNames>name</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="108" parent="15" name="branchId">
|
||||
<column id="119" parent="15" name="branchId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="109" parent="15" name="notePath">
|
||||
<column id="120" parent="15" name="notePath">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="110" parent="15" name="dateAccessed">
|
||||
<column id="121" parent="15" name="dateCreated">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="111" parent="15" name="isDeleted">
|
||||
<column id="122" parent="15" name="isDeleted">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
</column>
|
||||
<index id="112" parent="15" name="sqlite_autoindex_recent_notes_1">
|
||||
<column id="123" parent="15" name="hash">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="124" parent="15" name="sqlite_autoindex_recent_notes_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>branchId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="113" parent="15">
|
||||
<key id="125" parent="15">
|
||||
<ColNames>branchId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="114" parent="16" name="sourceId">
|
||||
<column id="126" parent="16" name="sourceId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="115" parent="16" name="dateCreated">
|
||||
<column id="127" parent="16" name="dateCreated">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="116" parent="16" name="sqlite_autoindex_source_ids_1">
|
||||
<index id="128" parent="16" name="sqlite_autoindex_source_ids_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>sourceId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="117" parent="16">
|
||||
<key id="129" parent="16">
|
||||
<ColNames>sourceId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="118" parent="17" name="type">
|
||||
<column id="130" parent="17" name="type">
|
||||
<Position>1</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="119" parent="17" name="name">
|
||||
<column id="131" parent="17" name="name">
|
||||
<Position>2</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="120" parent="17" name="tbl_name">
|
||||
<column id="132" parent="17" name="tbl_name">
|
||||
<Position>3</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="121" parent="17" name="rootpage">
|
||||
<column id="133" parent="17" name="rootpage">
|
||||
<Position>4</Position>
|
||||
<DataType>integer|0s</DataType>
|
||||
</column>
|
||||
<column id="122" parent="17" name="sql">
|
||||
<column id="134" parent="17" name="sql">
|
||||
<Position>5</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="123" parent="18" name="name">
|
||||
<column id="135" parent="18" name="name">
|
||||
<Position>1</Position>
|
||||
</column>
|
||||
<column id="124" parent="18" name="seq">
|
||||
<column id="136" parent="18" name="seq">
|
||||
<Position>2</Position>
|
||||
</column>
|
||||
<column id="125" parent="19" name="id">
|
||||
<column id="137" parent="19" name="id">
|
||||
<Position>1</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<SequenceIdentity>1</SequenceIdentity>
|
||||
</column>
|
||||
<column id="126" parent="19" name="entityName">
|
||||
<column id="138" parent="19" name="entityName">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="127" parent="19" name="entityId">
|
||||
<column id="139" parent="19" name="entityId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="128" parent="19" name="sourceId">
|
||||
<column id="140" parent="19" name="sourceId">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="129" parent="19" name="syncDate">
|
||||
<column id="141" parent="19" name="syncDate">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="130" parent="19" name="IDX_sync_entityName_entityId">
|
||||
<index id="142" parent="19" name="IDX_sync_entityName_entityId">
|
||||
<ColNames>entityName
|
||||
entityId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="131" parent="19" name="IDX_sync_syncDate">
|
||||
<index id="143" parent="19" name="IDX_sync_syncDate">
|
||||
<ColNames>syncDate</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="132" parent="19">
|
||||
<key id="144" parent="19">
|
||||
<ColNames>id</ColNames>
|
||||
<Primary>1</Primary>
|
||||
</key>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('root', 'root', 'none', 0, null, 1, 0, '2018-01-01T00:00:00.000Z');
|
||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('dLgtLUFn3GoN', '1Heh2acXfPNt', 'root', 21, null, 1, 0, '2017-12-23T00:46:39.304Z');
|
||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QLfS835GSfIh', '3RkyK9LI18dO', '1Heh2acXfPNt', 1, null, 1, 0, '2017-12-23T01:20:04.181Z');
|
||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QJAcYJ1gGUh9', 'L1Ox40M1aEyy', '3RkyK9LI18dO', 0, null, 0, 0, '2017-12-23T01:20:45.365Z');
|
||||
|
||||
5
db/migrations/0089__add_root_branch.sql
Normal file
5
db/migrations/0089__add_root_branch.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, dateModified)
|
||||
VALUES ('root', 'root', 'none', 0, null, 1, '2018-01-01T00:00:00.000Z');
|
||||
|
||||
INSERT INTO sync (entityName, entityId, sourceId, syncDate)
|
||||
VALUES ('branches' ,'root', 'SYNC_FILL', '2018-01-01T00:00:00.000Z');
|
||||
1
db/migrations/0090__branch_index.sql
Normal file
1
db/migrations/0090__branch_index.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
|
||||
2
db/migrations/0091__drop_isDeleted_index.sql
Normal file
2
db/migrations/0091__drop_isDeleted_index.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- index confuses planner and is mostly useless anyway since we're mostly used in non-deleted notes (which are presumably majority)
|
||||
DROP INDEX IDX_notes_isDeleted;
|
||||
2
db/migrations/0092__add_type_index.sql
Normal file
2
db/migrations/0092__add_type_index.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
create index IDX_notes_type
|
||||
on notes (type);
|
||||
9
db/migrations/0093__add_hash_field.sql
Normal file
9
db/migrations/0093__add_hash_field.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE notes ADD hash TEXT DEFAULT "" NOT NULL;
|
||||
ALTER TABLE branches ADD hash TEXT DEFAULT "" NOT NULL;
|
||||
ALTER TABLE note_revisions ADD hash TEXT DEFAULT "" NOT NULL;
|
||||
ALTER TABLE recent_notes ADD hash TEXT DEFAULT "" NOT NULL;
|
||||
ALTER TABLE options ADD hash TEXT DEFAULT "" NOT NULL;
|
||||
ALTER TABLE note_images ADD hash TEXT DEFAULT "" NOT NULL;
|
||||
ALTER TABLE images ADD hash TEXT DEFAULT "" NOT NULL;
|
||||
ALTER TABLE labels ADD hash TEXT DEFAULT "" NOT NULL;
|
||||
ALTER TABLE api_tokens ADD hash TEXT DEFAULT "" NOT NULL;
|
||||
30
db/migrations/0094__unify_auditing_fields.sql
Normal file
30
db/migrations/0094__unify_auditing_fields.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
ALTER TABLE branches ADD dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
|
||||
|
||||
CREATE TABLE `event_log_mig` (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`noteId` TEXT,
|
||||
`comment` TEXT,
|
||||
`dateCreated` TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO event_log_mig (id, noteId, comment, dateCreated)
|
||||
SELECT id, noteId, comment, dateAdded FROM event_log;
|
||||
|
||||
DROP TABLE event_log;
|
||||
ALTER TABLE event_log_mig RENAME TO event_log;
|
||||
|
||||
ALTER TABLE options ADD dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
|
||||
|
||||
CREATE TABLE `recent_notes_mig` (
|
||||
`branchId` TEXT NOT NULL PRIMARY KEY,
|
||||
`notePath` TEXT NOT NULL,
|
||||
hash TEXT DEFAULT "" NOT NULL,
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
isDeleted INT
|
||||
);
|
||||
|
||||
INSERT INTO recent_notes_mig (branchId, notePath, hash, dateCreated, isDeleted)
|
||||
SELECT branchId, notePath, hash, dateAccessed, isDeleted FROM recent_notes;
|
||||
|
||||
DROP TABLE recent_notes;
|
||||
ALTER TABLE recent_notes_mig RENAME TO recent_notes;
|
||||
1
db/migrations/0095__mime_type_for_render.sql
Normal file
1
db/migrations/0095__mime_type_for_render.sql
Normal file
@@ -0,0 +1 @@
|
||||
UPDATE notes SET mime = 'text/html' WHERE type = 'render';
|
||||
29
db/migrations/0096__unify_surrogate_keys.sql
Normal file
29
db/migrations/0096__unify_surrogate_keys.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
CREATE TABLE `event_log_mig` (
|
||||
`eventId` TEXT NOT NULL PRIMARY KEY,
|
||||
`noteId` TEXT,
|
||||
`comment` TEXT,
|
||||
`dateCreated` TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO event_log_mig (eventId, noteId, comment, dateCreated)
|
||||
SELECT id, noteId, comment, dateCreated FROM event_log;
|
||||
|
||||
DROP TABLE event_log;
|
||||
ALTER TABLE event_log_mig RENAME TO event_log;
|
||||
|
||||
create table options_mig
|
||||
(
|
||||
optionId TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT not null,
|
||||
value TEXT,
|
||||
dateModified INT,
|
||||
isSynced INTEGER default 0 not null,
|
||||
hash TEXT default "" not null,
|
||||
dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
|
||||
);
|
||||
|
||||
INSERT INTO options_mig (optionId, name, value, dateModified, isSynced, hash, dateCreated)
|
||||
SELECT name || "_key", name, value, dateModified, isSynced, hash, dateCreated FROM options;
|
||||
|
||||
DROP TABLE options;
|
||||
ALTER TABLE options_mig RENAME TO options;
|
||||
8509
package-lock.json
generated
8509
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.11.1",
|
||||
"version": "0.14.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"repository": {
|
||||
@@ -9,11 +9,11 @@
|
||||
"url": "https://github.com/zadam/trilium.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./bin/www",
|
||||
"start": "node ./src/www",
|
||||
"test-electron": "xo",
|
||||
"rebuild-electron": "electron-rebuild",
|
||||
"start-electron": "electron . --disable-gpu",
|
||||
"build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64",
|
||||
"build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64 --app-version=",
|
||||
"start-forge": "electron-forge start",
|
||||
"package-forge": "electron-forge package",
|
||||
"make-forge": "electron-forge make",
|
||||
@@ -21,22 +21,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.1.3",
|
||||
"axios": "^0.17.1",
|
||||
"body-parser": "~1.18.2",
|
||||
"axios": "^0.18",
|
||||
"body-parser": "^1.18.3",
|
||||
"cls-hooked": "^4.2.2",
|
||||
"cookie-parser": "~1.4.3",
|
||||
"debug": "~3.1.0",
|
||||
"devtron": "^1.4.0",
|
||||
"ejs": "~2.5.7",
|
||||
"electron": "^2.0.0-beta.5",
|
||||
"ejs": "~2.6.1",
|
||||
"electron-debug": "^1.5.0",
|
||||
"electron-dl": "^1.11.0",
|
||||
"electron-in-page-search": "^1.2.4",
|
||||
"electron-rebuild": "^1.7.3",
|
||||
"electron-dl": "^1.12.0",
|
||||
"electron-in-page-search": "^1.3.2",
|
||||
"express": "~4.16.3",
|
||||
"express-session": "^1.15.6",
|
||||
"fs-extra": "^4.0.3",
|
||||
"helmet": "^3.12.0",
|
||||
"fs-extra": "^6.0.1",
|
||||
"helmet": "^3.12.1",
|
||||
"html": "^1.0.0",
|
||||
"image-type": "^3.0.0",
|
||||
"imagemin": "^5.3.1",
|
||||
@@ -45,30 +43,34 @@
|
||||
"imagemin-pngquant": "^5.1.0",
|
||||
"ini": "^1.3.5",
|
||||
"jimp": "^0.2.28",
|
||||
"moment": "^2.21.0",
|
||||
"moment": "^2.22.1",
|
||||
"multer": "^1.3.0",
|
||||
"open": "0.0.5",
|
||||
"rand-token": "^0.4.0",
|
||||
"request": "^2.85.0",
|
||||
"rcedit": "^1.1.0",
|
||||
"request": "^2.87.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"rimraf": "^2.6.2",
|
||||
"sanitize-filename": "^1.6.1",
|
||||
"scrypt": "^6.0.3",
|
||||
"serve-favicon": "~2.4.5",
|
||||
"serve-favicon": "~2.5.0",
|
||||
"session-file-store": "^1.2.0",
|
||||
"simple-node-logger": "^0.93.37",
|
||||
"sqlite": "^2.9.1",
|
||||
"tar-stream": "^1.5.5",
|
||||
"sqlite": "^2.9.2",
|
||||
"tar-stream": "^1.6.1",
|
||||
"unescape": "^1.0.1",
|
||||
"ws": "^3.3.3"
|
||||
"ws": "^5.2.0",
|
||||
"xml2js": "^0.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^2.0.1",
|
||||
"electron-compile": "^6.4.2",
|
||||
"electron-packager": "^11.1.0",
|
||||
"electron-prebuilt-compile": "2.0.0-beta.5",
|
||||
"electron-packager": "^12.1.0",
|
||||
"electron-prebuilt-compile": "2.0.0",
|
||||
"electron-rebuild": "^1.7.3",
|
||||
"lorem-ipsum": "^1.0.4",
|
||||
"tape": "^4.9.0",
|
||||
"xo": "^0.18.0"
|
||||
"xo": "^0.21.1"
|
||||
},
|
||||
"config": {
|
||||
"forge": {
|
||||
|
||||
@@ -6,6 +6,7 @@ const dateUtils = require('../services/date_utils');
|
||||
class ApiToken extends Entity {
|
||||
static get tableName() { return "api_tokens"; }
|
||||
static get primaryKeyName() { return "apiTokenId"; }
|
||||
static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
@@ -8,6 +8,8 @@ const sql = require('../services/sql');
|
||||
class Branch extends Entity {
|
||||
static get tableName() { return "branches"; }
|
||||
static get primaryKeyName() { return "branchId"; }
|
||||
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
|
||||
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "dateModified", "isDeleted", "prefix"]; }
|
||||
|
||||
async getNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
|
||||
@@ -25,7 +27,11 @@ class Branch extends Entity {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate()
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,17 @@ class Entity {
|
||||
if (!this[this.constructor.primaryKeyName]) {
|
||||
this[this.constructor.primaryKeyName] = utils.newEntityId();
|
||||
}
|
||||
|
||||
let contentToHash = "";
|
||||
|
||||
for (const propertyName of this.constructor.hashedProperties) {
|
||||
contentToHash += "|" + this[propertyName];
|
||||
}
|
||||
|
||||
// this IF is to ease the migration from before hashed options, can be later removed
|
||||
if (this.constructor.tableName !== 'options' || this.isSynced) {
|
||||
this["hash"] = utils.hash(contentToHash).substr(0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
|
||||
@@ -6,6 +6,7 @@ const Branch = require('../entities/branch');
|
||||
const Label = require('../entities/label');
|
||||
const RecentNote = require('../entities/recent_note');
|
||||
const ApiToken = require('../entities/api_token');
|
||||
const Option = require('../entities/option');
|
||||
const repository = require('../services/repository');
|
||||
|
||||
function createEntityFromRow(row) {
|
||||
@@ -35,6 +36,9 @@ function createEntityFromRow(row) {
|
||||
else if (row.noteId) {
|
||||
entity = new Note(row);
|
||||
}
|
||||
else if (row.name) {
|
||||
entity = new Option(row);
|
||||
}
|
||||
else {
|
||||
throw new Error('Unknown entity type for row: ' + JSON.stringify(row));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const dateUtils = require('../services/date_utils');
|
||||
class Image extends Entity {
|
||||
static get tableName() { return "images"; }
|
||||
static get primaryKeyName() { return "imageId"; }
|
||||
static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateModified", "dateCreated"]; }
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
@@ -8,6 +8,7 @@ const sql = require('../services/sql');
|
||||
class Label extends Entity {
|
||||
static get tableName() { return "labels"; }
|
||||
static get primaryKeyName() { return "labelId"; }
|
||||
static get hashedProperties() { return ["labelId", "noteId", "name", "value", "dateModified", "dateCreated"]; }
|
||||
|
||||
async getNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const protected_session = require('../services/protected_session');
|
||||
const protectedSessionService = require('../services/protected_session');
|
||||
const repository = require('../services/repository');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
|
||||
class Note extends Entity {
|
||||
static get tableName() { return "notes"; }
|
||||
static get primaryKeyName() { return "noteId"; }
|
||||
static get hashedProperties() { return ["noteId", "title", "content", "type", "dateModified", "isProtected", "isDeleted"]; }
|
||||
|
||||
constructor(row) {
|
||||
super(row);
|
||||
|
||||
// check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
|
||||
if (this.isProtected && this.noteId) {
|
||||
protected_session.decryptNote(this);
|
||||
protectedSessionService.decryptNote(this);
|
||||
}
|
||||
|
||||
this.setContent(this.content);
|
||||
@@ -39,7 +40,7 @@ class Note extends Entity {
|
||||
}
|
||||
|
||||
isHtml() {
|
||||
return (this.type === "code" || this.type === "file") && this.mime === "text/html";
|
||||
return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
|
||||
}
|
||||
|
||||
getScriptEnv() {
|
||||
@@ -146,7 +147,7 @@ class Note extends Entity {
|
||||
}
|
||||
|
||||
if (this.isProtected) {
|
||||
protected_session.encryptNote(this);
|
||||
protectedSessionService.encryptNote(this);
|
||||
}
|
||||
|
||||
if (!this.isDeleted) {
|
||||
|
||||
@@ -7,6 +7,7 @@ const dateUtils = require('../services/date_utils');
|
||||
class NoteImage extends Entity {
|
||||
static get tableName() { return "note_images"; }
|
||||
static get primaryKeyName() { return "noteImageId"; }
|
||||
static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateModified", "dateCreated"]; }
|
||||
|
||||
async getNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const protected_session = require('../services/protected_session');
|
||||
const utils = require('../services/utils');
|
||||
const protectedSessionService = require('../services/protected_session');
|
||||
const repository = require('../services/repository');
|
||||
|
||||
class NoteRevision extends Entity {
|
||||
static get tableName() { return "note_revisions"; }
|
||||
static get primaryKeyName() { return "noteRevisionId"; }
|
||||
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "dateModifiedFrom", "dateModifiedTo"]; }
|
||||
|
||||
constructor(row) {
|
||||
super(row);
|
||||
|
||||
if (this.isProtected) {
|
||||
protected_session.decryptNoteRevision(this);
|
||||
protectedSessionService.decryptNoteRevision(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class NoteRevision extends Entity {
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isProtected) {
|
||||
protected_session.encryptNoteRevision(this);
|
||||
protectedSessionService.encryptNoteRevision(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
src/entities/option.js
Normal file
18
src/entities/option.js
Normal file
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
|
||||
class Option extends Entity {
|
||||
static get tableName() { return "options"; }
|
||||
static get primaryKeyName() { return "name"; }
|
||||
static get hashedProperties() { return ["name", "value"]; }
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Option;
|
||||
@@ -1,10 +1,24 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
|
||||
class RecentNote extends Entity {
|
||||
static get tableName() { return "recent_notes"; }
|
||||
static get primaryKeyName() { return "branchId"; }
|
||||
static get hashedProperties() { return ["branchId", "notePath", "dateCreated", "isDeleted"]; }
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RecentNote;
|
||||
BIN
src/public/images/icons/back.png
Normal file
BIN
src/public/images/icons/back.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 511 B |
BIN
src/public/images/icons/forward.png
Normal file
BIN
src/public/images/icons/forward.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 511 B |
BIN
src/public/images/icons/tree-root.png
Normal file
BIN
src/public/images/icons/tree-root.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 240 B |
1
src/public/images/trilium.svg
Normal file
1
src/public/images/trilium.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M63.966,45.043c0.008-0.009,0.021-0.021,0.027-0.029c0.938-1.156-0.823-13.453-5.063-20.125 c-1.389-2.186-2.239-3.423-3.219-4.719c-3.907-5.166-6-6.125-6-6.125S35.732,24.78,36.149,44.315 c-1.754,0.065-11.218,7.528-14.826,14.388c-1.206,2.291-1.856,3.645-2.493,5.141c-2.539,5.957-2.33,8.25-2.33,8.25 s16.271,6.79,33.014-3.294c0.007,0.021,0.013,0.046,0.02,0.063c0.537,1.389,12.08,5.979,19.976,5.621 c2.587-0.116,4.084-0.238,5.696-0.444c6.424-0.818,8.298-2.157,8.298-2.157S81.144,54.396,63.966,45.043z M50.787,65.343 c1.059-1.183,4.648-5.853,0.995-11.315c-0.253-0.377-0.496-0.236-0.496-0.236s0.063,10.822-5.162,12.359 c-5.225,1.537-13.886,4.4-20.427,0.455C25,66.186,26.924,53.606,38.544,47.229c0.546,1.599,2.836,6.854,9.292,6.409 c0.453-0.031,0.453-0.313,0.453-0.313s-9.422-5.328-8.156-10.625s3.089-14.236,9.766-17.948c0.714-0.397,10.746,7.593,10.417,20.94 c-1.606-0.319-7.377-1.004-10.226,4.864c-0.198,0.409,0.046,0.549,0.046,0.549s9.31-5.521,13.275-1.789 c3.965,3.733,10.813,9.763,10.71,17.4C74.111,67.533,62.197,72.258,50.787,65.343z M35.613,35.145c0,0-0.991,3.241-0.603,7.524 l-13.393-7.524C21.618,35.145,27.838,30.931,35.613,35.145z M21.193,36.03l13.344,7.612c-3.872,1.872-6.142,4.388-6.142,4.388 C20.78,43.531,21.193,36.03,21.193,36.03z M72.287,49.064c0,0-2.321-2.471-6.23-4.263l13.187-7.881 C79.243,36.92,79.808,44.413,72.287,49.064z M78.687,36.113l-13.237,7.794c0.3-4.291-0.754-7.511-0.754-7.511 C72.383,32.025,78.687,36.113,78.687,36.113z M42.076,73.778c0,0,3.309-0.737,6.845-3.185l0.056,15.361 C48.977,85.955,42.244,82.621,42.076,73.778z M49.956,85.888L50,70.526c3.539,2.445,6.846,3.181,6.846,3.181 C56.686,82.551,49.956,85.888,49.956,85.888z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -2,7 +2,8 @@ import cloningService from '../services/cloning.js';
|
||||
import linkService from '../services/link.js';
|
||||
import noteDetailService from '../services/note_detail.js';
|
||||
import treeUtils from '../services/tree_utils.js';
|
||||
import autocompleteService from '../services/autocomplete.js';
|
||||
import server from "../services/server.js";
|
||||
import noteDetailText from "../services/note_detail_text.js";
|
||||
|
||||
const $dialog = $("#add-link-dialog");
|
||||
const $form = $("#add-link-form");
|
||||
@@ -11,6 +12,7 @@ const $linkTitle = $("#link-title");
|
||||
const $clonePrefix = $("#clone-prefix");
|
||||
const $linkTitleFormGroup = $("#add-link-title-form-group");
|
||||
const $prefixFormGroup = $("#add-link-prefix-form-group");
|
||||
const $linkTypeDiv = $("#add-link-type-div");
|
||||
const $linkTypes = $("input[name='add-link-type']");
|
||||
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
|
||||
|
||||
@@ -45,16 +47,20 @@ async function showDialog() {
|
||||
$clonePrefix.val('');
|
||||
$linkTitle.val('');
|
||||
|
||||
function setDefaultLinkTitle(noteId) {
|
||||
const noteTitle = treeUtils.getNoteTitle(noteId);
|
||||
async function setDefaultLinkTitle(noteId) {
|
||||
const noteTitle = await treeUtils.getNoteTitle(noteId);
|
||||
|
||||
$linkTitle.val(noteTitle);
|
||||
}
|
||||
|
||||
$autoComplete.autocomplete({
|
||||
source: await autocompleteService.getAutocompleteItems(),
|
||||
minLength: 0,
|
||||
change: () => {
|
||||
source: async function(request, response) {
|
||||
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
|
||||
|
||||
response(result);
|
||||
},
|
||||
minLength: 2,
|
||||
change: async () => {
|
||||
const val = $autoComplete.val();
|
||||
const notePath = linkService.getNodePathFromLabel(val);
|
||||
if (!notePath) {
|
||||
@@ -64,16 +70,16 @@ async function showDialog() {
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
if (noteId) {
|
||||
setDefaultLinkTitle(noteId);
|
||||
await setDefaultLinkTitle(noteId);
|
||||
}
|
||||
},
|
||||
// this is called when user goes through autocomplete list with keyboard
|
||||
// at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
|
||||
focus: (event, ui) => {
|
||||
focus: async (event, ui) => {
|
||||
const notePath = linkService.getNodePathFromLabel(ui.item.value);
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
setDefaultLinkTitle(noteId);
|
||||
await setDefaultLinkTitle(noteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -92,7 +98,16 @@ $form.submit(() => {
|
||||
|
||||
$dialog.dialog("close");
|
||||
|
||||
linkService.addLinkToEditor(linkTitle, '#' + notePath);
|
||||
const linkHref = '#' + notePath;
|
||||
|
||||
if (hasSelection()) {
|
||||
const editor = noteDetailText.getEditor();
|
||||
|
||||
editor.execute('link', linkHref);
|
||||
}
|
||||
else {
|
||||
linkService.addLinkToEditor(linkTitle, linkHref);
|
||||
}
|
||||
}
|
||||
else if (linkType === 'selected-to-current') {
|
||||
const prefix = $clonePrefix.val();
|
||||
@@ -113,17 +128,21 @@ $form.submit(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
// returns true if user selected some text, false if there's no selection
|
||||
function hasSelection() {
|
||||
const model = noteDetailText.getEditor().model;
|
||||
const selection = model.document.selection;
|
||||
|
||||
return !selection.isCollapsed;
|
||||
}
|
||||
|
||||
function linkTypeChanged() {
|
||||
const value = $linkTypes.filter(":checked").val();
|
||||
|
||||
if (value === 'html') {
|
||||
$linkTitleFormGroup.show();
|
||||
$prefixFormGroup.hide();
|
||||
}
|
||||
else {
|
||||
$linkTitleFormGroup.hide();
|
||||
$prefixFormGroup.show();
|
||||
}
|
||||
$linkTitleFormGroup.toggle(!hasSelection() && value === 'html');
|
||||
$prefixFormGroup.toggle(!hasSelection() && value !== 'html');
|
||||
|
||||
$linkTypeDiv.toggle(!hasSelection());
|
||||
}
|
||||
|
||||
$linkTypes.change(linkTypeChanged);
|
||||
|
||||
@@ -25,7 +25,7 @@ async function showDialog() {
|
||||
|
||||
$treePrefixInput.val(branch.prefix).focus();
|
||||
|
||||
const noteTitle = treeUtils.getNoteTitle(currentNode.data.noteId);
|
||||
const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId);
|
||||
|
||||
$noteTitle.html(noteTitle);
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ async function showDialog() {
|
||||
$list.html('');
|
||||
|
||||
for (const event of result) {
|
||||
const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded));
|
||||
const dateTime = utils.formatDateTime(utils.parseDate(event.dateCreated));
|
||||
|
||||
if (event.noteId) {
|
||||
const noteLink = linkService.createNoteLink(event.noteId).prop('outerHTML');
|
||||
const noteLink = await linkService.createNoteLink(event.noteId).prop('outerHTML');
|
||||
|
||||
event.comment = event.comment.replace('<note>', noteLink);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import treeService from '../services/tree.js';
|
||||
import linkService from '../services/link.js';
|
||||
import utils from '../services/utils.js';
|
||||
import autocompleteService from '../services/autocomplete.js';
|
||||
import server from '../services/server.js';
|
||||
|
||||
const $dialog = $("#jump-to-note-dialog");
|
||||
const $autoComplete = $("#jump-to-note-autocomplete");
|
||||
@@ -18,8 +17,12 @@ async function showDialog() {
|
||||
});
|
||||
|
||||
await $autoComplete.autocomplete({
|
||||
source: await utils.stopWatch("building autocomplete", autocompleteService.getAutocompleteItems),
|
||||
minLength: 1
|
||||
source: async function(request, response) {
|
||||
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
|
||||
|
||||
response(result);
|
||||
},
|
||||
minLength: 2
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ async function showDialog() {
|
||||
noteLink = change.current_title;
|
||||
}
|
||||
else {
|
||||
noteLink = linkService.createNoteLink(change.noteId, change.title);
|
||||
noteLink = await linkService.createNoteLink(change.noteId, change.title);
|
||||
}
|
||||
|
||||
changesListEl.append($('<li>')
|
||||
|
||||
@@ -14,13 +14,15 @@ class NoteShort {
|
||||
}
|
||||
|
||||
async getBranches() {
|
||||
const branches = [];
|
||||
const branchIds = this.treeCache.parents[this.noteId].map(
|
||||
parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId));
|
||||
|
||||
for (const parent of this.treeCache.parents[this.noteId]) {
|
||||
branches.push(await this.treeCache.getBranchByChildParent(this.noteId, parent.noteId));
|
||||
}
|
||||
return this.treeCache.getBranches(branchIds);
|
||||
}
|
||||
|
||||
return branches;
|
||||
hasChildren() {
|
||||
return this.treeCache.children[this.noteId]
|
||||
&& this.treeCache.children[this.noteId].length > 0;
|
||||
}
|
||||
|
||||
async getChildBranches() {
|
||||
@@ -28,23 +30,28 @@ class NoteShort {
|
||||
return [];
|
||||
}
|
||||
|
||||
const branches = [];
|
||||
const branchIds = this.treeCache.children[this.noteId].map(
|
||||
childNoteId => this.treeCache.getBranchIdByChildParent(childNoteId, this.noteId));
|
||||
|
||||
for (const child of this.treeCache.children[this.noteId]) {
|
||||
branches.push(await this.treeCache.getBranchByChildParent(child.noteId, this.noteId));
|
||||
}
|
||||
|
||||
return branches;
|
||||
return await this.treeCache.getBranches(branchIds);
|
||||
}
|
||||
|
||||
async getParentNotes() {
|
||||
getParentNoteIds() {
|
||||
return this.treeCache.parents[this.noteId] || [];
|
||||
}
|
||||
|
||||
async getChildNotes() {
|
||||
async getParentNotes() {
|
||||
return await this.treeCache.getNotes(this.getParentNoteIds());
|
||||
}
|
||||
|
||||
getChildNoteIds() {
|
||||
return this.treeCache.children[this.noteId] || [];
|
||||
}
|
||||
|
||||
async getChildNotes() {
|
||||
return await this.treeCache.getNotes(this.getChildNoteIds());
|
||||
}
|
||||
|
||||
get toString() {
|
||||
return `Note(noteId=${this.noteId}, title=${this.title})`;
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import treeCache from "./tree_cache.js";
|
||||
import treeUtils from "./tree_utils.js";
|
||||
import protectedSessionHolder from './protected_session_holder.js';
|
||||
|
||||
async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
|
||||
if (!parentNoteId) {
|
||||
parentNoteId = 'root';
|
||||
}
|
||||
|
||||
const parentNote = await treeCache.getNote(parentNoteId);
|
||||
const childNotes = await parentNote.getChildNotes();
|
||||
|
||||
if (!childNotes.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!notePath) {
|
||||
notePath = '';
|
||||
}
|
||||
|
||||
if (!titlePath) {
|
||||
titlePath = '';
|
||||
}
|
||||
|
||||
const autocompleteItems = [];
|
||||
|
||||
for (const childNote of childNotes) {
|
||||
if (childNote.hideInAutocomplete) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId;
|
||||
const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId);
|
||||
|
||||
if (!childNote.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||
autocompleteItems.push({
|
||||
value: childTitlePath + ' (' + childNotePath + ')',
|
||||
label: childTitlePath
|
||||
});
|
||||
}
|
||||
|
||||
const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath);
|
||||
|
||||
for (const childItem of childItems) {
|
||||
autocompleteItems.push(childItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (parentNoteId === 'root') {
|
||||
console.log(`Generated ${autocompleteItems.length} autocomplete items`);
|
||||
}
|
||||
|
||||
return autocompleteItems;
|
||||
}
|
||||
|
||||
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
|
||||
$.ui.autocomplete.filter = (array, terms) => {
|
||||
if (!terms) {
|
||||
return array;
|
||||
}
|
||||
|
||||
const startDate = new Date();
|
||||
|
||||
const results = [];
|
||||
const tokens = terms.toLowerCase().split(" ");
|
||||
|
||||
for (const item of array) {
|
||||
const lcLabel = item.label.toLowerCase();
|
||||
|
||||
const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
|
||||
if (!found) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// this is not completely correct and might cause minor problems with note with names containing this " / "
|
||||
const lastSegmentIndex = lcLabel.lastIndexOf(" / ");
|
||||
|
||||
if (lastSegmentIndex !== -1) {
|
||||
const lastSegment = lcLabel.substr(lastSegmentIndex + 3);
|
||||
|
||||
// at least some token needs to be in the last segment (leaf note), otherwise this
|
||||
// particular note is not that interesting (query is satisfied by parent note)
|
||||
const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1);
|
||||
|
||||
if (!foundInLastSegment) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
results.push(item);
|
||||
|
||||
if (results.length > 100) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export default {
|
||||
getAutocompleteItems
|
||||
};
|
||||
1
src/public/javascripts/services/bootstrap.js
vendored
1
src/public/javascripts/services/bootstrap.js
vendored
@@ -35,6 +35,7 @@ import libraryLoader from "./library_loader.js";
|
||||
// required for CKEditor image upload plugin
|
||||
window.glob.getCurrentNode = treeService.getCurrentNode;
|
||||
window.glob.getHeaders = server.getHeaders;
|
||||
window.glob.showAddLinkDialog = addLinkDialog.showDialog;
|
||||
|
||||
// required for ESLint plugin
|
||||
window.glob.getCurrentNote = noteDetailService.getCurrentNote;
|
||||
|
||||
@@ -94,25 +94,31 @@ const contextMenuOptions = {
|
||||
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
|
||||
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
|
||||
{title: "----"},
|
||||
{title: "Export branch", cmd: "exportBranch", uiIcon: " ui-icon-arrowthick-1-ne"},
|
||||
{title: "Import into branch", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"},
|
||||
{title: "Export branch", cmd: "exportBranch", uiIcon: " ui-icon-arrowthick-1-ne", children: [
|
||||
{title: "Native Tar", cmd: "exportBranchToTar"},
|
||||
{title: "OPML", cmd: "exportBranchToOpml"}
|
||||
]},
|
||||
{title: "Import into branch (tar, opml)", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"},
|
||||
{title: "----"},
|
||||
{title: "Collapse branch <kbd>Alt+-</kbd>", cmd: "collapseBranch", uiIcon: "ui-icon-minus"},
|
||||
{title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"},
|
||||
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
|
||||
|
||||
],
|
||||
beforeOpen: async (event, ui) => {
|
||||
const node = $.ui.fancytree.getNode(ui.target);
|
||||
const branch = await treeCache.getBranch(node.data.branchId);
|
||||
const note = await treeCache.getNote(node.data.noteId);
|
||||
const parentNote = await treeCache.getNote(branch.parentNoteId);
|
||||
const isNotRoot = note.noteId !== 'root';
|
||||
|
||||
// Modify menu entries depending on node status
|
||||
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && (!parentNote || parentNote.type !== 'search'));
|
||||
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "insertNoteHere", !parentNote || parentNote.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "insertNoteHere", isNotRoot && parentNote.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "delete", isNotRoot);
|
||||
$tree.contextmenu("enableEntry", "copy", isNotRoot);
|
||||
$tree.contextmenu("enableEntry", "cut", isNotRoot);
|
||||
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "importBranch", note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search');
|
||||
|
||||
@@ -159,8 +165,11 @@ const contextMenuOptions = {
|
||||
else if (ui.cmd === "delete") {
|
||||
treeChangesService.deleteNodes(treeService.getSelectedNodes(true));
|
||||
}
|
||||
else if (ui.cmd === "exportBranch") {
|
||||
exportService.exportBranch(node.data.noteId);
|
||||
else if (ui.cmd === "exportBranchToTar") {
|
||||
exportService.exportBranch(node.data.noteId, 'tar');
|
||||
}
|
||||
else if (ui.cmd === "exportBranchToOpml") {
|
||||
exportService.exportBranch(node.data.noteId, 'opml');
|
||||
}
|
||||
else if (ui.cmd === "importBranch") {
|
||||
exportService.importBranch(node.data.noteId);
|
||||
|
||||
@@ -12,6 +12,7 @@ import recentChangesDialog from "../dialogs/recent_changes.js";
|
||||
import sqlConsoleDialog from "../dialogs/sql_console.js";
|
||||
import searchTreeService from "./search_tree.js";
|
||||
import labelsDialog from "../dialogs/labels.js";
|
||||
import protectedSessionService from "./protected_session.js";
|
||||
|
||||
function registerEntrypoints() {
|
||||
// hot keys are active also inside inputs and content editables
|
||||
@@ -31,6 +32,9 @@ function registerEntrypoints() {
|
||||
|
||||
$("#recent-changes-button").click(recentChangesDialog.showDialog);
|
||||
|
||||
$("#protected-session-on").click(protectedSessionService.enterProtectedSession);
|
||||
$("#protected-session-off").click(protectedSessionService.leaveProtectedSession);
|
||||
|
||||
$("#recent-notes-button").click(recentNotesDialog.showDialog);
|
||||
utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog);
|
||||
|
||||
@@ -45,6 +49,10 @@ function registerEntrypoints() {
|
||||
utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog);
|
||||
|
||||
if (utils.isElectron()) {
|
||||
$("#history-navigation").show();
|
||||
$("#history-back-button").click(window.history.back);
|
||||
$("#history-forward-button").click(window.history.forward);
|
||||
|
||||
utils.bindShortcut('alt+left', window.history.back);
|
||||
utils.bindShortcut('alt+right', window.history.forward);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import protectedSessionHolder from './protected_session_holder.js';
|
||||
import utils from './utils.js';
|
||||
import server from './server.js';
|
||||
|
||||
function exportBranch(noteId) {
|
||||
const url = utils.getHost() + "/api/notes/" + noteId + "/export?protectedSessionId="
|
||||
+ encodeURIComponent(protectedSessionHolder.getProtectedSessionId());
|
||||
function exportBranch(noteId, format) {
|
||||
const url = utils.getHost() + "/api/notes/" + noteId + "/export/" + format +
|
||||
"?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId());
|
||||
|
||||
utils.download(url);
|
||||
}
|
||||
@@ -29,7 +29,7 @@ $("#import-upload").change(async function() {
|
||||
type: 'POST',
|
||||
contentType: false, // NEEDED, DON'T OMIT THIS
|
||||
processData: false, // NEEDED, DON'T OMIT THIS
|
||||
});
|
||||
}).fail((xhr, status, error) => alert('Import error: ' + xhr.responseText));
|
||||
|
||||
await treeService.reload();
|
||||
});
|
||||
|
||||
@@ -23,11 +23,11 @@ function getNodePathFromLabel(label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function createNoteLink(notePath, noteTitle) {
|
||||
async function createNoteLink(notePath, noteTitle) {
|
||||
if (!noteTitle) {
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
noteTitle = treeUtils.getNoteTitle(noteId);
|
||||
noteTitle = await treeUtils.getNoteTitle(noteId);
|
||||
}
|
||||
|
||||
const noteLink = $("<a>", {
|
||||
@@ -76,9 +76,11 @@ function goToLink(e) {
|
||||
|
||||
function addLinkToEditor(linkTitle, linkHref) {
|
||||
const editor = noteDetailText.getEditor();
|
||||
const doc = editor.document;
|
||||
|
||||
doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection);
|
||||
editor.model.change( writer => {
|
||||
const insertPosition = editor.model.document.selection.getFirstPosition();
|
||||
writer.insertText(linkTitle, { linkHref: linkHref }, insertPosition);
|
||||
});
|
||||
}
|
||||
|
||||
function addTextToEditor(text) {
|
||||
|
||||
@@ -31,7 +31,8 @@ async function show() {
|
||||
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
|
||||
lint: true,
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
lineNumbers: true
|
||||
lineNumbers: true,
|
||||
tabindex: 2 // so that tab from title will lead to code editor focus
|
||||
});
|
||||
|
||||
codeEditor.on('change', noteDetailService.noteChanged);
|
||||
@@ -63,24 +64,27 @@ function focus() {
|
||||
}
|
||||
|
||||
async function executeCurrentNote() {
|
||||
if (noteDetailService.getCurrentNoteType() === 'code') {
|
||||
// make sure note is saved so we load latest changes
|
||||
await noteDetailService.saveNoteIfChanged();
|
||||
|
||||
const currentNote = noteDetailService.getCurrentNote();
|
||||
|
||||
if (currentNote.mime.endsWith("env=frontend")) {
|
||||
const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
|
||||
|
||||
bundleService.executeBundle(bundle);
|
||||
}
|
||||
|
||||
if (currentNote.mime.endsWith("env=backend")) {
|
||||
await server.post('script/run/' + noteDetailService.getCurrentNoteId());
|
||||
}
|
||||
|
||||
infoService.showMessage("Note executed");
|
||||
// ctrl+enter is also used elsewhere so make sure we're running only when appropriate
|
||||
if (noteDetailService.getCurrentNoteType() !== 'code') {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure note is saved so we load latest changes
|
||||
await noteDetailService.saveNoteIfChanged();
|
||||
|
||||
const currentNote = noteDetailService.getCurrentNote();
|
||||
|
||||
if (currentNote.mime.endsWith("env=frontend")) {
|
||||
const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
|
||||
|
||||
bundleService.executeBundle(bundle);
|
||||
}
|
||||
|
||||
if (currentNote.mime.endsWith("env=backend")) {
|
||||
await server.post('script/run/' + noteDetailService.getCurrentNoteId());
|
||||
}
|
||||
|
||||
infoService.showMessage("Note executed");
|
||||
}
|
||||
|
||||
$(document).bind('keydown', "ctrl+return", executeCurrentNote);
|
||||
|
||||
@@ -1,21 +1,73 @@
|
||||
import bundleService from "./bundle.js";
|
||||
import server from "./server.js";
|
||||
import noteDetailService from "./note_detail.js";
|
||||
import noteDetailCodeService from "./note_detail_code.js";
|
||||
|
||||
const $noteDetailCode = $('#note-detail-code');
|
||||
const $noteDetailRender = $('#note-detail-render');
|
||||
const $toggleEditButton = $('#toggle-edit-button');
|
||||
const $renderButton = $('#render-button');
|
||||
|
||||
let codeEditorInitialized;
|
||||
|
||||
async function show() {
|
||||
codeEditorInitialized = false;
|
||||
|
||||
$noteDetailRender.show();
|
||||
|
||||
await render();
|
||||
}
|
||||
|
||||
async function toggleEdit() {
|
||||
if ($noteDetailCode.is(":visible")) {
|
||||
$noteDetailCode.hide();
|
||||
}
|
||||
else {
|
||||
if (!codeEditorInitialized) {
|
||||
await noteDetailCodeService.show();
|
||||
|
||||
// because we can't properly scroll only the editor without scrolling the rendering
|
||||
// we limit its height
|
||||
$noteDetailCode.find('.CodeMirror').css('height', '300');
|
||||
|
||||
codeEditorInitialized = true;
|
||||
}
|
||||
else {
|
||||
$noteDetailCode.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$toggleEditButton.click(toggleEdit);
|
||||
|
||||
$renderButton.click(render);
|
||||
|
||||
async function render() {
|
||||
// ctrl+enter is also used elsewhere so make sure we're running only when appropriate
|
||||
if (noteDetailService.getCurrentNoteType() !== 'render') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (codeEditorInitialized) {
|
||||
await noteDetailService.saveNoteIfChanged();
|
||||
}
|
||||
|
||||
const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
|
||||
|
||||
$noteDetailRender.html(bundle.html);
|
||||
|
||||
// if the note is empty, it doesn't make sense to do render-only since nothing will be rendered
|
||||
if (!bundle.html.trim()) {
|
||||
toggleEdit();
|
||||
}
|
||||
|
||||
await bundleService.executeBundle(bundle);
|
||||
}
|
||||
|
||||
$(document).bind('keydown', "ctrl+return", render);
|
||||
|
||||
export default {
|
||||
show,
|
||||
getContent: () => null,
|
||||
getContent: noteDetailCodeService.getContent,
|
||||
focus: () => null
|
||||
}
|
||||
@@ -11,11 +11,10 @@ async function show() {
|
||||
|
||||
textEditor = await BalloonEditor.create($noteDetailText[0], {});
|
||||
|
||||
textEditor.document.on('change', noteDetailService.noteChanged);
|
||||
textEditor.model.document.on('change', noteDetailService.noteChanged);
|
||||
}
|
||||
|
||||
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
|
||||
textEditor.setData(noteDetailService.getCurrentNote().content || "<p></p>");
|
||||
textEditor.setData(noteDetailService.getCurrentNote().content);
|
||||
|
||||
$noteDetailText.show();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import server from './server.js';
|
||||
import infoService from "./info.js";
|
||||
|
||||
const $executeScriptButton = $("#execute-script-button");
|
||||
const $toggleEditButton = $('#toggle-edit-button');
|
||||
const $renderButton = $('#render-button');
|
||||
|
||||
const noteTypeModel = new NoteTypeModel();
|
||||
|
||||
function NoteTypeModel() {
|
||||
@@ -107,7 +110,7 @@ function NoteTypeModel() {
|
||||
|
||||
this.selectRender = function() {
|
||||
self.type('render');
|
||||
self.mime('');
|
||||
self.mime('text/html');
|
||||
|
||||
save();
|
||||
};
|
||||
@@ -128,6 +131,9 @@ function NoteTypeModel() {
|
||||
|
||||
this.updateExecuteScriptButtonVisibility = function() {
|
||||
$executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
|
||||
|
||||
$toggleEditButton.toggle(self.type() === 'render');
|
||||
$renderButton.toggle(self.type() === 'render');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,23 @@ const $password = $("#protected-session-password");
|
||||
const $noteDetailWrapper = $("#note-detail-wrapper");
|
||||
const $protectButton = $("#protect-button");
|
||||
const $unprotectButton = $("#unprotect-button");
|
||||
const $protectedSessionOnButton = $("#protected-session-on");
|
||||
const $protectedSessionOffButton = $("#protected-session-off");
|
||||
|
||||
let protectedSessionDeferred = null;
|
||||
|
||||
async function enterProtectedSession() {
|
||||
if (!protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||
await ensureProtectedSession(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function leaveProtectedSession() {
|
||||
if (protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||
utils.reloadApp();
|
||||
}
|
||||
}
|
||||
|
||||
function ensureProtectedSession(requireProtectedSession, modal) {
|
||||
const dfd = $.Deferred();
|
||||
|
||||
@@ -46,7 +60,7 @@ async function setupProtectedSession() {
|
||||
const password = $password.val();
|
||||
$password.val("");
|
||||
|
||||
const response = await enterProtectedSession(password);
|
||||
const response = await enterProtectedSessionOnServer(password);
|
||||
|
||||
if (!response.success) {
|
||||
infoService.showError("Wrong password.");
|
||||
@@ -67,6 +81,9 @@ async function setupProtectedSession() {
|
||||
|
||||
protectedSessionDeferred.resolve();
|
||||
|
||||
$protectedSessionOnButton.addClass('active');
|
||||
$protectedSessionOffButton.removeClass('active');
|
||||
|
||||
protectedSessionDeferred = null;
|
||||
}
|
||||
}
|
||||
@@ -81,7 +98,7 @@ function ensureDialogIsClosed() {
|
||||
$password.val('');
|
||||
}
|
||||
|
||||
async function enterProtectedSession(password) {
|
||||
async function enterProtectedSessionOnServer(password) {
|
||||
return await server.post('login/protected', {
|
||||
password: password
|
||||
});
|
||||
@@ -138,5 +155,7 @@ export default {
|
||||
protectNoteAndSendToServer,
|
||||
unprotectNoteAndSendToServer,
|
||||
protectBranch,
|
||||
ensureDialogIsClosed
|
||||
ensureDialogIsClosed,
|
||||
enterProtectedSession,
|
||||
leaveProtectedSession
|
||||
};
|
||||
@@ -17,11 +17,11 @@ import Branch from '../entities/branch.js';
|
||||
import NoteShort from '../entities/note_short.js';
|
||||
|
||||
const $tree = $("#tree");
|
||||
const $parentList = $("#parent-list");
|
||||
const $parentListList = $("#parent-list-inner");
|
||||
const $createTopLevelNoteButton = $("#create-top-level-note-button");
|
||||
const $collapseTreeButton = $("#collapse-tree-button");
|
||||
const $scrollToCurrentNoteButton = $("#scroll-to-current-note-button");
|
||||
const $notePathList = $("#note-path-list");
|
||||
const $notePathCount = $("#note-path-count");
|
||||
|
||||
let startNotePath = null;
|
||||
|
||||
@@ -81,11 +81,15 @@ async function expandToNote(notePath, expandOpts) {
|
||||
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
let parentNoteId = 'root';
|
||||
let parentNoteId = 'none';
|
||||
|
||||
for (const childNoteId of runPath) {
|
||||
const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId);
|
||||
|
||||
if (!node) {
|
||||
console.log(`Can't find node for noteId=${childNoteId} with parentNoteId=${parentNoteId}`);
|
||||
}
|
||||
|
||||
if (childNoteId === noteId) {
|
||||
return node;
|
||||
}
|
||||
@@ -115,7 +119,10 @@ async function getRunPath(notePath) {
|
||||
utils.assertArguments(notePath);
|
||||
|
||||
const path = notePath.split("/").reverse();
|
||||
path.push('root');
|
||||
|
||||
if (!path.includes("root")) {
|
||||
path.push('root');
|
||||
}
|
||||
|
||||
const effectivePath = [];
|
||||
let childNoteId = null;
|
||||
@@ -151,6 +158,8 @@ async function getRunPath(notePath) {
|
||||
for (const noteId of pathToRoot) {
|
||||
effectivePath.push(noteId);
|
||||
}
|
||||
|
||||
effectivePath.push('root');
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -162,7 +171,7 @@ async function getRunPath(notePath) {
|
||||
}
|
||||
}
|
||||
|
||||
if (parentNoteId === 'root') {
|
||||
if (parentNoteId === 'none') {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
@@ -180,16 +189,13 @@ async function showParentList(noteId, node) {
|
||||
const note = await treeCache.getNote(noteId);
|
||||
const parents = await note.getParentNotes();
|
||||
|
||||
if (!parents.length) {
|
||||
infoService.throwError("Can't find parents for noteId=" + noteId);
|
||||
}
|
||||
$notePathCount.html(parents.length + " path" + (parents.length > 0 ? "s" : ""));
|
||||
|
||||
if (parents.length <= 1) {
|
||||
$parentList.hide();
|
||||
}
|
||||
else {
|
||||
$parentList.show();
|
||||
$parentListList.empty();
|
||||
//$notePathList.show();
|
||||
$notePathList.empty();
|
||||
|
||||
for (const parentNote of parents) {
|
||||
const parentNotePath = await getSomeNotePath(parentNote);
|
||||
@@ -197,16 +203,13 @@ async function showParentList(noteId, node) {
|
||||
const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
|
||||
const title = await treeUtils.getNotePathTitle(notePath);
|
||||
|
||||
let item;
|
||||
const item = $("<li/>").append(await linkService.createNoteLink(notePath, title));
|
||||
|
||||
if (node.getParent().data.noteId === parentNote.noteId) {
|
||||
item = $("<span/>").attr("title", "Current note").append(title);
|
||||
}
|
||||
else {
|
||||
item = linkService.createNoteLink(notePath, title);
|
||||
item.addClass("current");
|
||||
}
|
||||
|
||||
$parentListList.append($("<li/>").append(item));
|
||||
$notePathList.append(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,15 +288,16 @@ async function treeInitialized() {
|
||||
}
|
||||
}
|
||||
|
||||
function initFancyTree(branch) {
|
||||
utils.assertArguments(branch);
|
||||
function initFancyTree(tree) {
|
||||
utils.assertArguments(tree);
|
||||
|
||||
$tree.fancytree({
|
||||
autoScroll: true,
|
||||
keyboard: false, // we takover keyboard handling in the hotkeys plugin
|
||||
extensions: ["hotkeys", "filter", "dnd", "clones"],
|
||||
source: branch,
|
||||
source: tree,
|
||||
scrollParent: $tree,
|
||||
minExpandLevel: 2, // root can't be collapsed
|
||||
click: (event, data) => {
|
||||
const targetType = data.targetType;
|
||||
const node = data.node;
|
||||
@@ -375,7 +379,7 @@ async function loadTree() {
|
||||
startNotePath = getNotePathFromAddress();
|
||||
}
|
||||
|
||||
return await treeBuilder.prepareTree(resp.notes, resp.branches);
|
||||
return await treeBuilder.prepareTree(resp.notes, resp.branches, resp.relations);
|
||||
}
|
||||
|
||||
function collapseTree(node = null) {
|
||||
@@ -541,9 +545,9 @@ $(window).bind('hashchange', function() {
|
||||
});
|
||||
|
||||
utils.bindShortcut('alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
|
||||
$collapseTreeButton.click(() => collapseTree());
|
||||
|
||||
$createTopLevelNoteButton.click(createNewTopLevelNote);
|
||||
$collapseTreeButton.click(collapseTree);
|
||||
$scrollToCurrentNoteButton.click(scrollToCurrentNote);
|
||||
|
||||
export default {
|
||||
|
||||
@@ -5,12 +5,12 @@ import server from "./server.js";
|
||||
import treeCache from "./tree_cache.js";
|
||||
import messagingService from "./messaging.js";
|
||||
|
||||
async function prepareTree(noteRows, branchRows) {
|
||||
utils.assertArguments(noteRows);
|
||||
async function prepareTree(noteRows, branchRows, relations) {
|
||||
utils.assertArguments(noteRows, branchRows, relations);
|
||||
|
||||
treeCache.load(noteRows, branchRows);
|
||||
treeCache.load(noteRows, branchRows, relations);
|
||||
|
||||
return await prepareRealBranch(await treeCache.getNote('root'));
|
||||
return [ await prepareNode(await treeCache.getBranch('root')) ];
|
||||
}
|
||||
|
||||
async function prepareBranch(note) {
|
||||
@@ -22,6 +22,35 @@ async function prepareBranch(note) {
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareNode(branch) {
|
||||
const note = await branch.getNote();
|
||||
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
|
||||
|
||||
const node = {
|
||||
noteId: note.noteId,
|
||||
parentNoteId: branch.parentNoteId,
|
||||
branchId: branch.branchId,
|
||||
isProtected: note.isProtected,
|
||||
title: utils.escapeHtml(title),
|
||||
extraClasses: await getExtraClasses(note),
|
||||
refKey: note.noteId,
|
||||
expanded: note.type !== 'search' && branch.isExpanded
|
||||
};
|
||||
|
||||
if (note.hasChildren() || note.type === 'search') {
|
||||
node.folder = true;
|
||||
|
||||
if (node.expanded && note.type !== 'search') {
|
||||
node.children = await prepareRealBranch(note);
|
||||
}
|
||||
else {
|
||||
node.lazy = true;
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
async function prepareRealBranch(parentNote) {
|
||||
utils.assertArguments(parentNote);
|
||||
|
||||
@@ -35,32 +64,7 @@ async function prepareRealBranch(parentNote) {
|
||||
const noteList = [];
|
||||
|
||||
for (const branch of childBranches) {
|
||||
const note = await branch.getNote();
|
||||
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
|
||||
|
||||
const node = {
|
||||
noteId: note.noteId,
|
||||
parentNoteId: branch.parentNoteId,
|
||||
branchId: branch.branchId,
|
||||
isProtected: note.isProtected,
|
||||
title: utils.escapeHtml(title),
|
||||
extraClasses: await getExtraClasses(note),
|
||||
refKey: note.noteId,
|
||||
expanded: note.type !== 'search' && branch.isExpanded
|
||||
};
|
||||
|
||||
const hasChildren = (await note.getChildNotes()).length > 0;
|
||||
|
||||
if (hasChildren || note.type === 'search') {
|
||||
node.folder = true;
|
||||
|
||||
if (node.expanded && note.type !== 'search') {
|
||||
node.children = await prepareRealBranch(note);
|
||||
}
|
||||
else {
|
||||
node.lazy = true;
|
||||
}
|
||||
}
|
||||
const node = await prepareNode(branch);
|
||||
|
||||
noteList.push(node);
|
||||
}
|
||||
@@ -92,11 +96,15 @@ async function getExtraClasses(note) {
|
||||
|
||||
const extraClasses = [];
|
||||
|
||||
if (note.noteId === 'root') {
|
||||
extraClasses.push("tree-root");
|
||||
}
|
||||
|
||||
if (note.isProtected) {
|
||||
extraClasses.push("protected");
|
||||
}
|
||||
|
||||
if ((await note.getParentNotes()).length > 1) {
|
||||
if (note.getParentNoteIds().length > 1) {
|
||||
extraClasses.push("multiple-parents");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,45 +2,93 @@ import utils from "./utils.js";
|
||||
import Branch from "../entities/branch.js";
|
||||
import NoteShort from "../entities/note_short.js";
|
||||
import infoService from "./info.js";
|
||||
import server from "./server.js";
|
||||
|
||||
class TreeCache {
|
||||
load(noteRows, branchRows) {
|
||||
this.parents = [];
|
||||
this.children = [];
|
||||
load(noteRows, branchRows, relations) {
|
||||
this.parents = {};
|
||||
this.children = {};
|
||||
this.childParentToBranch = {};
|
||||
|
||||
/** @type {Object.<string, NoteShort>} */
|
||||
this.notes = {};
|
||||
|
||||
/** @type {Object.<string, Branch>} */
|
||||
this.branches = {};
|
||||
|
||||
this.addResp(noteRows, branchRows, relations);
|
||||
}
|
||||
|
||||
addResp(noteRows, branchRows, relations) {
|
||||
for (const noteRow of noteRows) {
|
||||
const note = new NoteShort(this, noteRow);
|
||||
|
||||
this.notes[note.noteId] = note;
|
||||
}
|
||||
|
||||
/** @type {Object.<string, Branch>} */
|
||||
this.branches = {};
|
||||
for (const branchRow of branchRows) {
|
||||
const branch = new Branch(this, branchRow);
|
||||
|
||||
this.addBranch(branch);
|
||||
}
|
||||
|
||||
for (const relation of relations) {
|
||||
this.addBranchRelationship(relation.branchId, relation.childNoteId, relation.parentNoteId);
|
||||
}
|
||||
}
|
||||
|
||||
async getNotes(noteIds) {
|
||||
const missingNoteIds = noteIds.filter(noteId => this.notes[noteId] === undefined);
|
||||
|
||||
if (missingNoteIds.length > 0) {
|
||||
const resp = await server.post('tree/load', { noteIds: missingNoteIds });
|
||||
|
||||
this.addResp(resp.notes, resp.branches, resp.relations);
|
||||
}
|
||||
|
||||
return noteIds.map(noteId => {
|
||||
if (!this.notes[noteId]) {
|
||||
throw new Error(`Can't find note ${noteId}`);
|
||||
}
|
||||
else {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @return NoteShort */
|
||||
async getNote(noteId) {
|
||||
return this.notes[noteId];
|
||||
if (noteId === 'none') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await this.getNotes([noteId]))[0];
|
||||
}
|
||||
|
||||
addBranch(branch) {
|
||||
this.branches[branch.branchId] = branch;
|
||||
|
||||
this.parents[branch.noteId] = this.parents[branch.noteId] || [];
|
||||
this.parents[branch.noteId].push(this.notes[branch.parentNoteId]);
|
||||
this.addBranchRelationship(branch.branchId, branch.noteId, branch.parentNoteId);
|
||||
}
|
||||
|
||||
this.children[branch.parentNoteId] = this.children[branch.parentNoteId] || [];
|
||||
this.children[branch.parentNoteId].push(this.notes[branch.noteId]);
|
||||
addBranchRelationship(branchId, childNoteId, parentNoteId) {
|
||||
if (parentNoteId === 'none') { // applies only to root element
|
||||
return;
|
||||
}
|
||||
|
||||
this.childParentToBranch[branch.noteId + '-' + branch.parentNoteId] = branch;
|
||||
this.childParentToBranch[childNoteId + '-' + parentNoteId] = branchId;
|
||||
|
||||
this.parents[childNoteId] = this.parents[childNoteId] || [];
|
||||
|
||||
if (!this.parents[childNoteId].includes(parentNoteId)) {
|
||||
this.parents[childNoteId].push(parentNoteId);
|
||||
}
|
||||
|
||||
this.children[parentNoteId] = this.children[parentNoteId] || [];
|
||||
|
||||
if (!this.children[parentNoteId].includes(childNoteId)) {
|
||||
this.children[parentNoteId].push(childNoteId);
|
||||
}
|
||||
}
|
||||
|
||||
add(note, branch) {
|
||||
@@ -49,21 +97,46 @@ class TreeCache {
|
||||
this.addBranch(branch);
|
||||
}
|
||||
|
||||
async getBranches(branchIds) {
|
||||
const missingBranchIds = branchIds.filter(branchId => this.branches[branchId] === undefined);
|
||||
|
||||
if (missingBranchIds.length > 0) {
|
||||
const resp = await server.post('tree/load', { branchIds: branchIds });
|
||||
|
||||
this.addResp(resp.notes, resp.branches, resp.relations);
|
||||
}
|
||||
|
||||
return branchIds.map(branchId => {
|
||||
if (!this.branches[branchId]) {
|
||||
throw new Error(`Can't find branch ${branchId}`);
|
||||
}
|
||||
else {
|
||||
return this.branches[branchId];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @return Branch */
|
||||
async getBranch(branchId) {
|
||||
return this.branches[branchId];
|
||||
return (await this.getBranches([branchId]))[0];
|
||||
}
|
||||
|
||||
/** @return Branch */
|
||||
async getBranchByChildParent(childNoteId, parentNoteId) {
|
||||
const key = (childNoteId + '-' + parentNoteId);
|
||||
const branch = this.childParentToBranch[key];
|
||||
const branchId = this.getBranchIdByChildParent(childNoteId, parentNoteId);
|
||||
|
||||
if (!branch) {
|
||||
return await this.getBranch(branchId);
|
||||
}
|
||||
|
||||
getBranchIdByChildParent(childNoteId, parentNoteId) {
|
||||
const key = childNoteId + '-' + parentNoteId;
|
||||
const branchId = this.childParentToBranch[key];
|
||||
|
||||
if (!branchId) {
|
||||
infoService.throwError("Cannot find branch for child-parent=" + key);
|
||||
}
|
||||
|
||||
return branch;
|
||||
return branchId;
|
||||
}
|
||||
|
||||
/* Move note from one parent to another. */
|
||||
@@ -78,33 +151,14 @@ class TreeCache {
|
||||
delete treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId]; // this is correct because we know that oldParentId isn't same as newParentId
|
||||
|
||||
// remove old associations
|
||||
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== oldParentNoteId);
|
||||
treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch.noteId !== childNoteId);
|
||||
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p !== oldParentNoteId);
|
||||
treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch !== childNoteId);
|
||||
|
||||
// add new associations
|
||||
treeCache.parents[childNoteId].push(await treeCache.getNote(newParentNoteId));
|
||||
treeCache.parents[childNoteId].push(newParentNoteId);
|
||||
|
||||
treeCache.children[newParentNoteId] = treeCache.children[newParentNoteId] || []; // this might be first child
|
||||
treeCache.children[newParentNoteId].push(await treeCache.getNote(childNoteId));
|
||||
}
|
||||
|
||||
removeParentChildRelation(parentNoteId, childNoteId) {
|
||||
utils.assertArguments(parentNoteId, childNoteId);
|
||||
|
||||
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== parentNoteId);
|
||||
treeCache.children[parentNoteId] = treeCache.children[parentNoteId].filter(ch => ch.noteId !== childNoteId);
|
||||
|
||||
delete treeCache.childParentToBranch[childNoteId + '-' + parentNoteId];
|
||||
}
|
||||
|
||||
async setParentChildRelation(branchId, parentNoteId, childNoteId) {
|
||||
treeCache.parents[childNoteId] = treeCache.parents[childNoteId] || [];
|
||||
treeCache.parents[childNoteId].push(await treeCache.getNote(parentNoteId));
|
||||
|
||||
treeCache.children[parentNoteId] = treeCache.children[parentNoteId] || [];
|
||||
treeCache.children[parentNoteId].push(await treeCache.getNote(childNoteId));
|
||||
|
||||
treeCache.childParentToBranch[childNoteId + '-' + parentNoteId] = await treeCache.getBranch(branchId);
|
||||
treeCache.children[newParentNoteId].push(childNoteId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
src/public/libraries/ckeditor/ckeditor.js
vendored
2
src/public/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
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -12,9 +12,40 @@
|
||||
/*------------------------------------------------------------------------------
|
||||
* Helpers
|
||||
*----------------------------------------------------------------------------*/
|
||||
.ui-helper-hidden {
|
||||
.fancytree-helper-hidden {
|
||||
display: none;
|
||||
}
|
||||
.fancytree-helper-indeterminate-cb {
|
||||
color: #777;
|
||||
}
|
||||
.fancytree-helper-disabled {
|
||||
color: #c0c0c0;
|
||||
}
|
||||
/* Helper to allow spinning loader icon with glyph-, ligature-, and SVG-icons. */
|
||||
.fancytree-helper-spin {
|
||||
-webkit-animation: spin 1000ms infinite linear;
|
||||
animation: spin 1000ms infinite linear;
|
||||
}
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(359deg);
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
@keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(359deg);
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
/*------------------------------------------------------------------------------
|
||||
* Container and UL / LI
|
||||
*----------------------------------------------------------------------------*/
|
||||
@@ -338,6 +369,16 @@ span.fancytree-node.fancytree-error span.fancytree-title {
|
||||
/*------------------------------------------------------------------------------
|
||||
* Drag'n'drop support
|
||||
*----------------------------------------------------------------------------*/
|
||||
/* ext-dnd5: */
|
||||
span.fancytree-childcounter {
|
||||
color: #fff;
|
||||
background: #337ab7;
|
||||
border: 1px solid gray;
|
||||
border-radius: 10px;
|
||||
padding: 2px;
|
||||
text-align: center;
|
||||
}
|
||||
/* ext-dnd: */
|
||||
div.fancytree-drag-helper span.fancytree-childcounter,
|
||||
div.fancytree-drag-helper span.fancytree-dnd-modifier {
|
||||
display: inline-block;
|
||||
@@ -402,8 +443,7 @@ span.fancytree-drag-source.fancytree-drag-remove {
|
||||
.fancytree-container.fancytree-rtl span.fancytree-connector,
|
||||
.fancytree-container.fancytree-rtl span.fancytree-expander,
|
||||
.fancytree-container.fancytree-rtl span.fancytree-icon,
|
||||
.fancytree-container.fancytree-rtl span.fancytree-drag-helper-img,
|
||||
.fancytree-container.fancytree-rtl #fancytree-drop-marker {
|
||||
.fancytree-container.fancytree-rtl span.fancytree-drag-helper-img {
|
||||
background-image: url("../skin-win8/icons-rtl.gif");
|
||||
}
|
||||
.fancytree-container.fancytree-rtl .fancytree-exp-n span.fancytree-expander,
|
||||
@@ -425,6 +465,9 @@ ul.fancytree-container.fancytree-rtl li.fancytree-lastsib,
|
||||
ul.fancytree-container.fancytree-rtl.fancytree-no-connector > li {
|
||||
background-image: none;
|
||||
}
|
||||
#fancytree-drop-marker.fancytree-rtl {
|
||||
background-image: url("../skin-win8/icons-rtl.gif");
|
||||
}
|
||||
/*------------------------------------------------------------------------------
|
||||
* 'table' extension
|
||||
*----------------------------------------------------------------------------*/
|
||||
@@ -482,7 +525,7 @@ table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right:h
|
||||
* 'filter' extension
|
||||
*----------------------------------------------------------------------------*/
|
||||
.fancytree-ext-filter-dimm span.fancytree-node span.fancytree-title {
|
||||
color: silver;
|
||||
color: #c0c0c0;
|
||||
font-weight: lighter;
|
||||
}
|
||||
.fancytree-ext-filter-dimm tr.fancytree-submatch span.fancytree-title,
|
||||
@@ -501,7 +544,7 @@ table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right:h
|
||||
}
|
||||
.fancytree-ext-filter-hide tr.fancytree-submatch span.fancytree-title,
|
||||
.fancytree-ext-filter-hide span.fancytree-node.fancytree-submatch span.fancytree-title {
|
||||
color: silver;
|
||||
color: #c0c0c0;
|
||||
font-weight: lighter;
|
||||
}
|
||||
.fancytree-ext-filter-hide tr.fancytree-match span.fancytree-title,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,18 +4,12 @@
|
||||
|
||||
display: grid;
|
||||
grid-template-areas: "header header"
|
||||
"tree-actions title"
|
||||
"search note-detail"
|
||||
"tree note-detail"
|
||||
"parent-list note-detail"
|
||||
"parent-list label-list";
|
||||
grid-template-columns: 2fr 5fr;
|
||||
"left-pane title"
|
||||
"left-pane note-detail";
|
||||
grid-template-columns: 29% 70%;
|
||||
grid-template-rows: auto
|
||||
auto
|
||||
auto
|
||||
1fr
|
||||
auto
|
||||
auto;
|
||||
1fr;
|
||||
|
||||
justify-content: center;
|
||||
grid-gap: 10px;
|
||||
@@ -28,6 +22,23 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#note-detail-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
grid-area: note-detail;
|
||||
padding-left: 10px;
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#note-detail-component-wrapper {
|
||||
flex-grow: 100;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
flex-basis: content;
|
||||
}
|
||||
|
||||
.note-detail-component {
|
||||
display: none;
|
||||
}
|
||||
@@ -50,8 +61,6 @@
|
||||
}
|
||||
|
||||
ul.fancytree-container {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
@@ -96,6 +105,15 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span.fancytree-node.tree-root > span.fancytree-icon {
|
||||
background: url("../images/icons/tree-root.png") 0 0;
|
||||
}
|
||||
|
||||
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
|
||||
.ui-fancytree > li > ul {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
/* By default not focused active tree item is not easily visible, this makes it more visible */
|
||||
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||
background-color: #ddd !important;
|
||||
@@ -150,16 +168,11 @@ div.ui-tooltip {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#parent-list {
|
||||
display: none;
|
||||
margin-left: 20px;
|
||||
border-top: 2px solid #eee;
|
||||
padding-top: 10px;
|
||||
grid-area: parent-list;
|
||||
}
|
||||
|
||||
#parent-list ul {
|
||||
padding-left: 20px;
|
||||
#tree {
|
||||
overflow: auto;
|
||||
flex-grow: 100;
|
||||
flex-shrink: 100;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -222,7 +235,7 @@ div.ui-tooltip {
|
||||
filter: opacity(7%);
|
||||
}
|
||||
|
||||
.dropdown-menu li:not(.divider) {
|
||||
#note-type .dropdown-menu li:not(.divider) {
|
||||
padding: 5px;
|
||||
width: 200px;
|
||||
}
|
||||
@@ -250,11 +263,20 @@ div.ui-tooltip {
|
||||
|
||||
#note-detail-code {
|
||||
min-height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#note-detail-render {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
font-family: "Liberation Mono", "Lucida Console", monospace;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
#note-id-display {
|
||||
@@ -274,7 +296,6 @@ div.ui-tooltip {
|
||||
.cm-matchhighlight {background-color: #eeeeee}
|
||||
|
||||
#label-list {
|
||||
grid-area: label-list;
|
||||
color: #777777;
|
||||
border-top: 1px solid #eee;
|
||||
padding: 5px; display: none;
|
||||
@@ -320,4 +341,32 @@ div.ui-tooltip {
|
||||
|
||||
.child-overview a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
#sql-console-query {
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#sql-console-query .CodeMirror {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
#history-navigation {
|
||||
margin: 0 20px 0 5px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.btn.active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
#note-path-list .current a {
|
||||
font-weight: bold;
|
||||
}
|
||||
20
src/routes/api/autocomplete.js
Normal file
20
src/routes/api/autocomplete.js
Normal file
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
|
||||
const autocompleteService = require('../../services/autocomplete');
|
||||
|
||||
async function getAutocomplete(req) {
|
||||
const query = req.query.query;
|
||||
|
||||
const results = autocompleteService.getResults(query);
|
||||
|
||||
return results.map(res => {
|
||||
return {
|
||||
value: res.title + ' (' + res.path + ')',
|
||||
title: res.title
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAutocomplete
|
||||
};
|
||||
@@ -3,17 +3,7 @@
|
||||
const sql = require('../../services/sql');
|
||||
|
||||
async function getEventLog() {
|
||||
await deleteOld();
|
||||
|
||||
return await sql.getRows("SELECT * FROM event_log ORDER BY dateAdded DESC");
|
||||
}
|
||||
|
||||
async function deleteOld() {
|
||||
const cutoffId = await sql.getValue("SELECT id FROM event_log ORDER BY id DESC LIMIT 1000, 1");
|
||||
|
||||
if (cutoffId) {
|
||||
await sql.execute("DELETE FROM event_log WHERE id < ?", [cutoffId]);
|
||||
}
|
||||
return await sql.getRows("SELECT * FROM event_log ORDER BY dateCreated DESC");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -5,12 +5,85 @@ const html = require('html');
|
||||
const tar = require('tar-stream');
|
||||
const sanitize = require("sanitize-filename");
|
||||
const repository = require("../../services/repository");
|
||||
const utils = require('../../services/utils');
|
||||
|
||||
async function exportNote(req, res) {
|
||||
const noteId = req.params.noteId;
|
||||
const format = req.params.format;
|
||||
|
||||
const branchId = await sql.getValue('SELECT branchId FROM branches WHERE noteId = ?', [noteId]);
|
||||
|
||||
if (format === 'tar') {
|
||||
await exportToTar(branchId, res);
|
||||
}
|
||||
else if (format === 'opml') {
|
||||
await exportToOpml(branchId, res);
|
||||
}
|
||||
else {
|
||||
return [404, "Unrecognized export format " + format];
|
||||
}
|
||||
}
|
||||
|
||||
function escapeXmlAttribute(text) {
|
||||
return text.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function prepareText(text) {
|
||||
const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, '\n')
|
||||
.replace(/ /g, ' '); // nbsp isn't in XML standard (only HTML)
|
||||
|
||||
const stripped = utils.stripTags(newLines);
|
||||
|
||||
const escaped = escapeXmlAttribute(stripped);
|
||||
|
||||
return escaped.replace(/\n/g, ' ');
|
||||
}
|
||||
|
||||
async function exportToOpml(branchId, res) {
|
||||
const branch = await repository.getBranch(branchId);
|
||||
const note = await branch.getNote();
|
||||
const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
|
||||
const sanitizedTitle = sanitize(title);
|
||||
|
||||
async function exportNoteInner(branchId) {
|
||||
const branch = await repository.getBranch(branchId);
|
||||
const note = await branch.getNote();
|
||||
const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
|
||||
|
||||
const preparedTitle = prepareText(title);
|
||||
const preparedContent = prepareText(note.content);
|
||||
|
||||
res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`);
|
||||
|
||||
for (const child of await note.getChildBranches()) {
|
||||
await exportNoteInner(child.branchId);
|
||||
}
|
||||
|
||||
res.write('</outline>');
|
||||
}
|
||||
|
||||
res.setHeader('Content-Disposition', 'file; filename="' + sanitizedTitle + '.opml"');
|
||||
res.setHeader('Content-Type', 'text/x-opml');
|
||||
|
||||
res.write(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<opml version="1.0">
|
||||
<head>
|
||||
<title>Trilium export</title>
|
||||
</head>
|
||||
<body>`);
|
||||
|
||||
await exportNoteInner(branchId);
|
||||
|
||||
res.write(`</body>
|
||||
</opml>`);
|
||||
res.end();
|
||||
}
|
||||
|
||||
async function exportToTar(branchId, res) {
|
||||
const pack = tar.pack();
|
||||
|
||||
const exportedNoteIds = [];
|
||||
|
||||
@@ -7,6 +7,79 @@ const Branch = require('../../entities/branch');
|
||||
const tar = require('tar-stream');
|
||||
const stream = require('stream');
|
||||
const path = require('path');
|
||||
const parseString = require('xml2js').parseString;
|
||||
|
||||
async function importToBranch(req) {
|
||||
const parentNoteId = req.params.parentNoteId;
|
||||
const file = req.file;
|
||||
|
||||
const parentNote = await repository.getNote(parentNoteId);
|
||||
|
||||
if (!parentNote) {
|
||||
return [404, `Note ${parentNoteId} doesn't exist.`];
|
||||
}
|
||||
|
||||
const extension = path.extname(file.originalname).toLowerCase();
|
||||
|
||||
if (extension === '.tar') {
|
||||
await importTar(file, parentNoteId);
|
||||
}
|
||||
else if (extension === '.opml') {
|
||||
return await importOpml(file, parentNoteId);
|
||||
}
|
||||
else {
|
||||
return [400, `Unrecognized extension ${extension}, must be .tar or .opml`];
|
||||
}
|
||||
}
|
||||
|
||||
function toHtml(text) {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '<p>' + text.replace(/(?:\r\n|\r|\n)/g, '</p><p>') + '</p>';
|
||||
}
|
||||
|
||||
async function importOutline(outline, parentNoteId) {
|
||||
const {note} = await noteService.createNote(parentNoteId, outline.$.title, toHtml(outline.$.text));
|
||||
|
||||
for (const childOutline of (outline.outline || [])) {
|
||||
await importOutline(childOutline, note.noteId);
|
||||
}
|
||||
}
|
||||
|
||||
async function importOpml(file, parentNoteId) {
|
||||
const xml = await new Promise(function(resolve, reject)
|
||||
{
|
||||
parseString(file.buffer, function (err, result) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (xml.opml.$.version !== '1.0' && xml.opml.$.version !== '1.1') {
|
||||
return [400, 'Unsupported OPML version ' + xml.opml.$.version + ', 1.0 or 1.1 expected instead.'];
|
||||
}
|
||||
|
||||
const outlines = xml.opml.body[0].outline || [];
|
||||
|
||||
for (const outline of outlines) {
|
||||
await importOutline(outline, parentNoteId);
|
||||
}
|
||||
}
|
||||
|
||||
async function importTar(file, parentNoteId) {
|
||||
const files = await parseImportFile(file);
|
||||
|
||||
// maps from original noteId (in tar file) to newly generated noteId
|
||||
const noteIdMap = {};
|
||||
|
||||
await importNotes(files, parentNoteId, noteIdMap);
|
||||
}
|
||||
|
||||
function getFileName(name) {
|
||||
let key;
|
||||
@@ -86,24 +159,6 @@ async function parseImportFile(file) {
|
||||
});
|
||||
}
|
||||
|
||||
async function importTar(req) {
|
||||
const parentNoteId = req.params.parentNoteId;
|
||||
const file = req.file;
|
||||
|
||||
const parentNote = await repository.getNote(parentNoteId);
|
||||
|
||||
if (!parentNote) {
|
||||
return [404, `Note ${parentNoteId} doesn't exist.`];
|
||||
}
|
||||
|
||||
const files = await parseImportFile(file);
|
||||
|
||||
// maps from original noteId (in tar file) to newly generated noteId
|
||||
const noteIdMap = {};
|
||||
|
||||
await importNotes(files, parentNoteId, noteIdMap);
|
||||
}
|
||||
|
||||
async function importNotes(files, parentNoteId, noteIdMap) {
|
||||
for (const file of files) {
|
||||
if (file.meta.version !== 1) {
|
||||
@@ -143,5 +198,5 @@ async function importNotes(files, parentNoteId, noteIdMap) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
importTar
|
||||
importToBranch
|
||||
};
|
||||
@@ -7,6 +7,8 @@ const sourceIdService = require('../../services/source_id');
|
||||
const passwordEncryptionService = require('../../services/password_encryption');
|
||||
const protectedSessionService = require('../../services/protected_session');
|
||||
const appInfo = require('../../services/app_info');
|
||||
const eventService = require('../../services/events');
|
||||
const cls = require('../../services/cls');
|
||||
|
||||
async function loginSync(req) {
|
||||
const timestampStr = req.body.timestamp;
|
||||
@@ -53,7 +55,12 @@ async function loginToProtectedSession(req) {
|
||||
|
||||
const decryptedDataKey = await passwordEncryptionService.getDataKey(password);
|
||||
|
||||
const protectedSessionId = protectedSessionService.setDataKey(req, decryptedDataKey);
|
||||
const protectedSessionId = protectedSessionService.setDataKey(decryptedDataKey);
|
||||
|
||||
// this is set here so that event handlers have access to the protected session
|
||||
cls.namespace.set('protectedSessionId', protectedSessionId);
|
||||
|
||||
eventService.emit(eventService.ENTER_PROTECTED_SESSION);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -47,7 +47,7 @@ async function sortNotes(req) {
|
||||
|
||||
async function protectBranch(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const note = repository.getNote(noteId);
|
||||
const note = await repository.getNote(noteId);
|
||||
const protect = !!parseInt(req.params.isProtected);
|
||||
|
||||
await noteService.protectNoteRecursively(note, protect);
|
||||
|
||||
@@ -16,7 +16,7 @@ async function getRecentNotes() {
|
||||
recent_notes.isDeleted = 0
|
||||
AND branches.isDeleted = 0
|
||||
ORDER BY
|
||||
dateAccessed DESC
|
||||
dateCreated DESC
|
||||
LIMIT 200`);
|
||||
}
|
||||
|
||||
@@ -26,9 +26,7 @@ async function addRecentNote(req) {
|
||||
|
||||
await new RecentNote({
|
||||
branchId: branchId,
|
||||
notePath: notePath,
|
||||
dateAccessed: dateUtils.nowDate(),
|
||||
isDeleted: 0
|
||||
notePath: notePath
|
||||
}).save();
|
||||
|
||||
await optionService.setOption('startNotePath', notePath);
|
||||
|
||||
@@ -68,7 +68,7 @@ async function update(req) {
|
||||
const entities = req.body.entities;
|
||||
|
||||
for (const {sync, entity} of entities) {
|
||||
await syncUpdateService.updateEntity(sync.entityName, entity, sourceId);
|
||||
await syncUpdateService.updateEntity(sync, entity, sourceId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,58 +4,76 @@ const sql = require('../../services/sql');
|
||||
const optionService = require('../../services/options');
|
||||
const protectedSessionService = require('../../services/protected_session');
|
||||
|
||||
async function getTree() {
|
||||
const branches = await sql.getRows(`
|
||||
SELECT
|
||||
branchId,
|
||||
noteId,
|
||||
parentNoteId,
|
||||
notePosition,
|
||||
prefix,
|
||||
isExpanded
|
||||
FROM
|
||||
branches
|
||||
WHERE
|
||||
isDeleted = 0
|
||||
ORDER BY
|
||||
notePosition`);
|
||||
|
||||
const notes = [{
|
||||
noteId: 'root',
|
||||
title: 'root',
|
||||
isProtected: false,
|
||||
type: 'none',
|
||||
mime: 'none'
|
||||
}].concat(await sql.getRows(`
|
||||
SELECT
|
||||
notes.noteId,
|
||||
notes.title,
|
||||
notes.isProtected,
|
||||
notes.type,
|
||||
notes.mime,
|
||||
hideInAutocomplete.labelId AS 'hideInAutocomplete'
|
||||
FROM
|
||||
notes
|
||||
LEFT JOIN labels AS hideInAutocomplete ON hideInAutocomplete.noteId = notes.noteId
|
||||
AND hideInAutocomplete.name = 'hideInAutocomplete'
|
||||
AND hideInAutocomplete.isDeleted = 0
|
||||
WHERE
|
||||
notes.isDeleted = 0`));
|
||||
async function getNotes(noteIds) {
|
||||
const notes = await sql.getManyRows(`
|
||||
SELECT noteId, title, isProtected, type, mime
|
||||
FROM notes WHERE isDeleted = 0 AND noteId IN (???)`, noteIds);
|
||||
|
||||
protectedSessionService.decryptNotes(notes);
|
||||
|
||||
notes.forEach(note => {
|
||||
note.hideInAutocomplete = !!note.hideInAutocomplete;
|
||||
note.isProtected = !!note.isProtected;
|
||||
});
|
||||
notes.forEach(note => note.isProtected = !!note.isProtected);
|
||||
return notes;
|
||||
}
|
||||
|
||||
async function getRelations(noteIds) {
|
||||
// we need to fetch both parentNoteId and noteId matches because we can have loaded child
|
||||
// of which only some of the parents has been loaded.
|
||||
|
||||
return await sql.getManyRows(`SELECT branchId, noteId AS 'childNoteId', parentNoteId FROM branches WHERE isDeleted = 0
|
||||
AND (parentNoteId IN (???) OR noteId IN (???))`, noteIds);
|
||||
}
|
||||
|
||||
async function getTree() {
|
||||
// we fetch all branches of notes, even if that particular branch isn't visible
|
||||
// this allows us to e.g. detect and properly display clones
|
||||
const branches = await sql.getRows(`
|
||||
WITH RECURSIVE
|
||||
tree(branchId, noteId, isExpanded) AS (
|
||||
SELECT branchId, noteId, isExpanded FROM branches WHERE branchId = 'root'
|
||||
UNION ALL
|
||||
SELECT branches.branchId, branches.noteId, branches.isExpanded FROM branches
|
||||
JOIN tree ON branches.parentNoteId = tree.noteId
|
||||
WHERE tree.isExpanded = 1 AND branches.isDeleted = 0
|
||||
)
|
||||
SELECT branches.* FROM tree JOIN branches USING(noteId) ORDER BY branches.notePosition`);
|
||||
|
||||
const noteIds = branches.map(b => b.noteId);
|
||||
|
||||
const notes = await getNotes(noteIds);
|
||||
|
||||
const relations = await getRelations(noteIds);
|
||||
|
||||
return {
|
||||
startNotePath: await optionService.getOption('startNotePath'),
|
||||
branches: branches,
|
||||
notes: notes
|
||||
branches,
|
||||
notes,
|
||||
relations
|
||||
};
|
||||
}
|
||||
|
||||
async function load(req) {
|
||||
let noteIds = req.body.noteIds;
|
||||
const branchIds = req.body.branchIds;
|
||||
|
||||
if (branchIds && branchIds.length > 0) {
|
||||
noteIds = (await sql.getManyRows(`SELECT noteId FROM branches WHERE isDeleted = 0 AND branchId IN(???)`, branchIds))
|
||||
.map(note => note.noteId);
|
||||
}
|
||||
|
||||
const branches = await sql.getManyRows(`SELECT * FROM branches WHERE isDeleted = 0 AND noteId IN (???)`, noteIds);
|
||||
|
||||
const notes = await getNotes(noteIds);
|
||||
|
||||
const relations = await getRelations(noteIds);
|
||||
|
||||
return {
|
||||
branches,
|
||||
notes,
|
||||
relations
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTree
|
||||
getTree,
|
||||
load
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ const multer = require('multer')();
|
||||
const treeApiRoute = require('./api/tree');
|
||||
const notesApiRoute = require('./api/notes');
|
||||
const branchesApiRoute = require('./api/branches');
|
||||
const autocompleteApiRoute = require('./api/autocomplete');
|
||||
const cloningApiRoute = require('./api/cloning');
|
||||
const noteRevisionsApiRoute = require('./api/note_revisions');
|
||||
const recentChangesApiRoute = require('./api/recent_changes');
|
||||
@@ -99,6 +100,7 @@ function register(app) {
|
||||
route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage);
|
||||
|
||||
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
|
||||
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);
|
||||
@@ -107,6 +109,8 @@ function register(app) {
|
||||
apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded);
|
||||
apiRoute(DELETE, '/api/branches/:branchId', branchesApiRoute.deleteBranch);
|
||||
|
||||
apiRoute(GET, '/api/autocomplete', autocompleteApiRoute.getAutocomplete);
|
||||
|
||||
apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote);
|
||||
apiRoute(PUT, '/api/notes/:noteId', notesApiRoute.updateNote);
|
||||
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
|
||||
@@ -118,8 +122,8 @@ function register(app) {
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentNoteId', cloningApiRoute.cloneNoteToParent);
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
|
||||
|
||||
route(GET, '/api/notes/:noteId/export', [auth.checkApiAuthOrElectron], exportRoute.exportNote);
|
||||
route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importTar, apiResultHandler);
|
||||
route(GET, '/api/notes/:noteId/export/:format', [auth.checkApiAuthOrElectron], exportRoute.exportNote);
|
||||
route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importToBranch, apiResultHandler);
|
||||
|
||||
route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware],
|
||||
filesRoute.uploadFile, apiResultHandler);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
|
||||
const APP_DB_VERSION = 88;
|
||||
const APP_DB_VERSION = 96;
|
||||
|
||||
module.exports = {
|
||||
appVersion: packageJson.version,
|
||||
|
||||
254
src/services/autocomplete.js
Normal file
254
src/services/autocomplete.js
Normal file
@@ -0,0 +1,254 @@
|
||||
const sql = require('./sql');
|
||||
const sqlInit = require('./sql_init');
|
||||
const eventService = require('./events');
|
||||
const repository = require('./repository');
|
||||
const protectedSessionService = require('./protected_session');
|
||||
const utils = require('./utils');
|
||||
|
||||
let noteTitles;
|
||||
let protectedNoteTitles;
|
||||
let noteIds;
|
||||
const childToParent = {};
|
||||
const hideInAutocomplete = {};
|
||||
|
||||
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
|
||||
let prefixes = {};
|
||||
|
||||
async function load() {
|
||||
noteTitles = await sql.getMap(`SELECT noteId, title FROM notes WHERE isDeleted = 0 AND isProtected = 0`);
|
||||
noteIds = Object.keys(noteTitles);
|
||||
|
||||
prefixes = await sql.getMap(`SELECT noteId || '-' || parentNoteId, prefix FROM branches WHERE prefix IS NOT NULL AND prefix != ''`);
|
||||
|
||||
const relations = await sql.getRows(`SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0`);
|
||||
|
||||
for (const rel of relations) {
|
||||
childToParent[rel.noteId] = childToParent[rel.noteId] || [];
|
||||
childToParent[rel.noteId].push(rel.parentNoteId);
|
||||
}
|
||||
|
||||
const hiddenLabels = await sql.getColumn(`SELECT noteId FROM labels WHERE isDeleted = 0 AND name = 'hideInAutocomplete'`);
|
||||
|
||||
for (const noteId of hiddenLabels) {
|
||||
hideInAutocomplete[noteId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!noteTitles || query.length <= 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tokens = query.toLowerCase().split(" ");
|
||||
const results = [];
|
||||
|
||||
let noteIds = Object.keys(noteTitles);
|
||||
|
||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||
noteIds = noteIds.concat(Object.keys(protectedNoteTitles));
|
||||
}
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
if (hideInAutocomplete[noteId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parents = childToParent[noteId];
|
||||
if (!parents) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const parentNoteId of parents) {
|
||||
if (hideInAutocomplete[parentNoteId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const title = getNoteTitle(noteId, parentNoteId).toLowerCase();
|
||||
const foundTokens = [];
|
||||
|
||||
for (const token of tokens) {
|
||||
if (title.includes(token)) {
|
||||
foundTokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundTokens.length > 0) {
|
||||
const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
|
||||
|
||||
search(parentNoteId, remainingTokens, [noteId], results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.sort((a, b) => a.title < b.title ? -1 : 1);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function search(noteId, tokens, path, results) {
|
||||
if (tokens.length === 0) {
|
||||
const retPath = getSomePath(noteId, path);
|
||||
|
||||
if (retPath) {
|
||||
const noteTitle = getNoteTitleForPath(retPath);
|
||||
|
||||
results.push({
|
||||
title: noteTitle,
|
||||
path: retPath.join('/')
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const parents = childToParent[noteId];
|
||||
if (!parents) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const parentNoteId of parents) {
|
||||
if (results.length >= 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentNoteId === 'root' || hideInAutocomplete[parentNoteId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const title = getNoteTitle(noteId, parentNoteId);
|
||||
const foundTokens = [];
|
||||
|
||||
for (const token of tokens) {
|
||||
if (title.includes(token)) {
|
||||
foundTokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundTokens.length > 0) {
|
||||
const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
|
||||
|
||||
search(parentNoteId, remainingTokens, path.concat([noteId]), results);
|
||||
}
|
||||
else {
|
||||
search(parentNoteId, tokens, path.concat([noteId]), results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNoteTitle(noteId, parentNoteId) {
|
||||
const prefix = prefixes[noteId + '-' + parentNoteId];
|
||||
|
||||
let title = noteTitles[noteId];
|
||||
|
||||
if (!title) {
|
||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||
title = protectedNoteTitles[noteId];
|
||||
}
|
||||
else {
|
||||
title = '[protected]';
|
||||
}
|
||||
}
|
||||
|
||||
return (prefix ? (prefix + ' - ') : '') + title;
|
||||
}
|
||||
|
||||
function getNoteTitleForPath(path) {
|
||||
const titles = [];
|
||||
|
||||
let parentNoteId = 'root';
|
||||
|
||||
for (const noteId of path) {
|
||||
const title = getNoteTitle(noteId, parentNoteId);
|
||||
|
||||
titles.push(title);
|
||||
parentNoteId = noteId;
|
||||
}
|
||||
|
||||
return titles.join(' / ');
|
||||
}
|
||||
|
||||
function getSomePath(noteId, path) {
|
||||
if (noteId === 'root') {
|
||||
path.reverse();
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
const parents = childToParent[noteId];
|
||||
if (!parents || parents.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const parentNoteId of parents) {
|
||||
const retPath = getSomePath(parentNoteId, path.concat([noteId]));
|
||||
|
||||
if (retPath) {
|
||||
return retPath;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId}) => {
|
||||
if (entityName === 'notes') {
|
||||
const note = await repository.getNote(entityId);
|
||||
|
||||
if (note.isDeleted) {
|
||||
delete noteTitles[note.noteId];
|
||||
delete childToParent[note.noteId];
|
||||
}
|
||||
else {
|
||||
noteTitles[note.noteId] = note.title;
|
||||
}
|
||||
}
|
||||
else if (entityName === 'branches') {
|
||||
const branch = await repository.getBranch(entityId);
|
||||
|
||||
if (childToParent[branch.noteId]) {
|
||||
childToParent[branch.noteId] = childToParent[branch.noteId].filter(noteId => noteId !== branch.parentNoteId)
|
||||
}
|
||||
|
||||
if (branch.isDeleted) {
|
||||
delete prefixes[branch.noteId + '-' + branch.parentNoteId];
|
||||
}
|
||||
else {
|
||||
if (branch.prefix) {
|
||||
prefixes[branch.noteId + '-' + branch.parentNoteId] = branch.prefix;
|
||||
}
|
||||
|
||||
childToParent[branch.noteId] = childToParent[branch.noteId] || [];
|
||||
childToParent[branch.noteId].push(branch.parentNoteId);
|
||||
}
|
||||
}
|
||||
else if (entityName === 'labels') {
|
||||
const label = await repository.getLabel(entityId);
|
||||
|
||||
if (label.name === 'hideInAutocomplete') {
|
||||
// we're not using label object directly, since there might be other non-deleted hideInAutocomplete label
|
||||
const hideLabel = await repository.getEntity(`SELECT * FROM labels WHERE isDeleted = 0
|
||||
AND name = 'hideInAutocomplete' AND noteId = ?`, [label.noteId]);
|
||||
|
||||
if (hideLabel) {
|
||||
hideInAutocomplete[label.noteId] = true;
|
||||
}
|
||||
else {
|
||||
delete hideInAutocomplete[label.noteId];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
|
||||
protectedNoteTitles = await sql.getMap(`SELECT noteId, title FROM notes WHERE isDeleted = 0 AND isProtected = 1`);
|
||||
|
||||
for (const noteId in protectedNoteTitles) {
|
||||
protectedNoteTitles[noteId] = protectedSessionService.decryptNoteTitle(noteId, protectedNoteTitles[noteId]);
|
||||
}
|
||||
});
|
||||
|
||||
sqlInit.dbReady.then(() => utils.stopWatch("Autocomplete load", load));
|
||||
|
||||
module.exports = {
|
||||
getResults
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2018-04-11T00:10:33-04:00", buildRevision: "a4eafb934ff3cdb46dbc138b1b02850872948699" };
|
||||
module.exports = { buildDate:"2018-05-31T23:23:44-04:00", buildRevision: "80d2457b23b916b869fe2a91ae4e65e33ab42549" };
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const sql = require('./sql');
|
||||
const sqlInit = require('./sql_init');
|
||||
const log = require('./log');
|
||||
const utils = require('./utils');
|
||||
const messagingService = require('./messaging');
|
||||
const syncMutexService = require('./sync_mutex');
|
||||
const cls = require('./cls');
|
||||
@@ -116,7 +117,7 @@ async function runAllChecks() {
|
||||
WHERE
|
||||
notes.isDeleted = 1
|
||||
AND branches.isDeleted = 0`,
|
||||
"Note tree is not deleted even though main note is deleted for following branch IDs", errorList);
|
||||
"Branch is not deleted even though main note is deleted for following branch IDs", errorList);
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
@@ -125,12 +126,12 @@ async function runAllChecks() {
|
||||
branches AS child
|
||||
WHERE
|
||||
child.isDeleted = 0
|
||||
AND child.parentNoteId != 'root'
|
||||
AND child.parentNoteId != 'none'
|
||||
AND (SELECT COUNT(*) FROM branches AS parent WHERE parent.noteId = child.parentNoteId
|
||||
AND parent.isDeleted = 0) = 0`,
|
||||
"All parent branches are deleted but child note tree is not for these child note tree IDs", errorList);
|
||||
"All parent branches are deleted but child branch is not for these child branch IDs", errorList);
|
||||
|
||||
// we do extra JOIN to eliminate orphan notes without note tree (which are reported separately)
|
||||
// we do extra JOIN to eliminate orphan notes without branches (which are reported separately)
|
||||
await runCheck(`
|
||||
SELECT
|
||||
DISTINCT noteId
|
||||
@@ -150,7 +151,7 @@ async function runAllChecks() {
|
||||
LEFT JOIN branches AS parent ON parent.noteId = child.parentNoteId
|
||||
WHERE
|
||||
parent.noteId IS NULL
|
||||
AND child.parentNoteId != 'root'`,
|
||||
AND child.parentNoteId != 'none'`,
|
||||
"Not existing parent in the following parent > child relations", errorList);
|
||||
|
||||
await runCheck(`
|
||||
@@ -197,16 +198,6 @@ async function runAllChecks() {
|
||||
AND images.isDeleted = 1`,
|
||||
"Note image is not deleted while image is deleted for noteImageId", errorList);
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
noteId
|
||||
FROM
|
||||
notes
|
||||
WHERE
|
||||
isDeleted = 0
|
||||
AND (title IS NULL OR content IS NULL)`,
|
||||
"Note has null title or text", errorList);
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
noteId
|
||||
|
||||
@@ -5,117 +5,40 @@ const utils = require('./utils');
|
||||
const log = require('./log');
|
||||
const eventLogService = require('./event_log');
|
||||
const messagingService = require('./messaging');
|
||||
const ApiToken = require('../entities/api_token');
|
||||
const Branch = require('../entities/branch');
|
||||
const Image = require('../entities/image');
|
||||
const Note = require('../entities/note');
|
||||
const NoteImage = require('../entities/note_image');
|
||||
const Label = require('../entities/label');
|
||||
const NoteRevision = require('../entities/note_revision');
|
||||
const RecentNote = require('../entities/recent_note');
|
||||
const Option = require('../entities/option');
|
||||
|
||||
function getHash(rows) {
|
||||
let hash = '';
|
||||
async function getHash(entityConstructor, whereBranch) {
|
||||
let contentToHash = await sql.getValue(`SELECT GROUP_CONCAT(hash) FROM ${entityConstructor.tableName} `
|
||||
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName}`);
|
||||
|
||||
for (const row of rows) {
|
||||
hash = utils.hash(hash + JSON.stringify(row));
|
||||
if (!contentToHash) { // might be null in case of no rows
|
||||
contentToHash = "";
|
||||
}
|
||||
|
||||
return hash;
|
||||
return utils.hash(contentToHash);
|
||||
}
|
||||
|
||||
async function getHashes() {
|
||||
const startTime = new Date();
|
||||
|
||||
const hashes = {
|
||||
notes: getHash(await sql.getRows(`
|
||||
SELECT
|
||||
noteId,
|
||||
title,
|
||||
content,
|
||||
type,
|
||||
dateModified,
|
||||
isProtected,
|
||||
isDeleted
|
||||
FROM notes
|
||||
ORDER BY noteId`)),
|
||||
|
||||
branches: getHash(await sql.getRows(`
|
||||
SELECT
|
||||
branchId,
|
||||
noteId,
|
||||
parentNoteId,
|
||||
notePosition,
|
||||
dateModified,
|
||||
isDeleted,
|
||||
prefix
|
||||
FROM branches
|
||||
ORDER BY branchId`)),
|
||||
|
||||
note_revisions: getHash(await sql.getRows(`
|
||||
SELECT
|
||||
noteRevisionId,
|
||||
noteId,
|
||||
title,
|
||||
content,
|
||||
dateModifiedFrom,
|
||||
dateModifiedTo
|
||||
FROM note_revisions
|
||||
ORDER BY noteRevisionId`)),
|
||||
|
||||
recent_notes: getHash(await sql.getRows(`
|
||||
SELECT
|
||||
branchId,
|
||||
notePath,
|
||||
dateAccessed,
|
||||
isDeleted
|
||||
FROM recent_notes
|
||||
ORDER BY notePath`)),
|
||||
|
||||
options: getHash(await sql.getRows(`
|
||||
SELECT
|
||||
name,
|
||||
value
|
||||
FROM options
|
||||
WHERE isSynced = 1
|
||||
ORDER BY name`)),
|
||||
|
||||
// we don't include image data on purpose because they are quite large, checksum is good enough
|
||||
// to represent the data anyway
|
||||
images: getHash(await sql.getRows(`
|
||||
SELECT
|
||||
imageId,
|
||||
format,
|
||||
checksum,
|
||||
name,
|
||||
isDeleted,
|
||||
dateModified,
|
||||
dateCreated
|
||||
FROM images
|
||||
ORDER BY imageId`)),
|
||||
|
||||
note_images: getHash(await sql.getRows(`
|
||||
SELECT
|
||||
noteImageId,
|
||||
noteId,
|
||||
imageId,
|
||||
isDeleted,
|
||||
dateModified,
|
||||
dateCreated
|
||||
FROM note_images
|
||||
ORDER BY noteImageId`)),
|
||||
|
||||
labels: getHash(await sql.getRows(`
|
||||
SELECT
|
||||
labelId,
|
||||
noteId,
|
||||
name,
|
||||
value,
|
||||
dateModified,
|
||||
dateCreated
|
||||
FROM labels
|
||||
ORDER BY labelId`)),
|
||||
|
||||
api_tokens: getHash(await sql.getRows(`
|
||||
SELECT
|
||||
apiTokenId,
|
||||
token,
|
||||
dateCreated,
|
||||
isDeleted
|
||||
FROM api_tokens
|
||||
ORDER BY apiTokenId`))
|
||||
notes: await getHash(Note),
|
||||
branches: await getHash(Branch),
|
||||
note_revisions: await getHash(NoteRevision),
|
||||
recent_notes: await getHash(RecentNote),
|
||||
options: await getHash(Option, "isSynced = 1"),
|
||||
images: await getHash(Image),
|
||||
note_images: await getHash(NoteImage),
|
||||
labels: await getHash(Label),
|
||||
api_tokens: await getHash(ApiToken)
|
||||
};
|
||||
|
||||
const elapseTimeMs = new Date().getTime() - startTime.getTime();
|
||||
@@ -133,7 +56,7 @@ async function checkContentHashes(otherHashes) {
|
||||
if (hashes[key] !== otherHashes[key]) {
|
||||
allChecksPassed = false;
|
||||
|
||||
await eventLogService.addEvent(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${resp.hashes[key]}`);
|
||||
await eventLogService.addEvent(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${otherHashes[key]}`);
|
||||
|
||||
if (key !== 'recent_notes') {
|
||||
// let's not get alarmed about recent notes which get updated often and can cause failures in race conditions
|
||||
|
||||
@@ -31,6 +31,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
|
||||
}
|
||||
|
||||
async function getRootCalendarNote() {
|
||||
// some caching here could be useful (e.g. in CLS)
|
||||
let rootNote = await labelService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
|
||||
|
||||
if (!rootNote) {
|
||||
@@ -89,10 +90,8 @@ async function getMonthNote(dateTimeStr, rootNote) {
|
||||
return monthNote;
|
||||
}
|
||||
|
||||
async function getDateNote(dateTimeStr, rootNote = null) {
|
||||
if (!rootNote) {
|
||||
rootNote = await getRootCalendarNote();
|
||||
}
|
||||
async function getDateNote(dateTimeStr) {
|
||||
const rootNote = await getRootCalendarNote();
|
||||
|
||||
const dateStr = dateTimeStr.substr(0, 10);
|
||||
const dayNumber = dateTimeStr.substr(8, 2);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const sql = require('./sql');
|
||||
const dateUtils = require('./date_utils');
|
||||
const utils = require('./utils');
|
||||
const log = require('./log');
|
||||
|
||||
async function addEvent(comment) {
|
||||
@@ -8,9 +9,10 @@ async function addEvent(comment) {
|
||||
|
||||
async function addNoteEvent(noteId, comment) {
|
||||
await sql.insert('event_log', {
|
||||
noteId : noteId,
|
||||
comment: comment,
|
||||
dateAdded: dateUtils.nowDate()
|
||||
eventId: utils.newEntityId(),
|
||||
noteId : noteId,
|
||||
comment: comment,
|
||||
dateCreated: dateUtils.nowDate()
|
||||
});
|
||||
|
||||
log.info("Event log for " + noteId + ": " + comment);
|
||||
|
||||
28
src/services/events.js
Normal file
28
src/services/events.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
|
||||
const ENTITY_CHANGED = "ENTITY_CHANGED";
|
||||
|
||||
const eventListeners = {};
|
||||
|
||||
function subscribe(eventType, listener) {
|
||||
eventListeners[eventType] = eventListeners[eventType] || [];
|
||||
eventListeners[eventType].push(listener);
|
||||
}
|
||||
|
||||
function emit(eventType, data) {
|
||||
const listeners = eventListeners[eventType];
|
||||
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
// not awaiting for async processing
|
||||
listener(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
subscribe,
|
||||
emit,
|
||||
// event types:
|
||||
ENTER_PROTECTED_SESSION,
|
||||
ENTITY_CHANGED
|
||||
};
|
||||
@@ -1,45 +1,37 @@
|
||||
const sql = require('./sql');
|
||||
const repository = require('./repository');
|
||||
const utils = require('./utils');
|
||||
const dateUtils = require('./date_utils');
|
||||
const syncTableService = require('./sync_table');
|
||||
const appInfo = require('./app_info');
|
||||
const Option = require('../entities/option');
|
||||
|
||||
async function getOption(name) {
|
||||
const row = await await sql.getRowOrNull("SELECT value FROM options WHERE name = ?", [name]);
|
||||
const option = await repository.getOption(name);
|
||||
|
||||
if (!row) {
|
||||
if (!option) {
|
||||
throw new Error("Option " + name + " doesn't exist");
|
||||
}
|
||||
|
||||
return row.value;
|
||||
return option.value;
|
||||
}
|
||||
|
||||
async function setOption(name, value) {
|
||||
const opt = await sql.getRow("SELECT * FROM options WHERE name = ?", [name]);
|
||||
const option = await repository.getOption(name);
|
||||
|
||||
if (!opt) {
|
||||
if (!option) {
|
||||
throw new Error(`Option ${name} doesn't exist`);
|
||||
}
|
||||
|
||||
if (opt.isSynced) {
|
||||
await syncTableService.addOptionsSync(name);
|
||||
}
|
||||
option.value = value;
|
||||
|
||||
await sql.execute("UPDATE options SET value = ?, dateModified = ? WHERE name = ?",
|
||||
[value, dateUtils.nowDate(), name]);
|
||||
await option.save();
|
||||
}
|
||||
|
||||
async function createOption(name, value, isSynced) {
|
||||
await sql.insert("options", {
|
||||
await new Option({
|
||||
name: name,
|
||||
value: value,
|
||||
isSynced: isSynced,
|
||||
dateModified: dateUtils.nowDate()
|
||||
});
|
||||
|
||||
if (isSynced) {
|
||||
await syncTableService.addOptionsSync(name);
|
||||
}
|
||||
isSynced: isSynced
|
||||
}).save();
|
||||
}
|
||||
|
||||
async function initOptions(startNotePath) {
|
||||
|
||||
@@ -6,7 +6,7 @@ const cls = require('./cls');
|
||||
|
||||
const dataKeyMap = {};
|
||||
|
||||
function setDataKey(req, decryptedDataKey) {
|
||||
function setDataKey(decryptedDataKey) {
|
||||
const protectedSessionId = utils.randomSecureToken(32);
|
||||
|
||||
dataKeyMap[protectedSessionId] = Array.from(decryptedDataKey); // can't store buffer in session
|
||||
@@ -28,12 +28,20 @@ function getDataKey() {
|
||||
return dataKeyMap[protectedSessionId];
|
||||
}
|
||||
|
||||
function isProtectedSessionAvailable(req) {
|
||||
const protectedSessionId = getProtectedSessionId(req);
|
||||
function isProtectedSessionAvailable() {
|
||||
const protectedSessionId = getProtectedSessionId();
|
||||
|
||||
return !!dataKeyMap[protectedSessionId];
|
||||
}
|
||||
|
||||
function decryptNoteTitle(noteId, encryptedTitle) {
|
||||
const dataKey = getDataKey();
|
||||
|
||||
const iv = dataEncryptionService.noteTitleIv(noteId);
|
||||
|
||||
return dataEncryptionService.decryptString(dataKey, iv, encryptedTitle);
|
||||
}
|
||||
|
||||
function decryptNote(note) {
|
||||
const dataKey = getDataKey();
|
||||
|
||||
@@ -99,6 +107,7 @@ module.exports = {
|
||||
setDataKey,
|
||||
getDataKey,
|
||||
isProtectedSessionAvailable,
|
||||
decryptNoteTitle,
|
||||
decryptNote,
|
||||
decryptNotes,
|
||||
decryptNoteRevision,
|
||||
|
||||
@@ -41,6 +41,10 @@ async function getLabel(labelId) {
|
||||
return await getEntity("SELECT * FROM labels WHERE labelId = ?", [labelId]);
|
||||
}
|
||||
|
||||
async function getOption(name) {
|
||||
return await getEntity("SELECT * FROM options WHERE name = ?", [name]);
|
||||
}
|
||||
|
||||
async function updateEntity(entity) {
|
||||
if (entity.beforeSaving) {
|
||||
await entity.beforeSaving();
|
||||
@@ -55,7 +59,9 @@ async function updateEntity(entity) {
|
||||
|
||||
const primaryKey = entity[entity.constructor.primaryKeyName];
|
||||
|
||||
await syncTableService.addEntitySync(entity.constructor.tableName, primaryKey);
|
||||
if (entity.constructor.tableName !== 'options' || entity.isSynced) {
|
||||
await syncTableService.addEntitySync(entity.constructor.tableName, primaryKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,6 +72,7 @@ module.exports = {
|
||||
getBranch,
|
||||
getImage,
|
||||
getLabel,
|
||||
getOption,
|
||||
updateEntity,
|
||||
setEntityConstructor
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
const sql = require('./sql');
|
||||
const ScriptContext = require('./script_context');
|
||||
const repository = require('./repository');
|
||||
const cls = require('./cls');
|
||||
const sourceIdService = require('./source_id');
|
||||
|
||||
async function executeNote(note) {
|
||||
if (!note.isJavaScript()) {
|
||||
@@ -49,6 +51,9 @@ async function executeScript(script, params, startNoteId, currentNoteId) {
|
||||
}
|
||||
|
||||
async function execute(ctx, script, paramsStr) {
|
||||
// scripts run as "server" sourceId so clients recognize the changes as "foreign" and update themselves
|
||||
cls.namespace.set('sourceId', sourceIdService.getCurrentSourceId());
|
||||
|
||||
return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx));
|
||||
}
|
||||
|
||||
@@ -68,7 +73,7 @@ function getParams(params) {
|
||||
}
|
||||
|
||||
async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) {
|
||||
if (!note.isJavaScript() && !note.isHtml() && note.type !== 'render') {
|
||||
if (!note.isJavaScript() && !note.isHtml()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,26 @@ async function getValue(query, params = []) {
|
||||
return row[Object.keys(row)[0]];
|
||||
}
|
||||
|
||||
const PARAM_LIMIT = 900; // actual limit is 999
|
||||
|
||||
// this is to overcome 999 limit of number of query parameters
|
||||
async function getManyRows(query, params) {
|
||||
let results = [];
|
||||
|
||||
while (params.length > 0) {
|
||||
const curParams = params.slice(0, Math.max(params.length, PARAM_LIMIT));
|
||||
params = params.slice(curParams.length);
|
||||
|
||||
let i = 1;
|
||||
const questionMarks = curParams.map(() => "?" + i++).join(",");
|
||||
const curQuery = query.replace(/\?\?\?/g, questionMarks);
|
||||
|
||||
results = results.concat(await getRows(curQuery, curParams));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function getRows(query, params = []) {
|
||||
return await wrap(async db => db.all(query, ...params));
|
||||
}
|
||||
@@ -179,6 +199,7 @@ module.exports = {
|
||||
getRow,
|
||||
getRowOrNull,
|
||||
getRows,
|
||||
getManyRows,
|
||||
getMap,
|
||||
getColumn,
|
||||
execute,
|
||||
|
||||
@@ -101,7 +101,7 @@ async function pullSync(syncContext) {
|
||||
log.info(`Skipping pull #${sync.id} ${sync.entityName} ${sync.entityId} because ${sync.sourceId} is a local source id.`);
|
||||
}
|
||||
else {
|
||||
await syncUpdateService.updateEntity(sync.entityName, entity, syncContext.sourceId);
|
||||
await syncUpdateService.updateEntity(sync, entity, syncContext.sourceId);
|
||||
}
|
||||
|
||||
await setLastSyncedPull(sync.id);
|
||||
@@ -207,12 +207,12 @@ const primaryKeys = {
|
||||
"notes": "noteId",
|
||||
"branches": "branchId",
|
||||
"note_revisions": "noteRevisionId",
|
||||
"option": "name",
|
||||
"recent_notes": "branchId",
|
||||
"images": "imageId",
|
||||
"note_images": "noteImageId",
|
||||
"labels": "labelId",
|
||||
"api_tokens": "apiTokenId"
|
||||
"api_tokens": "apiTokenId",
|
||||
"options": "optionId"
|
||||
};
|
||||
|
||||
async function getEntityRow(entityName, entityId) {
|
||||
|
||||
@@ -4,6 +4,7 @@ const dateUtils = require('./date_utils');
|
||||
const syncSetup = require('./sync_setup');
|
||||
const log = require('./log');
|
||||
const cls = require('./cls');
|
||||
const eventService = require('./events');
|
||||
|
||||
async function addNoteSync(noteId, sourceId) {
|
||||
await addEntitySync("notes", noteId, sourceId)
|
||||
@@ -58,6 +59,11 @@ async function addEntitySync(entityName, entityId, sourceId) {
|
||||
// useful when you fork the DB for new "client" instance, it won't try to sync the whole DB
|
||||
await sql.execute("UPDATE options SET value = (SELECT MAX(id) FROM sync) WHERE name IN('lastSyncedPush', 'lastSyncedPull')");
|
||||
}
|
||||
|
||||
eventService.emit(eventService.ENTITY_CHANGED, {
|
||||
entityName,
|
||||
entityId
|
||||
});
|
||||
}
|
||||
|
||||
async function cleanupSyncRowsForMissingEntities(entityName, entityKey) {
|
||||
|
||||
@@ -3,7 +3,9 @@ const log = require('./log');
|
||||
const eventLogService = require('./event_log');
|
||||
const syncTableService = require('./sync_table');
|
||||
|
||||
async function updateEntity(entityName, entity, sourceId) {
|
||||
async function updateEntity(sync, entity, sourceId) {
|
||||
const {entityName} = sync;
|
||||
|
||||
if (entityName === 'notes') {
|
||||
await updateNote(entity, sourceId);
|
||||
}
|
||||
@@ -14,7 +16,7 @@ async function updateEntity(entityName, entity, sourceId) {
|
||||
await updateNoteRevision(entity, sourceId);
|
||||
}
|
||||
else if (entityName === 'note_reordering') {
|
||||
await updateNoteReordering(entity, sourceId);
|
||||
await updateNoteReordering(sync.entityId, entity, sourceId);
|
||||
}
|
||||
else if (entityName === 'options') {
|
||||
await updateOptions(entity, sourceId);
|
||||
@@ -35,7 +37,7 @@ async function updateEntity(entityName, entity, sourceId) {
|
||||
await updateApiToken(entity, sourceId);
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unrecognized entity type ${entityName}`);
|
||||
throw new Error(`Unrecognized entity type ${sync}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,13 +96,13 @@ async function updateNoteRevision(entity, sourceId) {
|
||||
});
|
||||
}
|
||||
|
||||
async function updateNoteReordering(entity, sourceId) {
|
||||
async function updateNoteReordering(entityId, entity, sourceId) {
|
||||
await sql.transactional(async () => {
|
||||
Object.keys(entity.ordering).forEach(async key => {
|
||||
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity.ordering[key], key]);
|
||||
Object.keys(entity).forEach(async key => {
|
||||
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]);
|
||||
});
|
||||
|
||||
await syncTableService.addNoteReorderingSync(entity.parentNoteId, sourceId);
|
||||
await syncTableService.addNoteReorderingSync(entityId, sourceId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -125,7 +127,7 @@ async function updateOptions(entity, sourceId) {
|
||||
async function updateRecentNotes(entity, sourceId) {
|
||||
const orig = await sql.getRowOrNull("SELECT * FROM recent_notes WHERE branchId = ?", [entity.branchId]);
|
||||
|
||||
if (orig === null || orig.dateAccessed < entity.dateAccessed) {
|
||||
if (orig === null || orig.dateCreated < entity.dateCreated) {
|
||||
await sql.transactional(async () => {
|
||||
await sql.replace('recent_notes', entity);
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ function toObject(array, fn) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
function stripTags(text) {
|
||||
return text.replace(/<(?:.|\n)*?>/gm, '');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
randomSecureToken,
|
||||
randomString,
|
||||
@@ -88,5 +92,6 @@ module.exports = {
|
||||
sanitizeSql,
|
||||
stopWatch,
|
||||
unescapeHtml,
|
||||
toObject
|
||||
toObject,
|
||||
stripTags
|
||||
};
|
||||
@@ -58,7 +58,7 @@ async function start() {
|
||||
}
|
||||
|
||||
// we'll create clones for 20% of notes
|
||||
for (let i = 0; i < (noteCount / 5); i++) {
|
||||
for (let i = 0; i < (noteCount / 50); i++) {
|
||||
const noteIdToClone = getRandomParentNoteId();
|
||||
const parentNoteId = getRandomParentNoteId();
|
||||
const prefix = Math.random() > 0.8 ? "prefix" : null;
|
||||
|
||||
@@ -13,10 +13,28 @@
|
||||
Trilium Notes
|
||||
</div>
|
||||
|
||||
<div style="flex-grow: 100;">
|
||||
<div id="history-navigation" style="display: none;">
|
||||
<a id="history-back-button" title="Go to previous note." class="icon-action"
|
||||
style="background: url('/images/icons/back.png')"></a>
|
||||
|
||||
|
||||
|
||||
<a id="history-forward-button" title="Go to next note." class="icon-action"
|
||||
style="background: url('/images/icons/forward.png')"></a>
|
||||
</div>
|
||||
|
||||
<div style="flex-grow: 100; display: flex;">
|
||||
<button class="btn btn-xs" id="jump-to-note-button" title="CTRL+J">Jump to note</button>
|
||||
<button class="btn btn-xs" id="recent-notes-button" title="CTRL+E">Recent notes</button>
|
||||
<button class="btn btn-xs" id="recent-changes-button">Recent changes</button>
|
||||
<div>
|
||||
<span style="font-size: smaller">Protected session:</span>
|
||||
|
||||
<div class="btn-group btn-group-xs">
|
||||
<button type="button" class="btn" id="protected-session-on">On</button>
|
||||
<button type="button" class="btn active" id="protected-session-off">Off</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="plugin-buttons">
|
||||
@@ -38,7 +56,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide-toggle" style="grid-area: tree-actions;">
|
||||
<div style="grid-area: left-pane; display: flex; flex-direction: column;" class="hide-toggle">
|
||||
<div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
|
||||
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action"
|
||||
style="background: url('/images/icons/file-plus.png')"></a>
|
||||
@@ -54,32 +72,39 @@
|
||||
</div>
|
||||
|
||||
<input type="file" id="import-upload" style="display: none" />
|
||||
</div>
|
||||
|
||||
<div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
|
||||
<button id="do-search-button" class="btn btn-primary btn-sm" title="Search">Search</button>
|
||||
<div id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
|
||||
<button id="do-search-button" class="btn btn-primary btn-sm" title="Search">Search</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; justify-content: space-evenly; margin-top: 10px;">
|
||||
<button id="reset-search-button" class="btn btn-sm" title="Reset search">Reset search</button>
|
||||
|
||||
<button id="save-search-button" class="btn btn-sm" title="Save search">Save search</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; justify-content: space-evenly; margin-top: 10px;">
|
||||
<button id="reset-search-button" class="btn btn-sm" title="Reset search">Reset search</button>
|
||||
|
||||
<button id="save-search-button" class="btn btn-sm" title="Save search">Save search</button>
|
||||
</div>
|
||||
<div id="tree"></div>
|
||||
</div>
|
||||
|
||||
<div id="tree" class="hide-toggle" style="grid-area: tree; overflow: auto;">
|
||||
</div>
|
||||
<div style="grid-area: title;">
|
||||
<div class="hide-toggle" style="display: flex; align-items: center;">
|
||||
<div class="dropdown">
|
||||
<button id="note-path-list-button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
|
||||
<span id="note-path-count">1 path</span>
|
||||
|
||||
<div id="parent-list" class="hide-toggle">
|
||||
<p><strong>Note locations:</strong></p>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul id="note-path-list" class="dropdown-menu" aria-labelledby="note-path-list-button">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul id="parent-list-inner"></ul>
|
||||
</div>
|
||||
<input autocomplete="off" value="" id="note-title" style="margin-left: 15px; font-size: x-large; border: 0; flex-grow: 100;" tabindex="1">
|
||||
|
||||
<span id="note-id-display" title="Note ID"></span>
|
||||
|
||||
<div class="hide-toggle" style="grid-area: title;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<a title="Protect the note so that password will be required to view the note"
|
||||
class="icon-action"
|
||||
id="protect-button"
|
||||
@@ -90,11 +115,15 @@
|
||||
id="unprotect-button"
|
||||
style="display: none; background: url('images/icons/unlock.png')"></a>
|
||||
|
||||
|
||||
|
||||
|
||||
<input autocomplete="off" value="" id="note-title" style="font-size: x-large; border: 0; flex-grow: 100;" tabindex="1">
|
||||
<button class="btn btn-sm"
|
||||
style="display: none; margin-right: 10px"
|
||||
id="toggle-edit-button">Toggle edit</button>
|
||||
|
||||
<span id="note-id-display" title="Note ID"></span>
|
||||
<button class="btn btn-sm"
|
||||
style="display: none; margin-right: 10px"
|
||||
id="render-button">Render <kbd>Ctrl+Enter</kbd></button>
|
||||
|
||||
<button class="btn btn-sm"
|
||||
style="display: none; margin-right: 10px"
|
||||
@@ -105,7 +134,7 @@
|
||||
Type: <span data-bind="text: typeString()"></span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul id="note-type-dropdown" class="dropdown-menu dropdown-menu-right" aria-labelledby="dLabel">
|
||||
<ul id="note-type-dropdown" class="dropdown-menu dropdown-menu-right">
|
||||
<li data-bind="click: selectText, css: { selected: type() == 'text' }"><span class="check">✓</span> <strong>Text</strong></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li data-bind="click: selectRender, css: { selected: type() == 'render' && mime() == '' }"><span class="check">✓</span> <strong>Render HTML note</strong></li>
|
||||
@@ -122,7 +151,7 @@
|
||||
Note actions
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dLabel">
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a id="show-note-revisions-button">Note revisions</a></li>
|
||||
<li><a class="show-labels-button"><kbd>Alt+L</kbd> Labels</a></li>
|
||||
<li><a id="show-source-button"><kbd>Ctrl+U</kbd> HTML source</a></li>
|
||||
@@ -132,9 +161,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: relative; overflow: hidden; grid-area: note-detail; padding-left: 10px; padding-top: 10px; display: flex; flex-direction: column;" id="note-detail-wrapper">
|
||||
<div style="flex-grow: 1; position: relative; overflow: auto; flex-basis: content;">
|
||||
<div id="note-detail-text" style="height: 100%;" class="note-detail-component"></div>
|
||||
<div id="note-detail-wrapper">
|
||||
<div id="note-detail-component-wrapper">
|
||||
<div id="note-detail-text" class="note-detail-component" tabindex="2"></div>
|
||||
|
||||
<div id="note-detail-search" class="note-detail-component">
|
||||
<div style="display: flex; align-items: center;">
|
||||
@@ -206,12 +235,12 @@
|
||||
</div>
|
||||
|
||||
<div id="children-overview"></div>
|
||||
</div>
|
||||
|
||||
<div id="label-list">
|
||||
<button class="btn btn-sm show-labels-button">Labels:</button>
|
||||
<div id="label-list">
|
||||
<button class="btn btn-sm show-labels-button">Labels:</button>
|
||||
|
||||
<span id="label-list-inner"></span>
|
||||
<span id="label-list-inner"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -221,7 +250,7 @@
|
||||
|
||||
<div id="add-link-dialog" title="Add link" style="display: none;">
|
||||
<form id="add-link-form">
|
||||
<div class="radio">
|
||||
<div id="add-link-type-div" class="radio">
|
||||
<label title="Add HTML link to the selected note at cursor in current note">
|
||||
<input type="radio" name="add-link-type" value="html"/>
|
||||
add normal HTML link</label>
|
||||
@@ -425,7 +454,7 @@
|
||||
</div>
|
||||
|
||||
<div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;">
|
||||
<div style="height: 150px; width: 100%; border: 1px solid #ccc; margin-bottom: 10px;" id="sql-console-query"></div>
|
||||
<div id="sql-console-query"></div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
|
||||
|
||||
Reference in New Issue
Block a user