Compare commits

...

20 Commits

Author SHA1 Message Date
azivner
31b76b23ce release 0.14.1 2018-06-02 09:39:37 -04:00
azivner
af529f82e5 fixed false sync error reporting 2018-06-02 09:39:04 -04:00
azivner
fc6669d254 initialization and schema fixes, closes #111 2018-06-01 22:26:37 -04:00
azivner
c07785be67 release 0.14.0 2018-05-31 23:23:44 -04:00
azivner
80d2457b23 moved parent list next to note title 2018-05-31 23:21:47 -04:00
azivner
5dde2752d2 add switch to manually enter/leave protected session, fixes #107 2018-05-31 20:00:39 -04:00
azivner
8bf4633cd0 fixes 2018-05-30 23:18:56 -04:00
azivner
bd66b8a1c8 fix issue with limitation of number of SQLite parameters (999) which caused problems when loading tree which was too expanded 2018-05-30 20:28:10 -04:00
azivner
be51e533fc OPML import support (issue #78) 2018-05-29 20:32:13 -04:00
azivner
f47ae12019 OPML export support (issue #78), import missing for now 2018-05-27 12:26:34 -04:00
azivner
cab54a458f unifying surrogate keys for event log and options, fixes #103 2018-05-26 23:25:09 -04:00
azivner
a30734f1bc Add history backwards/forwards buttons, fixes #94 2018-05-26 22:54:06 -04:00
azivner
7ad9f7b129 fixed layouting issues 2018-05-26 19:58:08 -04:00
azivner
40a32e6826 render notes can be edited and can contain HTML markup 2018-05-26 19:27:47 -04:00
azivner
ab0486aaf1 expose root node, fixes #101 2018-05-26 16:16:34 -04:00
azivner
874593a167 fix code editor growing 2018-05-26 15:28:36 -04:00
azivner
03bf33630e unify audit fields, fixes #102 2018-05-26 12:38:25 -04:00
azivner
933cce1b94 fix hideInAutocomplete bug 2018-05-26 10:50:13 -04:00
azivner
4a6ff573f8 fixed autocomplete issues with capitalization 2018-05-26 10:24:33 -04:00
azivner
1a737f7d19 expose add link on UI, fixes #95 2018-05-26 10:04:40 -04:00
51 changed files with 834 additions and 438 deletions

View File

@@ -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,546 +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>&quot;&quot;</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>&quot;&quot;</DefaultExpression>
</column>
<column id="36" parent="7" name="dateCreated">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;1970-01-01T00:00:00.000Z&apos;</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>&quot;&quot;</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>&apos;&apos;</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>&quot;&quot;</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>&quot;&quot;</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>&apos;&apos;</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>&apos;&apos;</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>&quot;&quot;</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>&quot;unnamed&quot;</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>&quot;&quot;</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>&apos;text&apos;</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>&apos;text/html&apos;</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>&quot;&quot;</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>&quot;&quot;</DefaultExpression>
</column>
<column id="116" parent="14" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;1970-01-01T00:00:00.000Z&apos;</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>&quot;&quot;</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>

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

View File

@@ -0,0 +1 @@
UPDATE notes SET mime = 'text/html' WHERE type = 'render';

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

View File

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

8
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.12.0",
"version": "0.13.0-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -13618,9 +13618,9 @@
},
"dependencies": {
"xmlbuilder": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
}
}
},

View File

@@ -1,7 +1,7 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.13.0-beta",
"version": "0.14.1",
"license": "AGPL-3.0-only",
"main": "electron.js",
"repository": {
@@ -59,7 +59,8 @@
"sqlite": "^2.9.2",
"tar-stream": "^1.6.1",
"unescape": "^1.0.1",
"ws": "^5.2.0"
"ws": "^5.2.0",
"xml2js": "^0.4.19"
},
"devDependencies": {
"electron": "^2.0.1",

View File

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

View File

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

View File

@@ -5,8 +5,8 @@ const dateUtils = require('../services/date_utils');
class Option extends Entity {
static get tableName() { return "options"; }
static get primaryKeyName() { return "name"; }
static get hashedProperties() { return ["name", "value"]; }
static get primaryKeyName() { return "optionId"; }
static get hashedProperties() { return ["optionId", "name", "value"]; }
beforeSaving() {
super.beforeSaving();

View File

@@ -1,11 +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", "dateAccessed", "isDeleted"]; }
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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

View 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

View File

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

View File

@@ -19,7 +19,7 @@ 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 = await linkService.createNoteLink(event.noteId).prop('outerHTML');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = await linkService.createNoteLink(notePath, title);
item.addClass("current");
}
$parentListList.append($("<li/>").append(item));
$notePathList.append(item);
}
}
}
@@ -294,6 +297,7 @@ function initFancyTree(tree) {
extensions: ["hotkeys", "filter", "dnd", "clones"],
source: tree,
scrollParent: $tree,
minExpandLevel: 2, // root can't be collapsed
click: (event, data) => {
const targetType = data.targetType;
const node = data.node;

View File

@@ -10,7 +10,7 @@ async function prepareTree(noteRows, branchRows, relations) {
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,30 +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
};
if (note.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);
}
@@ -90,6 +96,10 @@ async function getExtraClasses(note) {
const extraClasses = [];
if (note.noteId === 'root') {
extraClasses.push("tree-root");
}
if (note.isProtected) {
extraClasses.push("protected");
}

View File

@@ -58,6 +58,10 @@ class TreeCache {
/** @return NoteShort */
async getNote(noteId) {
if (noteId === 'none') {
return null;
}
return (await this.getNotes([noteId]))[0];
}
@@ -68,6 +72,10 @@ class TreeCache {
}
addBranchRelationship(branchId, childNoteId, parentNoteId) {
if (parentNoteId === 'none') { // applies only to root element
return;
}
this.childParentToBranch[childNoteId + '-' + parentNoteId] = branchId;
this.parents[childNoteId] = this.parents[childNoteId] || [];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -105,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;
@@ -166,20 +175,6 @@ div.ui-tooltip {
margin-top: 10px;
}
#parent-list {
display: none;
margin-left: 20px;
border-top: 2px solid #eee;
padding-top: 10px;
grid-area: parent-list;
max-height: 300px;
overflow: auto;
}
#parent-list ul {
padding-left: 20px;
}
/*
* .electron-in-page-search-window is a class specified to default
* <webview> element for search window.
@@ -240,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;
}
@@ -268,10 +263,20 @@ div.ui-tooltip {
#note-detail-code {
min-height: 200px;
overflow: auto;
}
#note-detail-render {
min-height: 200px;
}
.CodeMirror {
font-family: "Liberation Mono", "Lucida Console", monospace;
height: auto;
}
.CodeMirror-scroll {
min-height: 200px;
}
#note-id-display {
@@ -347,4 +352,21 @@ div.ui-tooltip {
#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;
}

View File

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

View File

@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
function prepareText(text) {
const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, '\n')
.replace(/&nbsp;/g, ' '); // nbsp isn't in XML standard (only HTML)
const stripped = utils.stripTags(newLines);
const escaped = escapeXmlAttribute(stripped);
return escaped.replace(/\n/g, '&#10;');
}
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 = [];

View File

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

View File

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

View File

@@ -5,11 +5,9 @@ const optionService = require('../../services/options');
const protectedSessionService = require('../../services/protected_session');
async function getNotes(noteIds) {
const questionMarks = noteIds.map(() => "?").join(",");
const notes = await sql.getRows(`
const notes = await sql.getManyRows(`
SELECT noteId, title, isProtected, type, mime
FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
FROM notes WHERE isDeleted = 0 AND noteId IN (???)`, noteIds);
protectedSessionService.decryptNotes(notes);
@@ -18,11 +16,11 @@ async function getNotes(noteIds) {
}
async function getRelations(noteIds) {
const questionMarks = noteIds.map(() => "?").join(",");
const doubledNoteIds = noteIds.concat(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.getRows(`SELECT branchId, noteId AS 'childNoteId', parentNoteId FROM branches WHERE isDeleted = 0
AND (parentNoteId IN (${questionMarks}) OR noteId IN (${questionMarks}))`, doubledNoteIds);
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() {
@@ -58,12 +56,11 @@ async function load(req) {
const branchIds = req.body.branchIds;
if (branchIds && branchIds.length > 0) {
noteIds = await sql.getColumn(`SELECT noteId FROM branches WHERE isDeleted = 0 AND branchId IN(${branchIds.map(() => "?").join(",")})`, branchIds);
noteIds = (await sql.getManyRows(`SELECT noteId FROM branches WHERE isDeleted = 0 AND branchId IN(???)`, branchIds))
.map(note => note.noteId);
}
const questionMarks = noteIds.map(() => "?").join(",");
const branches = await sql.getRows(`SELECT * FROM branches WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
const branches = await sql.getManyRows(`SELECT * FROM branches WHERE isDeleted = 0 AND noteId IN (???)`, noteIds);
const notes = await getNotes(noteIds);

View File

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

View File

@@ -3,7 +3,7 @@
const build = require('./build');
const packageJson = require('../../package');
const APP_DB_VERSION = 93;
const APP_DB_VERSION = 96;
module.exports = {
appVersion: packageJson.version,

View File

@@ -3,6 +3,7 @@ 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;
@@ -14,10 +15,10 @@ const hideInAutocomplete = {};
let prefixes = {};
async function load() {
noteTitles = await sql.getMap(`SELECT noteId, LOWER(title) FROM notes WHERE isDeleted = 0 AND isProtected = 0`);
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, LOWER(prefix) FROM branches WHERE prefix IS NOT NULL AND prefix != ''`);
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`);
@@ -58,7 +59,11 @@ function getResults(query) {
}
for (const parentNoteId of parents) {
const title = getNoteTitle(noteId, parentNoteId);
if (hideInAutocomplete[parentNoteId]) {
continue;
}
const title = getNoteTitle(noteId, parentNoteId).toLowerCase();
const foundTokens = [];
for (const token of tokens) {
@@ -109,6 +114,7 @@ function search(noteId, tokens, path, results) {
if (parentNoteId === 'root' || hideInAutocomplete[parentNoteId]) {
continue;
}
const title = getNoteTitle(noteId, parentNoteId);
const foundTokens = [];
@@ -241,7 +247,7 @@ eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
}
});
sqlInit.dbReady.then(load);
sqlInit.dbReady.then(() => utils.stopWatch("Autocomplete load", load));
module.exports = {
getResults

View File

@@ -1 +1 @@
module.exports = { buildDate:"2018-05-22T23:51:43-04:00", buildRevision: "a372cbb2dfa918084e2d447a01fca6f076ddf486" };
module.exports = { buildDate:"2018-06-02T09:39:37-04:00", buildRevision: "af529f82e5080f01b26ac7db104a8041f137dc48" };

View File

@@ -16,8 +16,11 @@ const RecentNote = require('../entities/recent_note');
const Option = require('../entities/option');
async function getHash(entityConstructor, whereBranch) {
let contentToHash = await sql.getValue(`SELECT GROUP_CONCAT(hash) FROM ${entityConstructor.tableName} `
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName}`);
// 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})`;
let contentToHash = await sql.getValue(query);
if (!contentToHash) { // might be null in case of no rows
contentToHash = "";
@@ -56,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

View File

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

View File

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

View File

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

View File

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

View File

@@ -207,13 +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",
"options": "name"
"options": "optionId"
};
async function getEntityRow(entityName, entityId) {

View File

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

View File

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

View File

@@ -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>
&nbsp; &nbsp;
<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">
@@ -69,16 +87,24 @@
</div>
<div id="tree"></div>
<div id="parent-list">
<p><strong>Note locations:</strong></p>
<ul id="parent-list-inner"></ul>
</div>
</div>
<div style="grid-area: title;">
<div class="hide-toggle" style="display: flex; align-items: center;">
<div class="dropdown">
<button id="note-path-list-button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
<span id="note-path-count">1 path</span>
<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"
@@ -89,11 +115,15 @@
id="unprotect-button"
style="display: none; background: url('images/icons/unlock.png')"></a>
&nbsp;
&nbsp; &nbsp;
<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"
@@ -104,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">&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">&check;</span> <strong>Render HTML note</strong></li>
@@ -121,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>
@@ -220,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>

View File

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