mirror of
https://github.com/zadam/trilium.git
synced 2025-10-28 08:46:43 +01:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31b76b23ce | ||
|
|
af529f82e5 | ||
|
|
fc6669d254 | ||
|
|
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 |
@@ -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;
|
||||
@@ -1,8 +1,3 @@
|
||||
CREATE TABLE IF NOT EXISTS "options" (
|
||||
`name` TEXT NOT NULL PRIMARY KEY,
|
||||
`value` TEXT,
|
||||
`dateModified` INT,
|
||||
isSynced INTEGER NOT NULL DEFAULT 0);
|
||||
CREATE TABLE IF NOT EXISTS "sync" (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`entityName` TEXT NOT NULL,
|
||||
@@ -29,7 +24,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (
|
||||
`isProtected` INT NOT NULL DEFAULT 0,
|
||||
`dateModifiedFrom` TEXT NOT NULL,
|
||||
`dateModifiedTo` TEXT NOT NULL
|
||||
, type TEXT DEFAULT '' NOT NULL, mime TEXT DEFAULT '' NOT NULL);
|
||||
, type TEXT DEFAULT '' NOT NULL, mime TEXT DEFAULT '' NOT NULL, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
|
||||
`noteId`
|
||||
);
|
||||
@@ -49,7 +44,7 @@ CREATE TABLE IF NOT EXISTS "images"
|
||||
isDeleted INT NOT NULL DEFAULT 0,
|
||||
dateModified TEXT NOT NULL,
|
||||
dateCreated TEXT NOT NULL
|
||||
);
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE TABLE note_images
|
||||
(
|
||||
noteImageId TEXT PRIMARY KEY NOT NULL,
|
||||
@@ -58,7 +53,7 @@ CREATE TABLE note_images
|
||||
isDeleted INT NOT NULL DEFAULT 0,
|
||||
dateModified TEXT NOT NULL,
|
||||
dateCreated TEXT NOT NULL
|
||||
);
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX IDX_note_images_noteId ON note_images (noteId);
|
||||
CREATE INDEX IDX_note_images_imageId ON note_images (imageId);
|
||||
CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId);
|
||||
@@ -68,7 +63,7 @@ CREATE TABLE IF NOT EXISTS "api_tokens"
|
||||
token TEXT NOT NULL,
|
||||
dateCreated TEXT NOT NULL,
|
||||
isDeleted INT NOT NULL DEFAULT 0
|
||||
);
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "branches" (
|
||||
`branchId` TEXT NOT NULL,
|
||||
`noteId` TEXT NOT NULL,
|
||||
@@ -77,7 +72,7 @@ CREATE TABLE IF NOT EXISTS "branches" (
|
||||
`prefix` TEXT,
|
||||
`isExpanded` BOOLEAN,
|
||||
`isDeleted` INTEGER NOT NULL DEFAULT 0,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
`dateModified` TEXT NOT NULL, hash TEXT DEFAULT "" NOT NULL, dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z',
|
||||
PRIMARY KEY(`branchId`)
|
||||
);
|
||||
CREATE INDEX `IDX_branches_noteId` ON `branches` (
|
||||
@@ -87,12 +82,6 @@ CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
|
||||
`noteId`,
|
||||
`parentNoteId`
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "recent_notes" (
|
||||
`branchId` TEXT NOT NULL PRIMARY KEY,
|
||||
`notePath` TEXT NOT NULL,
|
||||
`dateAccessed` TEXT NOT NULL,
|
||||
isDeleted INT
|
||||
);
|
||||
CREATE TABLE labels
|
||||
(
|
||||
labelId TEXT not null primary key,
|
||||
@@ -103,18 +92,11 @@ CREATE TABLE labels
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null
|
||||
);
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX IDX_labels_name_value
|
||||
on labels (name, value);
|
||||
CREATE INDEX IDX_labels_noteId
|
||||
on labels (noteId);
|
||||
CREATE TABLE IF NOT EXISTS "event_log"
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
noteId TEXT,
|
||||
comment TEXT,
|
||||
dateAdded TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "notes" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`title` TEXT NOT NULL DEFAULT "unnamed",
|
||||
@@ -124,9 +106,31 @@ CREATE TABLE IF NOT EXISTS "notes" (
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'text',
|
||||
mime TEXT NOT NULL DEFAULT 'text/html',
|
||||
mime TEXT NOT NULL DEFAULT 'text/html', hash TEXT DEFAULT "" NOT NULL,
|
||||
PRIMARY KEY(`noteId`)
|
||||
);
|
||||
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
|
||||
`isDeleted`
|
||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
|
||||
CREATE INDEX IDX_notes_type
|
||||
on notes (type);
|
||||
CREATE TABLE IF NOT EXISTS "recent_notes" (
|
||||
`branchId` TEXT NOT NULL PRIMARY KEY,
|
||||
`notePath` TEXT NOT NULL,
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
isDeleted INT
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "event_log" (
|
||||
`eventId` TEXT NOT NULL PRIMARY KEY,
|
||||
`noteId` TEXT,
|
||||
`comment` TEXT,
|
||||
`dateCreated` TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "options"
|
||||
(
|
||||
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
|
||||
);
|
||||
|
||||
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.12.0",
|
||||
"version": "0.14.1",
|
||||
"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 "optionId"; }
|
||||
static get hashedProperties() { return ["optionId", "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"]');
|
||||
|
||||
@@ -52,8 +54,12 @@ async function showDialog() {
|
||||
}
|
||||
|
||||
$autoComplete.autocomplete({
|
||||
source: await autocompleteService.getAutocompleteItems(),
|
||||
minLength: 0,
|
||||
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);
|
||||
@@ -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>", {
|
||||
|
||||
@@ -64,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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -6,7 +6,7 @@
|
||||
grid-template-areas: "header header"
|
||||
"left-pane title"
|
||||
"left-pane note-detail";
|
||||
grid-template-columns: 2fr 5fr;
|
||||
grid-template-columns: 29% 70%;
|
||||
grid-template-rows: auto
|
||||
auto
|
||||
1fr;
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
#note-detail-component-wrapper {
|
||||
flex-grow: 1;
|
||||
flex-grow: 100;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
flex-basis: content;
|
||||
@@ -61,8 +61,6 @@
|
||||
}
|
||||
|
||||
ul.fancytree-container {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
@@ -107,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;
|
||||
@@ -161,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;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -233,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;
|
||||
}
|
||||
@@ -261,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 {
|
||||
@@ -330,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);
|
||||
|
||||
@@ -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-14T08:28:50-04:00", buildRevision: "d57057ba28d2d93ffaeed15900116836fc791968" };
|
||||
module.exports = { buildDate:"2018-06-02T09:39:37-04:00", buildRevision: "af529f82e5080f01b26ac7db104a8041f137dc48" };
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -5,117 +5,43 @@ 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) {
|
||||
// subselect is necessary to have correct ordering in GROUP_CONCAT
|
||||
const query = `SELECT GROUP_CONCAT(hash) FROM (SELECT hash FROM ${entityConstructor.tableName} `
|
||||
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName})`;
|
||||
|
||||
for (const row of rows) {
|
||||
hash = utils.hash(hash + JSON.stringify(row));
|
||||
let contentToHash = await sql.getValue(query);
|
||||
|
||||
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 +59,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,
|
||||
|
||||
@@ -94,6 +94,12 @@ async function isDbUpToDate() {
|
||||
}
|
||||
|
||||
async function isUserInitialized() {
|
||||
const optionsTable = await sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='options'");
|
||||
|
||||
if (optionsTable.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const username = await sql.getValue("SELECT value FROM options WHERE name = 'username'");
|
||||
|
||||
return !!username;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -127,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">
|
||||
@@ -68,18 +86,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tree" class="hide-toggle" style="overflow: auto; flex-grow: 100; flex-shrink: 100; margin-top: 10px;">
|
||||
</div>
|
||||
|
||||
<div id="parent-list">
|
||||
<p><strong>Note locations:</strong></p>
|
||||
|
||||
<ul id="parent-list-inner"></ul>
|
||||
</div>
|
||||
<div id="tree"></div>
|
||||
</div>
|
||||
|
||||
<div class="hide-toggle" style="grid-area: title;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<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>
|
||||
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul id="note-path-list" class="dropdown-menu" aria-labelledby="note-path-list-button">
|
||||
</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>
|
||||
|
||||
<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,7 +161,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="note-detail-wrapper">
|
||||
<div id="note-detail-wrapper">
|
||||
<div id="note-detail-component-wrapper">
|
||||
<div id="note-detail-text" class="note-detail-component" tabindex="2"></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>
|
||||
|
||||
3
src/www
3
src/www
@@ -18,6 +18,7 @@ const log = require('./services/log');
|
||||
const appInfo = require('./services/app_info');
|
||||
const messagingService = require('./services/messaging');
|
||||
const utils = require('./services/utils');
|
||||
const sqlInit = require('./services/sql_init.js');
|
||||
|
||||
const port = normalizePort(config['Network']['port'] || '3000');
|
||||
app.set('port', port);
|
||||
@@ -54,7 +55,7 @@ httpServer.listen(port);
|
||||
httpServer.on('error', onError);
|
||||
httpServer.on('listening', onListening);
|
||||
|
||||
messagingService.init(httpServer, sessionParser);
|
||||
sqlInit.dbReady.then(() => messagingService.init(httpServer, sessionParser));
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const electronRouting = require('./routes/electron');
|
||||
|
||||
Reference in New Issue
Block a user