Compare commits

...

36 Commits

Author SHA1 Message Date
azivner
c8b9c7d936 release 0.12.0 2018-04-14 08:28:50 -04:00
azivner
d57057ba28 fix note ordering sync 2018-04-14 08:23:06 -04:00
azivner
66cee8daa4 restructuring CSS grid/flex which fixes jumpy scrolling in tree 2018-04-13 19:58:33 -04:00
azivner
afd7df0942 fix collapse tree button 2018-04-13 19:22:12 -04:00
azivner
bd6ae33d32 fancytree upgrade to 2.28.1 2018-04-12 20:42:12 -04:00
azivner
70660a0d68 Merge branch 'stable' 2018-04-12 20:04:01 -04:00
azivner
cdad18551a upgrade CKEditor to 1.0 beta.2, fixes #93 2018-04-12 20:03:23 -04:00
azivner
592c51d1a5 fix note reordering sync again 2018-04-12 18:31:29 -04:00
azivner
6a57b8a7e7 fix ordering sync 2018-04-12 18:13:48 -04:00
azivner
7a94e21c54 tabindex 2 for text and code editor so that tabbing from title leads to editor focus 2018-04-11 22:44:33 -04:00
azivner
5b43f321e2 release 0.11.1 2018-04-11 00:10:33 -04:00
azivner
a4eafb934f non null note title and content in the DB schema, allow saving non-valid JSON notes, children overview style changes 2018-04-11 00:10:11 -04:00
azivner
7b59a665dd hideChildrenOverview label which can disable children overview for specific notes 2018-04-10 23:15:41 -04:00
azivner
3d15450ffc children overview styling 2018-04-10 21:08:00 -04:00
azivner
b0c6d52461 can't rollback transaction multiple times 2018-04-10 20:28:02 -04:00
azivner
2dc16dd29f release 0.11.0-beta 2018-04-09 22:38:37 -04:00
azivner
d8924c536b Merge branch 'master' into stable 2018-04-09 22:30:50 -04:00
azivner
3ebbf2cc46 fix generating build.js 2018-04-09 22:30:11 -04:00
azivner
f4079604c9 basic implementation of children overview, closes #80 2018-04-08 22:38:52 -04:00
azivner
1f96a6beab export & import work correctly with clones 2018-04-08 13:14:30 -04:00
azivner
b277a250e5 protected notes are not in autocomplete when not in protected session, fixes #46 2018-04-08 12:27:10 -04:00
azivner
5b0e1a644d codemirror now doesn't hijack alt-left/right, fixes #86 2018-04-08 12:17:42 -04:00
azivner
6bb3cfa9a3 note revisions for code is now properly formatted, fixes #97 2018-04-08 12:13:52 -04:00
azivner
9720868f5a added type and mime to note revisions 2018-04-08 11:57:14 -04:00
azivner
8d8ee2a87a small sync refactorings 2018-04-08 10:09:33 -04:00
azivner
542e82ee5d upgraded uncompressed jquery 2018-04-08 09:40:28 -04:00
azivner
0104b19502 naming standards 2018-04-08 09:25:35 -04:00
azivner
120888b53e fix JSON saving bug 2018-04-08 08:31:19 -04:00
azivner
d2e2caed62 refactoring of note saving code & API 2018-04-08 08:21:49 -04:00
azivner
63066802a8 fix showMessage, showError
(cherry picked from commit 6128bb4)
2018-04-08 07:49:21 -04:00
azivner
6128bb4ff3 fix showMessage, showError 2018-04-08 07:48:47 -04:00
azivner
982796255d sync content check refactoring 2018-04-07 22:59:47 -04:00
azivner
36b15f474d sync cleanup 2018-04-07 22:32:46 -04:00
azivner
13f71f8967 bulk push sync 2018-04-07 22:25:28 -04:00
azivner
64336ffbee implemented bulk sync pull for increased performance 2018-04-07 21:53:42 -04:00
azivner
b09463d1b2 async logging of info messages 2018-04-07 21:30:01 -04:00
45 changed files with 1278 additions and 1096 deletions

View File

@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<dataSource name="document.db"> <dataSource name="document.db">
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.7"> <database-model serializer="dbm" rdbms="SQLITE" format-version="4.8">
<root id="1"/> <root id="1">
<ServerVersion>3.16.1</ServerVersion>
</root>
<schema id="2" parent="1" name="main"> <schema id="2" parent="1" name="main">
<Current>1</Current> <Current>1</Current>
<Visible>1</Visible> <Visible>1</Visible>
@@ -107,8 +109,7 @@
<index id="35" parent="7" name="IDX_branches_noteId_parentNoteId"> <index id="35" parent="7" name="IDX_branches_noteId_parentNoteId">
<ColNames>noteId <ColNames>noteId
parentNoteId</ColNames> parentNoteId</ColNames>
<ColumnCollations> <ColumnCollations></ColumnCollations>
</ColumnCollations>
</index> </index>
<index id="36" parent="7" name="IDX_branches_noteId"> <index id="36" parent="7" name="IDX_branches_noteId">
<ColNames>noteId</ColNames> <ColNames>noteId</ColNames>
@@ -142,445 +143,449 @@ parentNoteId</ColNames>
<ColNames>id</ColNames> <ColNames>id</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
</key> </key>
<foreign-key id="43" parent="8"> <column id="43" parent="9" name="imageId">
<ColNames>noteId</ColNames>
<RefTableName>notes</RefTableName>
<RefColNames>noteId</RefColNames>
</foreign-key>
<column id="44" parent="9" name="imageId">
<Position>1</Position> <Position>1</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="45" parent="9" name="format"> <column id="44" parent="9" name="format">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="46" parent="9" name="checksum"> <column id="45" parent="9" name="checksum">
<Position>3</Position> <Position>3</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="47" parent="9" name="name"> <column id="46" parent="9" name="name">
<Position>4</Position> <Position>4</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="48" parent="9" name="data"> <column id="47" parent="9" name="data">
<Position>5</Position> <Position>5</Position>
<DataType>BLOB|0s</DataType> <DataType>BLOB|0s</DataType>
</column> </column>
<column id="49" parent="9" name="isDeleted"> <column id="48" parent="9" name="isDeleted">
<Position>6</Position> <Position>6</Position>
<DataType>INT|0s</DataType> <DataType>INT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression> <DefaultExpression>0</DefaultExpression>
</column> </column>
<column id="50" parent="9" name="dateModified"> <column id="49" parent="9" name="dateModified">
<Position>7</Position> <Position>7</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="51" parent="9" name="dateCreated"> <column id="50" parent="9" name="dateCreated">
<Position>8</Position> <Position>8</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<index id="52" parent="9" name="sqlite_autoindex_images_1"> <index id="51" parent="9" name="sqlite_autoindex_images_1">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ColNames>imageId</ColNames> <ColNames>imageId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<key id="53" parent="9"> <key id="52" parent="9">
<ColNames>imageId</ColNames> <ColNames>imageId</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
</key> </key>
<column id="54" parent="10" name="labelId"> <column id="53" parent="10" name="labelId">
<Position>1</Position> <Position>1</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="55" parent="10" name="noteId"> <column id="54" parent="10" name="noteId">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="56" parent="10" name="name"> <column id="55" parent="10" name="name">
<Position>3</Position> <Position>3</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="57" parent="10" name="value"> <column id="56" parent="10" name="value">
<Position>4</Position> <Position>4</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression> <DefaultExpression>&apos;&apos;</DefaultExpression>
</column> </column>
<column id="58" parent="10" name="position"> <column id="57" parent="10" name="position">
<Position>5</Position> <Position>5</Position>
<DataType>INT|0s</DataType> <DataType>INT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression> <DefaultExpression>0</DefaultExpression>
</column> </column>
<column id="59" parent="10" name="dateCreated"> <column id="58" parent="10" name="dateCreated">
<Position>6</Position> <Position>6</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="60" parent="10" name="dateModified"> <column id="59" parent="10" name="dateModified">
<Position>7</Position> <Position>7</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="61" parent="10" name="isDeleted"> <column id="60" parent="10" name="isDeleted">
<Position>8</Position> <Position>8</Position>
<DataType>INT|0s</DataType> <DataType>INT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<index id="62" parent="10" name="sqlite_autoindex_labels_1"> <index id="61" parent="10" name="sqlite_autoindex_labels_1">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ColNames>labelId</ColNames> <ColNames>labelId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<index id="63" parent="10" name="IDX_labels_noteId"> <index id="62" parent="10" name="IDX_labels_noteId">
<ColNames>noteId</ColNames> <ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
</index> </index>
<index id="64" parent="10" name="IDX_labels_name_value"> <index id="63" parent="10" name="IDX_labels_name_value">
<ColNames>name <ColNames>name
value</ColNames> value</ColNames>
<ColumnCollations> <ColumnCollations></ColumnCollations>
</ColumnCollations>
</index> </index>
<key id="65" parent="10"> <key id="64" parent="10">
<ColNames>labelId</ColNames> <ColNames>labelId</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
</key> </key>
<column id="66" parent="11" name="noteImageId"> <column id="65" parent="11" name="noteImageId">
<Position>1</Position> <Position>1</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="67" parent="11" name="noteId"> <column id="66" parent="11" name="noteId">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="68" parent="11" name="imageId"> <column id="67" parent="11" name="imageId">
<Position>3</Position> <Position>3</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="69" parent="11" name="isDeleted"> <column id="68" parent="11" name="isDeleted">
<Position>4</Position> <Position>4</Position>
<DataType>INT|0s</DataType> <DataType>INT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression> <DefaultExpression>0</DefaultExpression>
</column> </column>
<column id="70" parent="11" name="dateModified"> <column id="69" parent="11" name="dateModified">
<Position>5</Position> <Position>5</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="71" parent="11" name="dateCreated"> <column id="70" parent="11" name="dateCreated">
<Position>6</Position> <Position>6</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<index id="72" parent="11" name="sqlite_autoindex_note_images_1"> <index id="71" parent="11" name="sqlite_autoindex_note_images_1">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ColNames>noteImageId</ColNames> <ColNames>noteImageId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<index id="73" parent="11" name="IDX_note_images_noteId_imageId"> <index id="72" parent="11" name="IDX_note_images_noteId_imageId">
<ColNames>noteId <ColNames>noteId
imageId</ColNames> imageId</ColNames>
<ColumnCollations> <ColumnCollations></ColumnCollations>
</ColumnCollations>
</index> </index>
<index id="74" parent="11" name="IDX_note_images_noteId"> <index id="73" parent="11" name="IDX_note_images_noteId">
<ColNames>noteId</ColNames> <ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
</index> </index>
<index id="75" parent="11" name="IDX_note_images_imageId"> <index id="74" parent="11" name="IDX_note_images_imageId">
<ColNames>imageId</ColNames> <ColNames>imageId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
</index> </index>
<key id="76" parent="11"> <key id="75" parent="11">
<ColNames>noteImageId</ColNames> <ColNames>noteImageId</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
</key> </key>
<column id="77" parent="12" name="noteRevisionId"> <column id="76" parent="12" name="noteRevisionId">
<Position>1</Position> <Position>1</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="78" parent="12" name="noteId"> <column id="77" parent="12" name="noteId">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="79" parent="12" name="title"> <column id="78" parent="12" name="title">
<Position>3</Position> <Position>3</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
</column> </column>
<column id="80" parent="12" name="content"> <column id="79" parent="12" name="content">
<Position>4</Position> <Position>4</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
</column> </column>
<column id="81" parent="12" name="isProtected"> <column id="80" parent="12" name="isProtected">
<Position>5</Position> <Position>5</Position>
<DataType>INT|0s</DataType> <DataType>INT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression> <DefaultExpression>0</DefaultExpression>
</column> </column>
<column id="82" parent="12" name="dateModifiedFrom"> <column id="81" parent="12" name="dateModifiedFrom">
<Position>6</Position> <Position>6</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="83" parent="12" name="dateModifiedTo"> <column id="82" parent="12" name="dateModifiedTo">
<Position>7</Position> <Position>7</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<index id="84" parent="12" name="sqlite_autoindex_note_revisions_1"> <column id="83" 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">
<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">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames> <ColNames>noteRevisionId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<index id="85" parent="12" name="IDX_note_revisions_noteId"> <index id="86" parent="12" name="IDX_note_revisions_noteId">
<ColNames>noteId</ColNames> <ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
</index> </index>
<index id="86" parent="12" name="IDX_note_revisions_dateModifiedFrom"> <index id="87" parent="12" name="IDX_note_revisions_dateModifiedFrom">
<ColNames>dateModifiedFrom</ColNames> <ColNames>dateModifiedFrom</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
</index> </index>
<index id="87" parent="12" name="IDX_note_revisions_dateModifiedTo"> <index id="88" parent="12" name="IDX_note_revisions_dateModifiedTo">
<ColNames>dateModifiedTo</ColNames> <ColNames>dateModifiedTo</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
</index> </index>
<key id="88" parent="12"> <key id="89" parent="12">
<ColNames>noteRevisionId</ColNames> <ColNames>noteRevisionId</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
</key> </key>
<column id="89" parent="13" name="noteId"> <column id="90" parent="13" name="noteId">
<Position>1</Position> <Position>1</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="90" parent="13" name="title"> <column id="91" parent="13" name="title">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
</column> </column>
<column id="91" parent="13" name="content"> <column id="92" parent="13" name="content">
<Position>3</Position> <Position>3</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
</column> </column>
<column id="92" parent="13" name="isProtected"> <column id="93" parent="13" name="isProtected">
<Position>4</Position> <Position>4</Position>
<DataType>INT|0s</DataType> <DataType>INT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression> <DefaultExpression>0</DefaultExpression>
</column> </column>
<column id="93" parent="13" name="isDeleted"> <column id="94" parent="13" name="isDeleted">
<Position>5</Position> <Position>5</Position>
<DataType>INT|0s</DataType> <DataType>INT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression> <DefaultExpression>0</DefaultExpression>
</column> </column>
<column id="94" parent="13" name="dateCreated"> <column id="95" parent="13" name="dateCreated">
<Position>6</Position> <Position>6</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="95" parent="13" name="dateModified"> <column id="96" parent="13" name="dateModified">
<Position>7</Position> <Position>7</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="96" parent="13" name="type"> <column id="97" parent="13" name="type">
<Position>8</Position> <Position>8</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<DefaultExpression>&apos;text&apos;</DefaultExpression> <DefaultExpression>&apos;text&apos;</DefaultExpression>
</column> </column>
<column id="97" parent="13" name="mime"> <column id="98" parent="13" name="mime">
<Position>9</Position> <Position>9</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<DefaultExpression>&apos;text/html&apos;</DefaultExpression> <DefaultExpression>&apos;text/html&apos;</DefaultExpression>
</column> </column>
<index id="98" parent="13" name="sqlite_autoindex_notes_1"> <index id="99" parent="13" name="sqlite_autoindex_notes_1">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames> <ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<index id="99" parent="13" name="IDX_notes_isDeleted"> <index id="100" parent="13" name="IDX_notes_isDeleted">
<ColNames>isDeleted</ColNames> <ColNames>isDeleted</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
</index> </index>
<key id="100" parent="13"> <key id="101" parent="13">
<ColNames>noteId</ColNames> <ColNames>noteId</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
</key> </key>
<column id="101" parent="14" name="name"> <column id="102" parent="14" name="name">
<Position>1</Position> <Position>1</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="102" parent="14" name="value"> <column id="103" parent="14" name="value">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
</column> </column>
<column id="103" parent="14" name="dateModified"> <column id="104" parent="14" name="dateModified">
<Position>3</Position> <Position>3</Position>
<DataType>INT|0s</DataType> <DataType>INT|0s</DataType>
</column> </column>
<column id="104" parent="14" name="isSynced"> <column id="105" parent="14" name="isSynced">
<Position>4</Position> <Position>4</Position>
<DataType>INTEGER|0s</DataType> <DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression> <DefaultExpression>0</DefaultExpression>
</column> </column>
<index id="105" parent="14" name="sqlite_autoindex_options_1"> <index id="106" parent="14" name="sqlite_autoindex_options_1">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ColNames>name</ColNames> <ColNames>name</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<key id="106" parent="14"> <key id="107" parent="14">
<ColNames>name</ColNames> <ColNames>name</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
</key> </key>
<column id="107" parent="15" name="branchId"> <column id="108" parent="15" name="branchId">
<Position>1</Position> <Position>1</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="108" parent="15" name="notePath"> <column id="109" parent="15" name="notePath">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="109" parent="15" name="dateAccessed"> <column id="110" parent="15" name="dateAccessed">
<Position>3</Position> <Position>3</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="110" parent="15" name="isDeleted"> <column id="111" parent="15" name="isDeleted">
<Position>4</Position> <Position>4</Position>
<DataType>INT|0s</DataType> <DataType>INT|0s</DataType>
</column> </column>
<index id="111" parent="15" name="sqlite_autoindex_recent_notes_1"> <index id="112" parent="15" name="sqlite_autoindex_recent_notes_1">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames> <ColNames>branchId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<key id="112" parent="15"> <key id="113" parent="15">
<ColNames>branchId</ColNames> <ColNames>branchId</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
</key> </key>
<column id="113" parent="16" name="sourceId"> <column id="114" parent="16" name="sourceId">
<Position>1</Position> <Position>1</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="114" parent="16" name="dateCreated"> <column id="115" parent="16" name="dateCreated">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<index id="115" parent="16" name="sqlite_autoindex_source_ids_1"> <index id="116" parent="16" name="sqlite_autoindex_source_ids_1">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ColNames>sourceId</ColNames> <ColNames>sourceId</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<key id="116" parent="16"> <key id="117" parent="16">
<ColNames>sourceId</ColNames> <ColNames>sourceId</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName> <UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
</key> </key>
<column id="117" parent="17" name="type"> <column id="118" parent="17" name="type">
<Position>1</Position> <Position>1</Position>
<DataType>text|0s</DataType> <DataType>text|0s</DataType>
</column> </column>
<column id="118" parent="17" name="name"> <column id="119" parent="17" name="name">
<Position>2</Position> <Position>2</Position>
<DataType>text|0s</DataType> <DataType>text|0s</DataType>
</column> </column>
<column id="119" parent="17" name="tbl_name"> <column id="120" parent="17" name="tbl_name">
<Position>3</Position> <Position>3</Position>
<DataType>text|0s</DataType> <DataType>text|0s</DataType>
</column> </column>
<column id="120" parent="17" name="rootpage"> <column id="121" parent="17" name="rootpage">
<Position>4</Position> <Position>4</Position>
<DataType>integer|0s</DataType> <DataType>integer|0s</DataType>
</column> </column>
<column id="121" parent="17" name="sql"> <column id="122" parent="17" name="sql">
<Position>5</Position> <Position>5</Position>
<DataType>text|0s</DataType> <DataType>text|0s</DataType>
</column> </column>
<column id="122" parent="18" name="name"> <column id="123" parent="18" name="name">
<Position>1</Position> <Position>1</Position>
</column> </column>
<column id="123" parent="18" name="seq"> <column id="124" parent="18" name="seq">
<Position>2</Position> <Position>2</Position>
</column> </column>
<column id="124" parent="19" name="id"> <column id="125" parent="19" name="id">
<Position>1</Position> <Position>1</Position>
<DataType>INTEGER|0s</DataType> <DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity> <SequenceIdentity>1</SequenceIdentity>
</column> </column>
<column id="125" parent="19" name="entityName"> <column id="126" parent="19" name="entityName">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="126" parent="19" name="entityId"> <column id="127" parent="19" name="entityId">
<Position>3</Position> <Position>3</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="127" parent="19" name="sourceId"> <column id="128" parent="19" name="sourceId">
<Position>4</Position> <Position>4</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<column id="128" parent="19" name="syncDate"> <column id="129" parent="19" name="syncDate">
<Position>5</Position> <Position>5</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull> <NotNull>1</NotNull>
</column> </column>
<index id="129" parent="19" name="IDX_sync_entityName_entityId"> <index id="130" parent="19" name="IDX_sync_entityName_entityId">
<ColNames>entityName <ColNames>entityName
entityId</ColNames> entityId</ColNames>
<ColumnCollations> <ColumnCollations></ColumnCollations>
</ColumnCollations>
<Unique>1</Unique> <Unique>1</Unique>
</index> </index>
<index id="130" parent="19" name="IDX_sync_syncDate"> <index id="131" parent="19" name="IDX_sync_syncDate">
<ColNames>syncDate</ColNames> <ColNames>syncDate</ColNames>
<ColumnCollations></ColumnCollations> <ColumnCollations></ColumnCollations>
</index> </index>
<key id="131" parent="19"> <key id="132" parent="19">
<ColNames>id</ColNames> <ColNames>id</ColNames>
<Primary>1</Primary> <Primary>1</Primary>
</key> </key>

View File

@@ -24,9 +24,9 @@ jq '.version = "'$VERSION'"' package.json|sponge package.json
git add package.json git add package.json
echo 'module.exports = { buildDate:"'`date --iso-8601=seconds`'", buildRevision: "'`git log -1 --format="%H"`'" };' > services/build.js echo 'module.exports = { buildDate:"'`date --iso-8601=seconds`'", buildRevision: "'`git log -1 --format="%H"`'" };' > src/services/build.js
git add services/build.js git add src/services/build.js
TAG=v$VERSION TAG=v$VERSION

View File

@@ -0,0 +1,5 @@
ALTER TABLE note_revisions ADD type TEXT DEFAULT '' NOT NULL;
ALTER TABLE note_revisions ADD mime TEXT DEFAULT '' NOT NULL;
UPDATE note_revisions SET type = (SELECT type FROM notes WHERE notes.noteId = note_revisions.noteId);
UPDATE note_revisions SET mime = (SELECT mime FROM notes WHERE notes.noteId = note_revisions.noteId);

View File

@@ -0,0 +1,34 @@
CREATE TABLE event_logc027
(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
noteId TEXT,
comment TEXT,
dateAdded TEXT NOT NULL
);
INSERT INTO event_logc027(id, noteId, comment, dateAdded) SELECT id, noteId, comment, dateAdded FROM event_log;
DROP TABLE event_log;
ALTER TABLE event_logc027 RENAME TO event_log;
CREATE TABLE IF NOT EXISTS "notes_mig" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "unnamed",
`content` TEXT NOT NULL DEFAULT "",
`isProtected` INT NOT NULL DEFAULT 0,
`isDeleted` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
PRIMARY KEY(`noteId`)
);
INSERT INTO notes_mig (noteId, title, content, isProtected, isDeleted, dateCreated, dateModified, type, mime)
SELECT noteId, title, content, isProtected, isDeleted, dateCreated, dateModified, type, mime FROM notes;
DROP TABLE notes;
ALTER TABLE notes_mig RENAME TO notes;
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
);

View File

@@ -21,28 +21,6 @@ CREATE TABLE IF NOT EXISTS "source_ids" (
`dateCreated` TEXT NOT NULL, `dateCreated` TEXT NOT NULL,
PRIMARY KEY(`sourceId`) PRIMARY KEY(`sourceId`)
); );
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT,
`content` TEXT,
`isProtected` INT NOT NULL DEFAULT 0,
`isDeleted` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
PRIMARY KEY(`noteId`)
);
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
);
CREATE TABLE IF NOT EXISTS "event_log" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`noteId` TEXT,
`comment` TEXT,
`dateAdded` TEXT NOT NULL,
FOREIGN KEY(noteId) REFERENCES notes(noteId)
);
CREATE TABLE IF NOT EXISTS "note_revisions" ( CREATE TABLE IF NOT EXISTS "note_revisions" (
`noteRevisionId` TEXT NOT NULL PRIMARY KEY, `noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL, `noteId` TEXT NOT NULL,
@@ -51,7 +29,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (
`isProtected` INT NOT NULL DEFAULT 0, `isProtected` INT NOT NULL DEFAULT 0,
`dateModifiedFrom` TEXT NOT NULL, `dateModifiedFrom` TEXT NOT NULL,
`dateModifiedTo` TEXT NOT NULL `dateModifiedTo` TEXT NOT NULL
); , type TEXT DEFAULT '' NOT NULL, mime TEXT DEFAULT '' NOT NULL);
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` ( CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
`noteId` `noteId`
); );
@@ -130,3 +108,25 @@ CREATE INDEX IDX_labels_name_value
on labels (name, value); on labels (name, value);
CREATE INDEX IDX_labels_noteId CREATE INDEX IDX_labels_noteId
on 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",
`content` TEXT NOT NULL DEFAULT "",
`isProtected` INT NOT NULL DEFAULT 0,
`isDeleted` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
PRIMARY KEY(`noteId`)
);
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
);

View File

@@ -1,7 +1,7 @@
{ {
"name": "trilium", "name": "trilium",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.10.2-beta", "version": "0.12.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"repository": { "repository": {

View File

@@ -12,13 +12,21 @@ class Note extends Entity {
constructor(row) { constructor(row) {
super(row); super(row);
if (this.isProtected) { // 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); protected_session.decryptNote(this);
} }
if (this.isJson()) { this.setContent(this.content);
}
setContent(content) {
this.content = content;
try {
this.jsonContent = JSON.parse(this.content); this.jsonContent = JSON.parse(this.content);
} }
catch(e) {}
} }
isJson() { isJson() {
@@ -133,7 +141,7 @@ class Note extends Entity {
beforeSaving() { beforeSaving() {
super.beforeSaving(); super.beforeSaving();
if (this.isJson()) { if (this.isJson() && this.jsonContent) {
this.content = JSON.stringify(this.jsonContent, null, '\t'); this.content = JSON.stringify(this.jsonContent, null, '\t');
} }

View File

@@ -45,8 +45,8 @@ async function showDialog() {
$clonePrefix.val(''); $clonePrefix.val('');
$linkTitle.val(''); $linkTitle.val('');
function setDefaultLinkTitle(noteId) { async function setDefaultLinkTitle(noteId) {
const noteTitle = treeUtils.getNoteTitle(noteId); const noteTitle = await treeUtils.getNoteTitle(noteId);
$linkTitle.val(noteTitle); $linkTitle.val(noteTitle);
} }
@@ -54,7 +54,7 @@ async function showDialog() {
$autoComplete.autocomplete({ $autoComplete.autocomplete({
source: await autocompleteService.getAutocompleteItems(), source: await autocompleteService.getAutocompleteItems(),
minLength: 0, minLength: 0,
change: () => { change: async () => {
const val = $autoComplete.val(); const val = $autoComplete.val();
const notePath = linkService.getNodePathFromLabel(val); const notePath = linkService.getNodePathFromLabel(val);
if (!notePath) { if (!notePath) {
@@ -64,16 +64,16 @@ async function showDialog() {
const noteId = treeUtils.getNoteIdFromNotePath(notePath); const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (noteId) { if (noteId) {
setDefaultLinkTitle(noteId); await setDefaultLinkTitle(noteId);
} }
}, },
// this is called when user goes through autocomplete list with keyboard // this is called when user goes through autocomplete list with keyboard
// at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is // at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
focus: (event, ui) => { focus: async (event, ui) => {
const notePath = linkService.getNodePathFromLabel(ui.item.value); const notePath = linkService.getNodePathFromLabel(ui.item.value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath); const noteId = treeUtils.getNoteIdFromNotePath(notePath);
setDefaultLinkTitle(noteId); await setDefaultLinkTitle(noteId);
} }
}); });
} }

View File

@@ -54,7 +54,13 @@ $list.on('change', () => {
const revisionItem = revisionItems.find(r => r.noteRevisionId === optVal); const revisionItem = revisionItems.find(r => r.noteRevisionId === optVal);
$title.html(revisionItem.title); $title.html(revisionItem.title);
$content.html(revisionItem.content);
if (revisionItem.type === 'text') {
$content.html(revisionItem.content);
}
else if (revisionItem.type === 'code') {
$content.html($("<pre>").text(revisionItem.content));
}
}); });
$(document).on('click', "a[action='note-revision']", event => { $(document).on('click', "a[action='note-revision']", event => {

View File

@@ -14,6 +14,10 @@ class Branch {
return await this.treeCache.getNote(this.noteId); return await this.treeCache.getNote(this.noteId);
} }
isTopLevel() {
return this.parentNoteId === 'root';
}
get toString() { get toString() {
return `Branch(branchId=${this.branchId})`; return `Branch(branchId=${this.branchId})`;
} }

View File

@@ -6,8 +6,11 @@ class NoteFull extends NoteShort {
this.content = row.content; this.content = row.content;
if (this.isJson()) { if (this.content !== "" && this.isJson()) {
this.jsonContent = JSON.parse(this.content); try {
this.jsonContent = JSON.parse(this.content);
}
catch(e) {}
} }
} }
} }

View File

@@ -24,6 +24,10 @@ class NoteShort {
} }
async getChildBranches() { async getChildBranches() {
if (!this.treeCache.children[this.noteId]) {
return [];
}
const branches = []; const branches = [];
for (const child of this.treeCache.children[this.noteId]) { for (const child of this.treeCache.children[this.noteId]) {
@@ -44,6 +48,14 @@ class NoteShort {
get toString() { get toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`; return `Note(noteId=${this.noteId}, title=${this.title})`;
} }
get dto() {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.hideInAutocomplete;
return dto;
}
} }
export default NoteShort; export default NoteShort;

View File

@@ -1,5 +1,6 @@
import treeCache from "./tree_cache.js"; import treeCache from "./tree_cache.js";
import treeUtils from "./tree_utils.js"; import treeUtils from "./tree_utils.js";
import protectedSessionHolder from './protected_session_holder.js';
async function getAutocompleteItems(parentNoteId, notePath, titlePath) { async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
if (!parentNoteId) { if (!parentNoteId) {
@@ -21,9 +22,6 @@ async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
titlePath = ''; titlePath = '';
} }
// https://github.com/zadam/trilium/issues/46
// unfortunately not easy to implement because we don't have an easy access to note's isProtected property
const autocompleteItems = []; const autocompleteItems = [];
for (const childNote of childNotes) { for (const childNote of childNotes) {
@@ -34,10 +32,12 @@ async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId; const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId;
const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId); const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId);
autocompleteItems.push({ if (!childNote.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
value: childTitlePath + ' (' + childNotePath + ')', autocompleteItems.push({
label: childTitlePath value: childTitlePath + ' (' + childNotePath + ')',
}); label: childTitlePath
});
}
const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath); const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath);

View File

@@ -76,9 +76,11 @@ function goToLink(e) {
function addLinkToEditor(linkTitle, linkHref) { function addLinkToEditor(linkTitle, linkHref) {
const editor = noteDetailText.getEditor(); const editor = noteDetailText.getEditor();
const doc = editor.document;
doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection); editor.model.change( writer => {
const insertPosition = editor.model.document.selection.getFirstPosition();
writer.insertText(linkTitle, { linkHref: linkHref }, insertPosition);
});
} }
function addTextToEditor(text) { function addTextToEditor(text) {

View File

@@ -1,4 +1,5 @@
import treeService from './tree.js'; import treeService from './tree.js';
import treeUtils from './tree_utils.js';
import noteTypeService from './note_type.js'; import noteTypeService from './note_type.js';
import protectedSessionService from './protected_session.js'; import protectedSessionService from './protected_session.js';
import protectedSessionHolder from './protected_session_holder.js'; import protectedSessionHolder from './protected_session_holder.js';
@@ -24,6 +25,7 @@ const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display"); const $noteIdDisplay = $("#note-id-display");
const $labelList = $("#label-list"); const $labelList = $("#label-list");
const $labelListInner = $("#label-list-inner"); const $labelListInner = $("#label-list-inner");
const $childrenOverview = $("#children-overview");
let currentNote = null; let currentNote = null;
@@ -73,50 +75,42 @@ function noteChanged() {
async function reload() { async function reload() {
// no saving here // no saving here
await loadNoteToEditor(getCurrentNoteId()); await loadNoteDetail(getCurrentNoteId());
} }
async function switchToNote(noteId) { async function switchToNote(noteId) {
if (getCurrentNoteId() !== noteId) { if (getCurrentNoteId() !== noteId) {
await saveNoteIfChanged(); await saveNoteIfChanged();
await loadNoteToEditor(noteId); await loadNoteDetail(noteId);
} }
} }
async function saveNote() {
const note = getCurrentNote();
note.title = $noteTitle.val();
note.content = getComponent(note.type).getContent();
treeService.setNoteTitle(note.noteId, note.title);
await server.put('notes/' + note.noteId, note.dto);
isNoteChanged = false;
if (note.isProtected) {
protectedSessionHolder.touchProtectedSession();
}
infoService.showMessage("Saved!");
}
async function saveNoteIfChanged() { async function saveNoteIfChanged() {
if (!isNoteChanged) { if (!isNoteChanged) {
return; return;
} }
const note = getCurrentNote(); await saveNote();
updateNoteFromInputs(note);
await saveNoteToServer(note);
if (note.isProtected) {
protectedSessionHolder.touchProtectedSession();
}
}
function updateNoteFromInputs(note) {
note.title = $noteTitle.val();
note.content = getComponent(note.type).getContent();
treeService.setNoteTitle(note.noteId, note.title);
}
async function saveNoteToServer(note) {
const dto = Object.assign({}, note);
delete dto.treeCache;
delete dto.hideInAutocomplete;
await server.put('notes/' + dto.noteId, dto);
isNoteChanged = false;
infoService.showMessage("Saved!");
} }
function setNoteBackgroundIfProtected(note) { function setNoteBackgroundIfProtected(note) {
@@ -145,7 +139,7 @@ async function handleProtectedSession() {
protectedSessionService.ensureDialogIsClosed(); protectedSessionService.ensureDialogIsClosed();
} }
async function loadNoteToEditor(noteId) { async function loadNoteDetail(noteId) {
currentNote = await loadNote(noteId); currentNote = await loadNote(noteId);
if (isNewNoteCreated) { if (isNewNoteCreated) {
@@ -182,7 +176,35 @@ async function loadNoteToEditor(noteId) {
// after loading new note make sure editor is scrolled to the top // after loading new note make sure editor is scrolled to the top
$noteDetailWrapper.scrollTop(0); $noteDetailWrapper.scrollTop(0);
await loadLabelList(); const labels = await loadLabelList();
const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview');
await showChildrenOverview(hideChildrenOverview);
}
async function showChildrenOverview(hideChildrenOverview) {
if (hideChildrenOverview) {
$childrenOverview.hide();
return;
}
const note = getCurrentNote();
$childrenOverview.empty();
const notePath = treeService.getCurrentNotePath();
for (const childBranch of await note.getChildBranches()) {
const link = $('<a>', {
href: 'javascript:',
text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
}).attr('action', 'note').attr('note-path', notePath + '/' + childBranch.noteId);
const childEl = $('<div class="child-overview">').html(link);
$childrenOverview.append(childEl);
}
$childrenOverview.show();
} }
async function loadLabelList() { async function loadLabelList() {
@@ -202,6 +224,8 @@ async function loadLabelList() {
else { else {
$labelList.hide(); $labelList.hide();
} }
return labels;
} }
async function loadNote(noteId) { async function loadNote(noteId) {
@@ -245,8 +269,6 @@ setInterval(saveNoteIfChanged, 5000);
export default { export default {
reload, reload,
switchToNote, switchToNote,
updateNoteFromInputs,
saveNoteToServer,
setNoteBackgroundIfProtected, setNoteBackgroundIfProtected,
loadNote, loadNote,
getCurrentNote, getCurrentNote,
@@ -255,6 +277,7 @@ export default {
newNoteCreated, newNoteCreated,
focus, focus,
loadLabelList, loadLabelList,
saveNote,
saveNoteIfChanged, saveNoteIfChanged,
noteChanged noteChanged
}; };

View File

@@ -16,6 +16,10 @@ async function show() {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore"; CodeMirror.keyMap.default["Tab"] = "indentMore";
// these conflict with backward/forward navigation shortcuts
delete CodeMirror.keyMap.default["Alt-Left"];
delete CodeMirror.keyMap.default["Alt-Right"];
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($noteDetailCode[0], { codeEditor = CodeMirror($noteDetailCode[0], {
@@ -27,7 +31,8 @@ async function show() {
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false}, highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
lint: true, lint: true,
gutters: ["CodeMirror-lint-markers"], gutters: ["CodeMirror-lint-markers"],
lineNumbers: true lineNumbers: true,
tabindex: 2 // so that tab from title will lead to code editor focus
}); });
codeEditor.on('change', noteDetailService.noteChanged); codeEditor.on('change', noteDetailService.noteChanged);

View File

@@ -11,11 +11,10 @@ async function show() {
textEditor = await BalloonEditor.create($noteDetailText[0], {}); textEditor = await BalloonEditor.create($noteDetailText[0], {});
textEditor.document.on('change', noteDetailService.noteChanged); textEditor.model.document.on('change', noteDetailService.noteChanged);
} }
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49 textEditor.setData(noteDetailService.getCurrentNote().content);
textEditor.setData(noteDetailService.getCurrentNote().content || "<p></p>");
$noteDetailText.show(); $noteDetailText.show();
} }

View File

@@ -1,5 +1,5 @@
import treeService from './tree.js'; import treeService from './tree.js';
import noteDetail from './note_detail.js'; import noteDetailService from './note_detail.js';
import server from './server.js'; import server from './server.js';
import infoService from "./info.js"; import infoService from "./info.js";
@@ -84,13 +84,13 @@ function NoteTypeModel() {
}; };
async function save() { async function save() {
const note = noteDetail.getCurrentNote(); const note = noteDetailService.getCurrentNote();
await server.put('notes/' + note.noteId await server.put('notes/' + note.noteId
+ '/type/' + encodeURIComponent(self.type()) + '/type/' + encodeURIComponent(self.type())
+ '/mime/' + encodeURIComponent(self.mime())); + '/mime/' + encodeURIComponent(self.mime()));
await noteDetail.reload(); await noteDetailService.reload();
// for the note icon to be updated in the tree // for the note icon to be updated in the tree
await treeService.reload(); await treeService.reload();

View File

@@ -1,5 +1,5 @@
import treeService from './tree.js'; import treeService from './tree.js';
import noteDetail from './note_detail.js'; import noteDetailService from './note_detail.js';
import utils from './utils.js'; import utils from './utils.js';
import server from './server.js'; import server from './server.js';
import protectedSessionHolder from './protected_session_holder.js'; import protectedSessionHolder from './protected_session_holder.js';
@@ -57,7 +57,7 @@ async function setupProtectedSession() {
$dialog.dialog("close"); $dialog.dialog("close");
noteDetail.reload(); noteDetailService.reload();
treeService.reload(); treeService.reload();
if (protectedSessionDeferred !== null) { if (protectedSessionDeferred !== null) {
@@ -90,33 +90,27 @@ async function enterProtectedSession(password) {
async function protectNoteAndSendToServer() { async function protectNoteAndSendToServer() {
await ensureProtectedSession(true, true); await ensureProtectedSession(true, true);
const note = noteDetail.getCurrentNote(); const note = noteDetailService.getCurrentNote();
noteDetail.updateNoteFromInputs(note);
note.isProtected = true; note.isProtected = true;
await noteDetail.saveNoteToServer(note); await noteDetailService.saveNote(note);
treeService.setProtected(note.noteId, note.isProtected); treeService.setProtected(note.noteId, note.isProtected);
noteDetail.setNoteBackgroundIfProtected(note); noteDetailService.setNoteBackgroundIfProtected(note);
} }
async function unprotectNoteAndSendToServer() { async function unprotectNoteAndSendToServer() {
await ensureProtectedSession(true, true); await ensureProtectedSession(true, true);
const note = noteDetail.getCurrentNote(); const note = noteDetailService.getCurrentNote();
noteDetail.updateNoteFromInputs(note);
note.isProtected = false; note.isProtected = false;
await noteDetail.saveNoteToServer(note); await noteDetailService.saveNote(note);
treeService.setProtected(note.noteId, note.isProtected); treeService.setProtected(note.noteId, note.isProtected);
noteDetail.setNoteBackgroundIfProtected(note); noteDetailService.setNoteBackgroundIfProtected(note);
} }
async function protectBranch(noteId, protect) { async function protectBranch(noteId, protect) {
@@ -127,7 +121,7 @@ async function protectBranch(noteId, protect) {
infoService.showMessage("Request to un/protect sub tree has finished successfully"); infoService.showMessage("Request to un/protect sub tree has finished successfully");
treeService.reload(); treeService.reload();
noteDetail.reload(); noteDetailService.reload();
} }
$passwordForm.submit(() => { $passwordForm.submit(() => {

View File

@@ -1,6 +1,7 @@
import treeService from './tree.js'; import treeService from './tree.js';
import server from './server.js'; import server from './server.js';
import utils from './utils.js'; import utils from './utils.js';
import infoService from './info.js';
function ScriptApi(startNote, currentNote) { function ScriptApi(startNote, currentNote) {
const $pluginButtons = $("#plugin-buttons"); const $pluginButtons = $("#plugin-buttons");
@@ -56,8 +57,8 @@ function ScriptApi(startNote, currentNote) {
runOnServer, runOnServer,
formatDateISO: utils.formatDateISO, formatDateISO: utils.formatDateISO,
parseDate: utils.parseDate, parseDate: utils.parseDate,
showMessage: utils.showMessage, showMessage: infoService.showMessage,
showError: utils.showError, showError: infoService.showError,
reloadTree: treeService.reload reloadTree: treeService.reload
} }
} }

View File

@@ -293,7 +293,7 @@ function initFancyTree(branch) {
keyboard: false, // we takover keyboard handling in the hotkeys plugin keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"], extensions: ["hotkeys", "filter", "dnd", "clones"],
source: branch, source: branch,
scrollParent: $("#tree"), scrollParent: $tree,
click: (event, data) => { click: (event, data) => {
const targetType = data.targetType; const targetType = data.targetType;
const node = data.node; const node = data.node;
@@ -541,9 +541,9 @@ $(window).bind('hashchange', function() {
}); });
utils.bindShortcut('alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument utils.bindShortcut('alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
$collapseTreeButton.click(() => collapseTree());
$createTopLevelNoteButton.click(createNewTopLevelNote); $createTopLevelNoteButton.click(createNewTopLevelNote);
$collapseTreeButton.click(collapseTree);
$scrollToCurrentNoteButton.click(scrollToCurrentNote); $scrollToCurrentNoteButton.click(scrollToCurrentNote);
export default { export default {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -12,9 +12,40 @@
/*------------------------------------------------------------------------------ /*------------------------------------------------------------------------------
* Helpers * Helpers
*----------------------------------------------------------------------------*/ *----------------------------------------------------------------------------*/
.ui-helper-hidden { .fancytree-helper-hidden {
display: none; display: none;
} }
.fancytree-helper-indeterminate-cb {
color: #777;
}
.fancytree-helper-disabled {
color: #c0c0c0;
}
/* Helper to allow spinning loader icon with glyph-, ligature-, and SVG-icons. */
.fancytree-helper-spin {
-webkit-animation: spin 1000ms infinite linear;
animation: spin 1000ms infinite linear;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
/*------------------------------------------------------------------------------ /*------------------------------------------------------------------------------
* Container and UL / LI * Container and UL / LI
*----------------------------------------------------------------------------*/ *----------------------------------------------------------------------------*/
@@ -338,6 +369,16 @@ span.fancytree-node.fancytree-error span.fancytree-title {
/*------------------------------------------------------------------------------ /*------------------------------------------------------------------------------
* Drag'n'drop support * Drag'n'drop support
*----------------------------------------------------------------------------*/ *----------------------------------------------------------------------------*/
/* ext-dnd5: */
span.fancytree-childcounter {
color: #fff;
background: #337ab7;
border: 1px solid gray;
border-radius: 10px;
padding: 2px;
text-align: center;
}
/* ext-dnd: */
div.fancytree-drag-helper span.fancytree-childcounter, div.fancytree-drag-helper span.fancytree-childcounter,
div.fancytree-drag-helper span.fancytree-dnd-modifier { div.fancytree-drag-helper span.fancytree-dnd-modifier {
display: inline-block; display: inline-block;
@@ -402,8 +443,7 @@ span.fancytree-drag-source.fancytree-drag-remove {
.fancytree-container.fancytree-rtl span.fancytree-connector, .fancytree-container.fancytree-rtl span.fancytree-connector,
.fancytree-container.fancytree-rtl span.fancytree-expander, .fancytree-container.fancytree-rtl span.fancytree-expander,
.fancytree-container.fancytree-rtl span.fancytree-icon, .fancytree-container.fancytree-rtl span.fancytree-icon,
.fancytree-container.fancytree-rtl span.fancytree-drag-helper-img, .fancytree-container.fancytree-rtl span.fancytree-drag-helper-img {
.fancytree-container.fancytree-rtl #fancytree-drop-marker {
background-image: url("../skin-win8/icons-rtl.gif"); background-image: url("../skin-win8/icons-rtl.gif");
} }
.fancytree-container.fancytree-rtl .fancytree-exp-n span.fancytree-expander, .fancytree-container.fancytree-rtl .fancytree-exp-n span.fancytree-expander,
@@ -425,6 +465,9 @@ ul.fancytree-container.fancytree-rtl li.fancytree-lastsib,
ul.fancytree-container.fancytree-rtl.fancytree-no-connector > li { ul.fancytree-container.fancytree-rtl.fancytree-no-connector > li {
background-image: none; background-image: none;
} }
#fancytree-drop-marker.fancytree-rtl {
background-image: url("../skin-win8/icons-rtl.gif");
}
/*------------------------------------------------------------------------------ /*------------------------------------------------------------------------------
* 'table' extension * 'table' extension
*----------------------------------------------------------------------------*/ *----------------------------------------------------------------------------*/
@@ -482,7 +525,7 @@ table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right:h
* 'filter' extension * 'filter' extension
*----------------------------------------------------------------------------*/ *----------------------------------------------------------------------------*/
.fancytree-ext-filter-dimm span.fancytree-node span.fancytree-title { .fancytree-ext-filter-dimm span.fancytree-node span.fancytree-title {
color: silver; color: #c0c0c0;
font-weight: lighter; font-weight: lighter;
} }
.fancytree-ext-filter-dimm tr.fancytree-submatch span.fancytree-title, .fancytree-ext-filter-dimm tr.fancytree-submatch span.fancytree-title,
@@ -501,7 +544,7 @@ table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right:h
} }
.fancytree-ext-filter-hide tr.fancytree-submatch span.fancytree-title, .fancytree-ext-filter-hide tr.fancytree-submatch span.fancytree-title,
.fancytree-ext-filter-hide span.fancytree-node.fancytree-submatch span.fancytree-title { .fancytree-ext-filter-hide span.fancytree-node.fancytree-submatch span.fancytree-title {
color: silver; color: #c0c0c0;
font-weight: lighter; font-weight: lighter;
} }
.fancytree-ext-filter-hide tr.fancytree-match span.fancytree-title, .fancytree-ext-filter-hide tr.fancytree-match span.fancytree-title,

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -4,18 +4,12 @@
display: grid; display: grid;
grid-template-areas: "header header" grid-template-areas: "header header"
"tree-actions title" "left-pane title"
"search note-content" "left-pane note-detail";
"tree note-content"
"parent-list note-content"
"parent-list label-list";
grid-template-columns: 2fr 5fr; grid-template-columns: 2fr 5fr;
grid-template-rows: auto grid-template-rows: auto
auto auto
auto 1fr;
1fr
auto
auto;
justify-content: center; justify-content: center;
grid-gap: 10px; grid-gap: 10px;
@@ -28,6 +22,23 @@
align-items: center; align-items: center;
} }
#note-detail-wrapper {
position: relative;
overflow: hidden;
grid-area: note-detail;
padding-left: 10px;
padding-top: 10px;
display: flex;
flex-direction: column;
}
#note-detail-component-wrapper {
flex-grow: 1;
position: relative;
overflow: auto;
flex-basis: content;
}
.note-detail-component { .note-detail-component {
display: none; display: none;
} }
@@ -36,7 +47,7 @@
border: 0 !important; border: 0 !important;
box-shadow: none !important; box-shadow: none !important;
/* This is because with empty content height of editor is 0 and it's impossible to click into it */ /* This is because with empty content height of editor is 0 and it's impossible to click into it */
min-height: 400px; min-height: 200px;
overflow: auto; overflow: auto;
} }
@@ -249,7 +260,7 @@ div.ui-tooltip {
} }
#note-detail-code { #note-detail-code {
height: 100%; min-height: 200px;
} }
.CodeMirror { .CodeMirror {
@@ -274,7 +285,6 @@ div.ui-tooltip {
.cm-matchhighlight {background-color: #eeeeee} .cm-matchhighlight {background-color: #eeeeee}
#label-list { #label-list {
grid-area: label-list;
color: #777777; color: #777777;
border-top: 1px solid #eee; border-top: 1px solid #eee;
padding: 5px; display: none; padding: 5px; display: none;
@@ -288,4 +298,36 @@ div.ui-tooltip {
#file-table th, #file-table td { #file-table th, #file-table td {
padding: 10px; padding: 10px;
font-size: large; font-size: large;
}
#children-overview {
flex-grow: 1000;
flex-shrink: 1000;
flex-basis: 0px;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
height: 100px;
overflow: auto;
}
.child-overview {
font-weight: bold;
font-size: large;
padding: 10px;
background: #f4f4f4;
width: 150px;
height: 90px;
line-height: 2em;
margin-right: 20px;
border-radius: 15px;
overflow: hidden;
text-align: center;
margin-top: 15px;
text-overflow: ellipsis;
word-wrap: break-word;
}
.child-overview a {
color: #444;
} }

View File

@@ -13,7 +13,68 @@ async function exportNote(req, res) {
const pack = tar.pack(); const pack = tar.pack();
const name = await exportNoteInner(branchId, '', pack); const exportedNoteIds = [];
const name = await exportNoteInner(branchId, '');
async function exportNoteInner(branchId, directory) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
const childFileName = directory + sanitize(note.title);
if (exportedNoteIds.includes(note.noteId)) {
saveMetadataFile(childFileName, {
version: 1,
clone: true,
noteId: note.noteId,
prefix: branch.prefix
});
return;
}
const metadata = {
version: 1,
clone: false,
noteId: note.noteId,
title: note.title,
prefix: branch.prefix,
type: note.type,
mime: note.mime,
labels: (await note.getLabels()).map(label => {
return {
name: label.name,
value: label.value
};
})
};
if (metadata.labels.find(label => label.name === 'excludeFromExport')) {
return;
}
saveMetadataFile(childFileName, metadata);
saveDataFile(childFileName, note);
exportedNoteIds.push(note.noteId);
for (const child of await note.getChildBranches()) {
await exportNoteInner(child.branchId, childFileName + "/");
}
return childFileName;
}
function saveDataFile(childFileName, note) {
const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
pack.entry({name: childFileName + ".dat", size: content.length}, content);
}
function saveMetadataFile(childFileName, metadata) {
const metadataJson = JSON.stringify(metadata, null, '\t');
pack.entry({name: childFileName + ".meta", size: metadataJson.length}, metadataJson);
}
pack.finalize(); pack.finalize();
@@ -23,51 +84,6 @@ async function exportNote(req, res) {
pack.pipe(res); pack.pipe(res);
} }
async function exportNoteInner(branchId, directory, pack) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
if (note.isProtected) {
return;
}
const metadata = await getMetadata(note);
if (metadata.labels.find(label => label.name === 'excludeFromExport')) {
return;
}
const metadataJson = JSON.stringify(metadata, null, '\t');
const childFileName = directory + sanitize(note.title);
pack.entry({ name: childFileName + ".meta", size: metadataJson.length }, metadataJson);
const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
pack.entry({ name: childFileName + ".dat", size: content.length }, content);
for (const child of await note.getChildBranches()) {
await exportNoteInner(child.branchId, childFileName + "/", pack);
}
return childFileName;
}
async function getMetadata(note) {
return {
version: 1,
title: note.title,
type: note.type,
mime: note.mime,
labels: (await note.getLabels()).map(label => {
return {
name: label.name,
value: label.value
};
})
};
}
module.exports = { module.exports = {
exportNote exportNote
}; };

View File

@@ -3,6 +3,7 @@
const repository = require('../../services/repository'); const repository = require('../../services/repository');
const labelService = require('../../services/labels'); const labelService = require('../../services/labels');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const Branch = require('../../entities/branch');
const tar = require('tar-stream'); const tar = require('tar-stream');
const stream = require('stream'); const stream = require('stream');
const path = require('path'); const path = require('path');
@@ -31,7 +32,7 @@ async function parseImportFile(file) {
const extract = tar.extract(); const extract = tar.extract();
extract.on('entry', function(header, stream, next) { extract.on('entry', function(header, stream, next) {
let {name, key} = getFileName(header.name); const {name, key} = getFileName(header.name);
let file = fileMap[name]; let file = fileMap[name];
@@ -97,30 +98,46 @@ async function importTar(req) {
const files = await parseImportFile(file); const files = await parseImportFile(file);
await importNotes(files, parentNoteId); // maps from original noteId (in tar file) to newly generated noteId
const noteIdMap = {};
await importNotes(files, parentNoteId, noteIdMap);
} }
async function importNotes(files, parentNoteId) { async function importNotes(files, parentNoteId, noteIdMap) {
for (const file of files) { for (const file of files) {
if (file.meta.version !== 1) { if (file.meta.version !== 1) {
throw new Error("Can't read meta data version " + file.meta.version); throw new Error("Can't read meta data version " + file.meta.version);
} }
if (file.meta.clone) {
await new Branch({
parentNoteId: parentNoteId,
noteId: noteIdMap[file.meta.noteId],
prefix: file.meta.prefix
}).save();
return;
}
if (file.meta.type !== 'file') { if (file.meta.type !== 'file') {
file.data = file.data.toString("UTF-8"); file.data = file.data.toString("UTF-8");
} }
const {note} = await noteService.createNote(parentNoteId, file.meta.title, file.data, { const {note} = await noteService.createNote(parentNoteId, file.meta.title, file.data, {
type: file.meta.type, type: file.meta.type,
mime: file.meta.mime mime: file.meta.mime,
prefix: file.meta.prefix
}); });
noteIdMap[file.meta.noteId] = note.noteId;
for (const label of file.meta.labels) { for (const label of file.meta.labels) {
await labelService.createLabel(note.noteId, label.name, label.value); await labelService.createLabel(note.noteId, label.name, label.value);
} }
if (file.children.length > 0) { if (file.children.length > 0) {
await importNotes(file.children, note.noteId); await importNotes(file.children, note.noteId, noteIdMap);
} }
} }
} }

View File

@@ -10,8 +10,8 @@ const log = require('../../services/log');
async function checkSync() { async function checkSync() {
return { return {
'hashes': await contentHashService.getHashes(), hashes: await contentHashService.getHashes(),
'max_sync_id': await sql.getValue('SELECT MAX(id) FROM sync') maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync')
}; };
} }
@@ -58,126 +58,18 @@ async function forceNoteSync(req) {
async function getChanged(req) { async function getChanged(req) {
const lastSyncId = parseInt(req.query.lastSyncId); const lastSyncId = parseInt(req.query.lastSyncId);
return await sql.getRows("SELECT * FROM sync WHERE id > ?", [lastSyncId]); const syncs = await sql.getRows("SELECT * FROM sync WHERE id > ? LIMIT 1000", [lastSyncId]);
return await syncService.getSyncRecords(syncs);
} }
async function getNote(req) { async function update(req) {
const noteId = req.params.noteId; const sourceId = req.body.sourceId;
const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); const entities = req.body.entities;
syncService.serializeNoteContentBuffer(entity); for (const {sync, entity} of entities) {
await syncUpdateService.updateEntity(sync, entity, sourceId);
return {
entity: entity
};
}
async function getBranch(req) {
const branchId = req.params.branchId;
return await sql.getRow("SELECT * FROM branches WHERE branchId = ?", [branchId]);
}
async function getNoteRevision(req) {
const noteRevisionId = req.params.noteRevisionId;
return await sql.getRow("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [noteRevisionId]);
}
async function getOption(req) {
const name = req.params.name;
const opt = await sql.getRow("SELECT * FROM options WHERE name = ?", [name]);
if (!opt.isSynced) {
return [400, "This option can't be synced."];
} }
else {
return opt;
}
}
async function getNoteReordering(req) {
const parentNoteId = req.params.parentNoteId;
return {
parentNoteId: parentNoteId,
ordering: await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId])
};
}
async function getRecentNote(req) {
const branchId = req.params.branchId;
return await sql.getRow("SELECT * FROM recent_notes WHERE branchId = ?", [branchId]);
}
async function getImage(req) {
const imageId = req.params.imageId;
const entity = await sql.getRow("SELECT * FROM images WHERE imageId = ?", [imageId]);
if (entity && entity.data !== null) {
entity.data = entity.data.toString('base64');
}
return entity;
}
async function getNoteImage(req) {
const noteImageId = req.params.noteImageId;
return await sql.getRow("SELECT * FROM note_images WHERE noteImageId = ?", [noteImageId]);
}
async function getLabel(req) {
const labelId = req.params.labelId;
return await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [labelId]);
}
async function getApiToken(req) {
const apiTokenId = req.params.apiTokenId;
return await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [apiTokenId]);
}
async function updateNote(req) {
await syncUpdateService.updateNote(req.body.entity, req.body.sourceId);
}
async function updateBranch(req) {
await syncUpdateService.updateBranch(req.body.entity, req.body.sourceId);
}
async function updateNoteRevision(req) {
await syncUpdateService.updateNoteRevision(req.body.entity, req.body.sourceId);
}
async function updateNoteReordering(req) {
await syncUpdateService.updateNoteReordering(req.body.entity, req.body.sourceId);
}
async function updateOption(req) {
await syncUpdateService.updateOptions(req.body.entity, req.body.sourceId);
}
async function updateRecentNote(req) {
await syncUpdateService.updateRecentNotes(req.body.entity, req.body.sourceId);
}
async function updateImage(req) {
await syncUpdateService.updateImage(req.body.entity, req.body.sourceId);
}
async function updateNoteImage(req) {
await syncUpdateService.updateNoteImage(req.body.entity, req.body.sourceId);
}
async function updateLabel(req) {
await syncUpdateService.updateLabel(req.body.entity, req.body.sourceId);
}
async function updateApiToken(req) {
await syncUpdateService.updateApiToken(req.body.entity, req.body.sourceId);
} }
module.exports = { module.exports = {
@@ -187,24 +79,5 @@ module.exports = {
forceFullSync, forceFullSync,
forceNoteSync, forceNoteSync,
getChanged, getChanged,
getNote, update
getBranch,
getImage,
getNoteImage,
getNoteReordering,
getNoteRevision,
getRecentNote,
getOption,
getLabel,
getApiToken,
updateNote,
updateBranch,
updateImage,
updateNoteImage,
updateNoteReordering,
updateNoteRevision,
updateRecentNote,
updateOption,
updateLabel,
updateApiToken
}; };

View File

@@ -147,26 +147,7 @@ function register(app) {
apiRoute(POST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); apiRoute(POST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync);
apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync);
apiRoute(GET, '/api/sync/changed', syncApiRoute.getChanged); apiRoute(GET, '/api/sync/changed', syncApiRoute.getChanged);
apiRoute(GET, '/api/sync/notes/:noteId', syncApiRoute.getNote); apiRoute(PUT, '/api/sync/update', syncApiRoute.update);
apiRoute(GET, '/api/sync/branches/:branchId', syncApiRoute.getBranch);
apiRoute(GET, '/api/sync/note_revisions/:noteRevisionId', syncApiRoute.getNoteRevision);
apiRoute(GET, '/api/sync/options/:name', syncApiRoute.getOption);
apiRoute(GET, '/api/sync/note_reordering/:parentNoteId', syncApiRoute.getNoteReordering);
apiRoute(GET, '/api/sync/recent_notes/:branchId', syncApiRoute.getRecentNote);
apiRoute(GET, '/api/sync/images/:imageId', syncApiRoute.getImage);
apiRoute(GET, '/api/sync/note_images/:noteImageId', syncApiRoute.getNoteImage);
apiRoute(GET, '/api/sync/labels/:labelId', syncApiRoute.getLabel);
apiRoute(GET, '/api/sync/api_tokens/:apiTokenId', syncApiRoute.getApiToken);
apiRoute(PUT, '/api/sync/notes', syncApiRoute.updateNote);
apiRoute(PUT, '/api/sync/branches', syncApiRoute.updateBranch);
apiRoute(PUT, '/api/sync/note_revisions', syncApiRoute.updateNoteRevision);
apiRoute(PUT, '/api/sync/note_reordering', syncApiRoute.updateNoteReordering);
apiRoute(PUT, '/api/sync/options', syncApiRoute.updateOption);
apiRoute(PUT, '/api/sync/recent_notes', syncApiRoute.updateRecentNote);
apiRoute(PUT, '/api/sync/images', syncApiRoute.updateImage);
apiRoute(PUT, '/api/sync/note_images', syncApiRoute.updateNoteImage);
apiRoute(PUT, '/api/sync/labels', syncApiRoute.updateLabel);
apiRoute(PUT, '/api/sync/api_tokens', syncApiRoute.updateApiToken);
apiRoute(GET, '/api/event-log', eventLogRoute.getEventLog); apiRoute(GET, '/api/event-log', eventLogRoute.getEventLog);

View File

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

View File

@@ -1 +1 @@
module.exports = { buildDate:"2018-01-17T23:59:03-05:00", buildRevision: "651a9fb3272c85d287c16d5a4978464fb7d2490d" }; module.exports = { buildDate:"2018-04-14T08:28:50-04:00", buildRevision: "d57057ba28d2d93ffaeed15900116836fc791968" };

View File

@@ -197,16 +197,6 @@ async function runAllChecks() {
AND images.isDeleted = 1`, AND images.isDeleted = 1`,
"Note image is not deleted while image is deleted for noteImageId", errorList); "Note image is not deleted while image is deleted for noteImageId", errorList);
await runCheck(`
SELECT
noteId
FROM
notes
WHERE
isDeleted = 0
AND (title IS NULL OR content IS NULL)`,
"Note has null title or text", errorList);
await runCheck(` await runCheck(`
SELECT SELECT
noteId noteId

View File

@@ -1,6 +1,10 @@
"use strict";
const sql = require('./sql'); const sql = require('./sql');
const utils = require('./utils'); const utils = require('./utils');
const log = require('./log'); const log = require('./log');
const eventLogService = require('./event_log');
const messagingService = require('./messaging');
function getHash(rows) { function getHash(rows) {
let hash = ''; let hash = '';
@@ -121,6 +125,29 @@ async function getHashes() {
return hashes; return hashes;
} }
async function checkContentHashes(otherHashes) {
const hashes = await getHashes();
let allChecksPassed = true;
for (const key in hashes) {
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]}`);
if (key !== 'recent_notes') {
// let's not get alarmed about recent notes which get updated often and can cause failures in race conditions
await messagingService.sendMessageToAllClients({type: 'sync-hash-check-failed'});
}
}
}
if (allChecksPassed) {
log.info("Content hash checks PASSED");
}
}
module.exports = { module.exports = {
getHashes getHashes,
checkContentHashes
}; };

View File

@@ -11,7 +11,8 @@ const BUILTIN_LABELS = [
'run', 'run',
'manualTransactionHandling', 'manualTransactionHandling',
'disableInclusion', 'disableInclusion',
'appCss' 'appCss',
'hideChildrenOverview'
]; ];
async function getNotesWithLabel(name, value) { async function getNotesWithLabel(name, value) {

View File

@@ -15,14 +15,22 @@ const logger = require('simple-node-logger').createRollingFileLogger({
}); });
function info(message) { function info(message) {
logger.info(message); // info messages are logged asynchronously
setTimeout(() => {
console.log(message);
console.log(message); logger.info(message);
}, 0);
} }
function error(message) { function error(message) {
message = "ERROR: " + message;
// we're using .info() instead of .error() because simple-node-logger emits weird error for showError() // we're using .info() instead of .error() because simple-node-logger emits weird error for showError()
info("ERROR: " + message); // errors are logged synchronously to make sure it doesn't get lost in case of crash
logger.info(message);
console.trace(message);
} }
const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ]; const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ];

View File

@@ -56,6 +56,7 @@ async function createNewNote(parentNoteId, noteData) {
noteId: note.noteId, noteId: note.noteId,
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
notePosition: newNotePos, notePosition: newNotePos,
prefix: noteData.prefix,
isExpanded: 0 isExpanded: 0
}).save(); }).save();
@@ -180,6 +181,8 @@ async function saveNoteRevision(note) {
// title and text should be decrypted now // title and text should be decrypted now
title: note.title, title: note.title,
content: note.content, content: note.content,
type: note.type,
mime: note.mime,
isProtected: 0, // will be fixed in the protectNoteRevisions() call isProtected: 0, // will be fixed in the protectNoteRevisions() call
dateModifiedFrom: note.dateModified, dateModifiedFrom: note.dateModified,
dateModifiedTo: dateUtils.nowDate() dateModifiedTo: dateUtils.nowDate()
@@ -198,7 +201,7 @@ async function updateNote(noteId, noteUpdates) {
await saveNoteRevision(note); await saveNoteRevision(note);
note.title = noteUpdates.title; note.title = noteUpdates.title;
note.content = noteUpdates.content; note.setContent(noteUpdates.content);
note.isProtected = noteUpdates.isProtected; note.isProtected = noteUpdates.isProtected;
await note.save(); await note.save();

View File

@@ -149,11 +149,13 @@ async function transactional(func) {
resolve(); resolve();
} }
catch (e) { catch (e) {
log.error("Error executing transaction, executing rollback. Inner exception: " + e.stack + error.stack); if (transactionActive) {
log.error("Error executing transaction, executing rollback. Inner exception: " + e.stack + error.stack);
await rollback(); await rollback();
transactionActive = false; transactionActive = false;
}
reject(e); reject(e);
} }

View File

@@ -10,10 +10,8 @@ const sourceIdService = require('./source_id');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const syncUpdateService = require('./sync_update'); const syncUpdateService = require('./sync_update');
const contentHashService = require('./content_hash'); const contentHashService = require('./content_hash');
const eventLogService = require('./event_log');
const fs = require('fs'); const fs = require('fs');
const appInfo = require('./app_info'); const appInfo = require('./app_info');
const messagingService = require('./messaging');
const syncSetup = require('./sync_setup'); const syncSetup = require('./sync_setup');
const syncMutexService = require('./sync_mutex'); const syncMutexService = require('./sync_mutex');
const cls = require('./cls'); const cls = require('./cls');
@@ -91,69 +89,19 @@ async function login() {
return syncContext; return syncContext;
} }
async function getLastSyncedPull() {
return parseInt(await optionService.getOption('lastSyncedPull'));
}
async function setLastSyncedPull(syncId) {
await optionService.setOption('lastSyncedPull', syncId);
}
async function pullSync(syncContext) { async function pullSync(syncContext) {
const lastSyncedPull = await getLastSyncedPull(); const changesUri = '/api/sync/changed?lastSyncId=' + await getLastSyncedPull();
const changesUri = '/api/sync/changed?lastSyncId=' + lastSyncedPull; const rows = await syncRequest(syncContext, 'GET', changesUri);
const syncRows = await syncRequest(syncContext, 'GET', changesUri); log.info("Pulled " + rows.length + " changes from " + changesUri);
log.info("Pulled " + syncRows.length + " changes from " + changesUri); for (const {sync, entity} of rows) {
for (const sync of syncRows) {
if (sourceIdService.isLocalSourceId(sync.sourceId)) { if (sourceIdService.isLocalSourceId(sync.sourceId)) {
log.info(`Skipping pull #${sync.id} ${sync.entityName} ${sync.entityId} because ${sync.sourceId} is a local source id.`); log.info(`Skipping pull #${sync.id} ${sync.entityName} ${sync.entityId} because ${sync.sourceId} is a local source id.`);
await setLastSyncedPull(sync.id);
continue;
}
const resp = await syncRequest(syncContext, 'GET', "/api/sync/" + sync.entityName + "/" + encodeURIComponent(sync.entityId));
if (!resp || (sync.entityName === 'notes' && !resp.entity)) {
log.error(`Empty response to pull for sync #${sync.id} ${sync.entityName}, id=${sync.entityId}`);
}
else if (sync.entityName === 'notes') {
await syncUpdateService.updateNote(resp.entity, syncContext.sourceId);
}
else if (sync.entityName === 'branches') {
await syncUpdateService.updateBranch(resp, syncContext.sourceId);
}
else if (sync.entityName === 'note_revisions') {
await syncUpdateService.updateNoteRevision(resp, syncContext.sourceId);
}
else if (sync.entityName === 'note_reordering') {
await syncUpdateService.updateNoteReordering(resp, syncContext.sourceId);
}
else if (sync.entityName === 'options') {
await syncUpdateService.updateOptions(resp, syncContext.sourceId);
}
else if (sync.entityName === 'recent_notes') {
await syncUpdateService.updateRecentNotes(resp, syncContext.sourceId);
}
else if (sync.entityName === 'images') {
await syncUpdateService.updateImage(resp, syncContext.sourceId);
}
else if (sync.entityName === 'note_images') {
await syncUpdateService.updateNoteImage(resp, syncContext.sourceId);
}
else if (sync.entityName === 'labels') {
await syncUpdateService.updateLabel(resp, syncContext.sourceId);
}
else if (sync.entityName === 'api_tokens') {
await syncUpdateService.updateApiToken(resp, syncContext.sourceId);
} }
else { else {
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); await syncUpdateService.updateEntity(sync, entity, syncContext.sourceId);
} }
await setLastSyncedPull(sync.id); await setLastSyncedPull(sync.id);
@@ -162,145 +110,69 @@ async function pullSync(syncContext) {
log.info("Finished pull"); log.info("Finished pull");
} }
async function getLastSyncedPush() {
return parseInt(await optionService.getOption('lastSyncedPush'));
}
async function setLastSyncedPush(lastSyncedPush) {
await optionService.setOption('lastSyncedPush', lastSyncedPush);
}
async function pushSync(syncContext) { async function pushSync(syncContext) {
let lastSyncedPush = await getLastSyncedPush(); let lastSyncedPush = await getLastSyncedPush();
while (true) { while (true) {
const sync = await sql.getRowOrNull('SELECT * FROM sync WHERE id > ? LIMIT 1', [lastSyncedPush]); const syncs = await sql.getRows('SELECT * FROM sync WHERE id > ? LIMIT 1000', [lastSyncedPush]);
if (sync === null) { const filteredSyncs = syncs.filter(sync => {
// nothing to sync if (sync.sourceId === syncContext.sourceId) {
log.info(`Skipping push #${sync.id} ${sync.entityName} ${sync.entityId} because it originates from sync target`);
// this may set lastSyncedPush beyond what's actually sent (because of size limit)
// so this is applied to the database only if there's no actual update
// TODO: it would be better to simplify this somehow
lastSyncedPush = sync.id;
return false;
}
else {
return true;
}
});
if (filteredSyncs.length === 0) {
log.info("Nothing to push"); log.info("Nothing to push");
await setLastSyncedPush(lastSyncedPush);
break; break;
} }
if (sync.sourceId === syncContext.sourceId) { const syncRecords = await getSyncRecords(filteredSyncs);
log.info(`Skipping push #${sync.id} ${sync.entityName} ${sync.entityId} because it originates from sync target`);
}
else {
await pushEntity(sync, syncContext);
}
lastSyncedPush = sync.id; log.info(`Pushing ${syncRecords.length} syncs.`);
await syncRequest(syncContext, 'PUT', '/api/sync/update', {
sourceId: sourceIdService.getCurrentSourceId(),
entities: syncRecords
});
lastSyncedPush = syncRecords[syncRecords.length - 1].sync.id;
await setLastSyncedPush(lastSyncedPush); await setLastSyncedPush(lastSyncedPush);
} }
} }
async function pushEntity(sync, syncContext) {
let entity;
if (sync.entityName === 'notes') {
entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]);
serializeNoteContentBuffer(entity);
}
else if (sync.entityName === 'branches') {
entity = await sql.getRow('SELECT * FROM branches WHERE branchId = ?', [sync.entityId]);
}
else if (sync.entityName === 'note_revisions') {
entity = await sql.getRow('SELECT * FROM note_revisions WHERE noteRevisionId = ?', [sync.entityId]);
}
else if (sync.entityName === 'note_reordering') {
entity = {
parentNoteId: sync.entityId,
ordering: await sql.getMap('SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [sync.entityId])
};
}
else if (sync.entityName === 'options') {
entity = await sql.getRow('SELECT * FROM options WHERE name = ?', [sync.entityId]);
}
else if (sync.entityName === 'recent_notes') {
entity = await sql.getRow('SELECT * FROM recent_notes WHERE branchId = ?', [sync.entityId]);
}
else if (sync.entityName === 'images') {
entity = await sql.getRow('SELECT * FROM images WHERE imageId = ?', [sync.entityId]);
if (entity.data !== null) {
entity.data = entity.data.toString('base64');
}
}
else if (sync.entityName === 'note_images') {
entity = await sql.getRow('SELECT * FROM note_images WHERE noteImageId = ?', [sync.entityId]);
}
else if (sync.entityName === 'labels') {
entity = await sql.getRow('SELECT * FROM labels WHERE labelId = ?', [sync.entityId]);
}
else if (sync.entityName === 'api_tokens') {
entity = await sql.getRow('SELECT * FROM api_tokens WHERE apiTokenId = ?', [sync.entityId]);
}
else {
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
}
if (!entity) {
log.info(`Sync #${sync.id} entity for ${sync.entityName} ${sync.entityId} doesn't exist. Skipping.`);
return;
}
log.info(`Pushing changes in sync #${sync.id} ${sync.entityName} ${sync.entityId}`);
const payload = {
sourceId: sourceIdService.getCurrentSourceId(),
entity: entity
};
await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload);
}
function serializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = note.content.toString("binary");
}
}
async function checkContentHash(syncContext) { async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
if (await getLastSyncedPull() < resp.max_sync_id) { if (await getLastSyncedPull() < resp.maxSyncId) {
log.info("There are some outstanding pulls, skipping content check."); log.info("There are some outstanding pulls, skipping content check.");
return; return;
} }
const lastSyncedPush = await getLastSyncedPush(); const notPushedSyncs = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [await getLastSyncedPush()]);
const notPushedSyncs = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
if (notPushedSyncs > 0) { if (notPushedSyncs > 0) {
log.info("There's " + notPushedSyncs + " outstanding pushes, skipping content check."); log.info(`There's ${notPushedSyncs} outstanding pushes, skipping content check.`);
return; return;
} }
const hashes = await contentHashService.getHashes(); await contentHashService.checkContentHashes(resp.hashes);
let allChecksPassed = true;
for (const key in hashes) {
if (hashes[key] !== resp.hashes[key]) {
allChecksPassed = false;
await eventLogService.addEvent(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${resp.hashes[key]}`);
if (key !== 'recent_notes') {
// let's not get alarmed about recent notes which get updated often and can cause failures in race conditions
await messagingService.sendMessageToAllClients({type: 'sync-hash-check-failed'});
}
}
}
if (allChecksPassed) {
log.info("Content hash checks PASSED");
}
} }
async function syncRequest(syncContext, method, uri, body) { async function syncRequest(syncContext, method, uri, body) {
@@ -331,6 +203,80 @@ async function syncRequest(syncContext, method, uri, body) {
} }
} }
const primaryKeys = {
"notes": "noteId",
"branches": "branchId",
"note_revisions": "noteRevisionId",
"option": "name",
"recent_notes": "branchId",
"images": "imageId",
"note_images": "noteImageId",
"labels": "labelId",
"api_tokens": "apiTokenId"
};
async function getEntityRow(entityName, entityId) {
if (entityName === 'note_reordering') {
return await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [entityId]);
}
else {
const primaryKey = primaryKeys[entityName];
if (!primaryKey) {
throw new Error("Unknown entity " + entityName);
}
const entity = await sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]);
if (entityName === 'notes' && entity.type === 'file') {
entity.content = entity.content.toString("binary");
}
else if (entityName === 'images') {
entity.data = entity.data.toString('base64');
}
return entity;
}
}
async function getSyncRecords(syncs) {
const records = [];
let length = 0;
for (const sync of syncs) {
const record = {
sync: sync,
entity: await getEntityRow(sync.entityName, sync.entityId)
};
records.push(record);
length += JSON.stringify(record).length;
if (length > 1000000) {
break;
}
}
return records;
}
async function getLastSyncedPull() {
return parseInt(await optionService.getOption('lastSyncedPull'));
}
async function setLastSyncedPull(syncId) {
await optionService.setOption('lastSyncedPull', syncId);
}
async function getLastSyncedPush() {
return parseInt(await optionService.getOption('lastSyncedPush'));
}
async function setLastSyncedPush(lastSyncedPush) {
await optionService.setOption('lastSyncedPush', lastSyncedPush);
}
sqlInit.dbReady.then(() => { sqlInit.dbReady.then(() => {
if (syncSetup.isSyncSetup) { if (syncSetup.isSyncSetup) {
log.info("Setting up sync to " + syncSetup.SYNC_SERVER + " with timeout " + syncSetup.SYNC_TIMEOUT); log.info("Setting up sync to " + syncSetup.SYNC_SERVER + " with timeout " + syncSetup.SYNC_TIMEOUT);
@@ -357,5 +303,5 @@ sqlInit.dbReady.then(() => {
module.exports = { module.exports = {
sync, sync,
serializeNoteContentBuffer getSyncRecords
}; };

View File

@@ -91,6 +91,8 @@ async function fillSyncRows(entityName, entityKey) {
} }
async function fillAllSyncRows() { async function fillAllSyncRows() {
await sql.execute("DELETE FROM sync");
await fillSyncRows("notes", "noteId"); await fillSyncRows("notes", "noteId");
await fillSyncRows("branches", "branchId"); await fillSyncRows("branches", "branchId");
await fillSyncRows("note_revisions", "noteRevisionId"); await fillSyncRows("note_revisions", "noteRevisionId");

View File

@@ -3,6 +3,44 @@ const log = require('./log');
const eventLogService = require('./event_log'); const eventLogService = require('./event_log');
const syncTableService = require('./sync_table'); const syncTableService = require('./sync_table');
async function updateEntity(sync, entity, sourceId) {
const {entityName} = sync;
if (entityName === 'notes') {
await updateNote(entity, sourceId);
}
else if (entityName === 'branches') {
await updateBranch(entity, sourceId);
}
else if (entityName === 'note_revisions') {
await updateNoteRevision(entity, sourceId);
}
else if (entityName === 'note_reordering') {
await updateNoteReordering(sync.entityId, entity, sourceId);
}
else if (entityName === 'options') {
await updateOptions(entity, sourceId);
}
else if (entityName === 'recent_notes') {
await updateRecentNotes(entity, sourceId);
}
else if (entityName === 'images') {
await updateImage(entity, sourceId);
}
else if (entityName === 'note_images') {
await updateNoteImage(entity, sourceId);
}
else if (entityName === 'labels') {
await updateLabel(entity, sourceId);
}
else if (entityName === 'api_tokens') {
await updateApiToken(entity, sourceId);
}
else {
throw new Error(`Unrecognized entity type ${sync}`);
}
}
function deserializeNoteContentBuffer(note) { function deserializeNoteContentBuffer(note) {
if (note.type === 'file') { if (note.type === 'file') {
note.content = new Buffer(note.content, 'binary'); note.content = new Buffer(note.content, 'binary');
@@ -58,13 +96,13 @@ async function updateNoteRevision(entity, sourceId) {
}); });
} }
async function updateNoteReordering(entity, sourceId) { async function updateNoteReordering(entityId, entity, sourceId) {
await sql.transactional(async () => { await sql.transactional(async () => {
Object.keys(entity.ordering).forEach(async key => { Object.keys(entity).forEach(async key => {
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity.ordering[key], key]); await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]);
}); });
await syncTableService.addNoteReorderingSync(entity.parentNoteId, sourceId); await syncTableService.addNoteReorderingSync(entityId, sourceId);
}); });
} }
@@ -159,14 +197,5 @@ async function updateApiToken(entity, sourceId) {
} }
module.exports = { module.exports = {
updateNote, updateEntity
updateBranch,
updateNoteRevision,
updateNoteReordering,
updateOptions,
updateRecentNotes,
updateImage,
updateNoteImage,
updateLabel,
updateApiToken
}; };

View File

@@ -38,7 +38,7 @@
</div> </div>
</div> </div>
<div class="hide-toggle" style="grid-area: tree-actions;"> <div style="grid-area: left-pane; display: flex; flex-direction: column;" class="hide-toggle">
<div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;"> <div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action" <a id="create-top-level-note-button" title="Create new top level note" class="icon-action"
style="background: url('/images/icons/file-plus.png')"></a> style="background: url('/images/icons/file-plus.png')"></a>
@@ -54,28 +54,28 @@
</div> </div>
<input type="file" id="import-upload" style="display: none" /> <input type="file" id="import-upload" style="display: none" />
</div>
<div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;"> <div id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off"> <input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
<button id="do-search-button" class="btn btn-primary btn-sm" title="Search">Search</button> <button id="do-search-button" class="btn btn-primary btn-sm" title="Search">Search</button>
</div>
<div style="display: flex; align-items: center; justify-content: space-evenly; margin-top: 10px;">
<button id="reset-search-button" class="btn btn-sm" title="Reset search">Reset search</button>
<button id="save-search-button" class="btn btn-sm" title="Save search">Save search</button>
</div>
</div> </div>
<div style="display: flex; align-items: center; justify-content: space-evenly; margin-top: 10px;"> <div id="tree" class="hide-toggle" style="overflow: auto; flex-grow: 100; flex-shrink: 100; margin-top: 10px;">
<button id="reset-search-button" class="btn btn-sm" title="Reset search">Reset search</button>
<button id="save-search-button" class="btn btn-sm" title="Save search">Save search</button>
</div> </div>
</div>
<div id="tree" class="hide-toggle" style="grid-area: tree; overflow: auto;"> <div id="parent-list">
</div> <p><strong>Note locations:</strong></p>
<div id="parent-list" class="hide-toggle"> <ul id="parent-list-inner"></ul>
<p><strong>Note locations:</strong></p> </div>
<ul id="parent-list-inner"></ul>
</div> </div>
<div class="hide-toggle" style="grid-area: title;"> <div class="hide-toggle" style="grid-area: title;">
@@ -132,82 +132,86 @@
</div> </div>
</div> </div>
<div style="position: relative; overflow: auto; grid-area: note-content; padding-left: 10px; padding-top: 10px;" id="note-detail-wrapper"> <div id="note-detail-wrapper">
<div id="note-detail-text" class="note-detail-component"></div> <div id="note-detail-component-wrapper">
<div id="note-detail-text" class="note-detail-component" tabindex="2"></div>
<div id="note-detail-search" class="note-detail-component"> <div id="note-detail-search" class="note-detail-component">
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<strong>Search string: &nbsp; &nbsp;</strong> <strong>Search string: &nbsp; &nbsp;</strong>
<textarea rows="4" cols="50" id="search-string"></textarea> <textarea rows="4" cols="50" id="search-string"></textarea>
</div>
<br />
<h4>Help</h4>
<p>
<ul>
<li>
<code>@abc</code> - matches notes with label abc</li>
<li>
<code>@!abc</code> - matches notes without abc label (maybe not the best syntax)</li>
<li>
<code>@abc=true</code> - matches notes with label abc having value true</li>
<li><code>@abc!=true</code></li>
<li>
<code>@"weird label"="weird value"</code> - works also with whitespace inside names values</li>
<li>
<code>@abc and @def</code> - matches notes with both abc and def</li>
<li>
<code>@abc @def</code> - AND relation is implicit when specifying multiple labels</li>
<li>
<code>@abc or @def</code> - OR relation</li>
<li>
<code>@abc&lt;=5</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
<li>
<code>some search string @abc @def</code> - combination of fulltext and label search - both of them need to match (OR not supported)</li>
<li>
<code>@abc @def some search string</code> - same combination</li>
</ul>
<a href="https://github.com/zadam/trilium/wiki/Labels">Complete help on search syntax</a>
</p>
</div> </div>
<br /> <div id="note-detail-code" class="note-detail-component"></div>
<h4>Help</h4> <div id="note-detail-render" class="note-detail-component"></div>
<p>
<ul>
<li>
<code>@abc</code> - matches notes with label abc</li>
<li>
<code>@!abc</code> - matches notes without abc label (maybe not the best syntax)</li>
<li>
<code>@abc=true</code> - matches notes with label abc having value true</li>
<li><code>@abc!=true</code></li>
<li>
<code>@"weird label"="weird value"</code> - works also with whitespace inside names values</li>
<li>
<code>@abc and @def</code> - matches notes with both abc and def</li>
<li>
<code>@abc @def</code> - AND relation is implicit when specifying multiple labels</li>
<li>
<code>@abc or @def</code> - OR relation</li>
<li>
<code>@abc&lt;=5</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
<li>
<code>some search string @abc @def</code> - combination of fulltext and label search - both of them need to match (OR not supported)</li>
<li>
<code>@abc @def some search string</code> - same combination</li>
</ul>
<a href="https://github.com/zadam/trilium/wiki/Labels">Complete help on search syntax</a> <div id="note-detail-file" class="note-detail-component">
</p> <table id="file-table">
<tr>
<th>File name:</th>
<td id="file-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="file-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="file-filesize"></td>
</tr>
<tr>
<td>
<button id="file-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="file-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div>
<input type="file" id="file-upload" style="display: none" />
</div> </div>
<div id="note-detail-code" class="note-detail-component"></div> <div id="children-overview"></div>
<div id="note-detail-render" class="note-detail-component"></div> <div id="label-list">
<button class="btn btn-sm show-labels-button">Labels:</button>
<div id="note-detail-file" class="note-detail-component"> <span id="label-list-inner"></span>
<table id="file-table">
<tr>
<th>File name:</th>
<td id="file-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="file-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="file-filesize"></td>
</tr>
<tr>
<td>
<button id="file-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="file-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div> </div>
<input type="file" id="file-upload" style="display: none" />
</div>
<div id="label-list">
<button class="btn btn-sm show-labels-button">Labels:</button>
<span id="label-list-inner"></span>
</div> </div>
</div> </div>