mirror of
https://github.com/zadam/trilium.git
synced 2025-10-29 09:16:45 +01:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c9ac488e8 | ||
|
|
fec1574447 | ||
|
|
f7587de452 | ||
|
|
41a6e777ea | ||
|
|
8fb0de900b | ||
|
|
a40bf71fd4 | ||
|
|
2a53bb03ae | ||
|
|
a684879b91 | ||
|
|
ddbd4f73c8 | ||
|
|
b0ed790edf | ||
|
|
3424406ff1 | ||
|
|
ce5c385c15 | ||
|
|
cd9eef32b0 | ||
|
|
12d82e3b33 | ||
|
|
f071d3f651 | ||
|
|
297b536ebc | ||
|
|
7cca2d9247 | ||
|
|
36dc802d16 | ||
|
|
c78ddb70cb | ||
|
|
9fb0599c45 | ||
|
|
13f524fb39 | ||
|
|
27be3b4c90 | ||
|
|
af4ea66742 | ||
|
|
0f42c396f3 | ||
|
|
9e96272eb3 | ||
|
|
965dbcbc9a | ||
|
|
7ac109e7f7 | ||
|
|
ac25770c0e | ||
|
|
5b15424498 | ||
|
|
f1240c26bf | ||
|
|
1c0fd243d1 | ||
|
|
3491235533 | ||
|
|
5f36856571 | ||
|
|
d3e44b37e9 | ||
|
|
90e9297ec5 | ||
|
|
c568ef2f8a | ||
|
|
fcf6141cde | ||
|
|
21551d7b77 | ||
|
|
12031d369f | ||
|
|
b44c523845 | ||
|
|
49989695ff | ||
|
|
a55d3530e9 | ||
|
|
2aab3ad281 | ||
|
|
194ce4f10f | ||
|
|
2089c32839 | ||
|
|
f437be7af0 | ||
|
|
96dc56098d | ||
|
|
61987e46f7 | ||
|
|
509093b755 | ||
|
|
097114c0f2 | ||
|
|
040f9185f8 | ||
|
|
6dc934abbe | ||
|
|
2d24bf81dd | ||
|
|
9452fc236b | ||
|
|
365c37604b | ||
|
|
01c7e58d47 | ||
|
|
d3d49923b1 | ||
|
|
263ac299d0 |
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<dataSource name="document.db">
|
||||
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.9">
|
||||
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.11">
|
||||
<root id="1">
|
||||
<ServerVersion>3.16.1</ServerVersion>
|
||||
</root>
|
||||
@@ -12,727 +12,638 @@
|
||||
<collation id="4" parent="1" name="NOCASE"/>
|
||||
<collation id="5" parent="1" name="RTRIM"/>
|
||||
<table id="6" parent="2" name="api_tokens"/>
|
||||
<table id="7" parent="2" name="branches"/>
|
||||
<table id="8" parent="2" name="event_log"/>
|
||||
<table id="9" parent="2" name="images"/>
|
||||
<table id="10" parent="2" name="labels"/>
|
||||
<table id="7" parent="2" name="attributes"/>
|
||||
<table id="8" parent="2" name="branches"/>
|
||||
<table id="9" parent="2" name="event_log"/>
|
||||
<table id="10" parent="2" name="images"/>
|
||||
<table id="11" parent="2" name="note_images"/>
|
||||
<table id="12" parent="2" name="note_revisions"/>
|
||||
<table id="13" parent="2" name="notes"/>
|
||||
<table id="14" parent="2" name="options"/>
|
||||
<table id="15" parent="2" name="recent_notes"/>
|
||||
<table id="16" parent="2" name="relations"/>
|
||||
<table id="17" parent="2" name="source_ids"/>
|
||||
<table id="18" parent="2" name="sqlite_master">
|
||||
<table id="16" parent="2" name="source_ids"/>
|
||||
<table id="17" parent="2" name="sqlite_master">
|
||||
<System>1</System>
|
||||
</table>
|
||||
<table id="19" parent="2" name="sqlite_sequence">
|
||||
<table id="18" parent="2" name="sqlite_sequence">
|
||||
<System>1</System>
|
||||
</table>
|
||||
<table id="20" parent="2" name="sync"/>
|
||||
<column id="21" parent="6" name="apiTokenId">
|
||||
<table id="19" parent="2" name="sync"/>
|
||||
<column id="20" parent="6" name="apiTokenId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="22" parent="6" name="token">
|
||||
<column id="21" parent="6" name="token">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="23" parent="6" name="dateCreated">
|
||||
<column id="22" parent="6" name="dateCreated">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="24" parent="6" name="isDeleted">
|
||||
<column id="23" parent="6" name="isDeleted">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="25" parent="6" name="hash">
|
||||
<column id="24" parent="6" name="hash">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="26" parent="6" name="sqlite_autoindex_api_tokens_1">
|
||||
<index id="25" parent="6" name="sqlite_autoindex_api_tokens_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>apiTokenId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="27" parent="6">
|
||||
<key id="26" parent="6">
|
||||
<ColNames>apiTokenId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="28" parent="7" name="branchId">
|
||||
<column id="27" parent="7" name="attributeId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="29" parent="7" name="noteId">
|
||||
<column id="28" parent="7" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="30" parent="7" name="parentNoteId">
|
||||
<column id="29" parent="7" name="type">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="31" parent="7" name="notePosition">
|
||||
<column id="30" parent="7" name="name">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="31" parent="7" name="value">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="32" parent="7" name="position">
|
||||
<Position>6</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="33" parent="7" name="dateCreated">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="34" parent="7" name="dateModified">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="35" parent="7" name="isDeleted">
|
||||
<Position>9</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="36" parent="7" name="hash">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="37" parent="7" name="isInheritable">
|
||||
<Position>11</Position>
|
||||
<DataType>int|0s</DataType>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<index id="38" parent="7" name="sqlite_autoindex_attributes_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>attributeId</ColNames>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="39" parent="7">
|
||||
<ColNames>attributeId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_attributes_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="40" parent="8" name="branchId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="41" parent="8" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="42" parent="8" name="parentNoteId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="43" parent="8" name="notePosition">
|
||||
<Position>4</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="32" parent="7" name="prefix">
|
||||
<column id="44" parent="8" name="prefix">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="33" parent="7" name="isExpanded">
|
||||
<column id="45" parent="8" name="isExpanded">
|
||||
<Position>6</Position>
|
||||
<DataType>BOOLEAN|0s</DataType>
|
||||
</column>
|
||||
<column id="34" parent="7" name="isDeleted">
|
||||
<column id="46" parent="8" name="isDeleted">
|
||||
<Position>7</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="35" parent="7" name="dateModified">
|
||||
<column id="47" parent="8" name="dateModified">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="36" parent="7" name="hash">
|
||||
<column id="48" parent="8" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="37" parent="7" name="dateCreated">
|
||||
<column id="49" parent="8" name="dateCreated">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'1970-01-01T00:00:00.000Z'</DefaultExpression>
|
||||
</column>
|
||||
<index id="38" parent="7" name="sqlite_autoindex_branches_1">
|
||||
<index id="50" parent="8" name="sqlite_autoindex_branches_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>branchId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="39" parent="7" name="IDX_branches_noteId_parentNoteId">
|
||||
<index id="51" parent="8" name="IDX_branches_noteId_parentNoteId">
|
||||
<ColNames>noteId
|
||||
parentNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="40" parent="7" name="IDX_branches_noteId">
|
||||
<index id="52" parent="8" name="IDX_branches_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="41" parent="7" name="IDX_branches_parentNoteId">
|
||||
<index id="53" parent="8" name="IDX_branches_parentNoteId">
|
||||
<ColNames>parentNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="42" parent="7">
|
||||
<key id="54" parent="8">
|
||||
<ColNames>branchId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="43" parent="8" name="eventId">
|
||||
<column id="55" parent="9" name="eventId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="44" parent="8" name="noteId">
|
||||
<column id="56" parent="9" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="45" parent="8" name="comment">
|
||||
<column id="57" parent="9" name="comment">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="46" parent="8" name="dateCreated">
|
||||
<column id="58" parent="9" name="dateCreated">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="47" parent="8" name="sqlite_autoindex_event_log_1">
|
||||
<index id="59" parent="9" name="sqlite_autoindex_event_log_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>eventId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="48" parent="8">
|
||||
<key id="60" parent="9">
|
||||
<ColNames>eventId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_event_log_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="49" parent="9" name="imageId">
|
||||
<column id="61" parent="10" name="imageId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="50" parent="9" name="format">
|
||||
<column id="62" parent="10" name="format">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="51" parent="9" name="checksum">
|
||||
<column id="63" parent="10" name="checksum">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="52" parent="9" name="name">
|
||||
<column id="64" parent="10" name="name">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="53" parent="9" name="data">
|
||||
<column id="65" parent="10" name="data">
|
||||
<Position>5</Position>
|
||||
<DataType>BLOB|0s</DataType>
|
||||
</column>
|
||||
<column id="54" parent="9" name="isDeleted">
|
||||
<column id="66" parent="10" name="isDeleted">
|
||||
<Position>6</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="55" parent="9" name="dateModified">
|
||||
<column id="67" parent="10" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="56" parent="9" name="dateCreated">
|
||||
<column id="68" parent="10" name="dateCreated">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="57" parent="9" name="hash">
|
||||
<column id="69" parent="10" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="58" parent="9" name="sqlite_autoindex_images_1">
|
||||
<index id="70" parent="10" name="sqlite_autoindex_images_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="59" parent="9">
|
||||
<key id="71" parent="10">
|
||||
<ColNames>imageId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="60" parent="10" name="labelId">
|
||||
<column id="72" parent="11" name="noteImageId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="61" parent="10" name="noteId">
|
||||
<column id="73" parent="11" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="62" parent="10" name="name">
|
||||
<column id="74" parent="11" name="imageId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="63" parent="10" name="value">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="64" parent="10" name="position">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="65" parent="10" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="66" parent="10" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="67" parent="10" name="isDeleted">
|
||||
<Position>8</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="68" parent="10" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="69" parent="10" name="sqlite_autoindex_labels_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>labelId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="70" parent="10" name="IDX_labels_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="71" parent="10" name="IDX_labels_name_value">
|
||||
<ColNames>name
|
||||
value</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="72" parent="10">
|
||||
<ColNames>labelId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="73" parent="11" name="noteImageId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="74" parent="11" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="75" parent="11" name="imageId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="76" parent="11" name="isDeleted">
|
||||
<column id="75" parent="11" name="isDeleted">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="77" parent="11" name="dateModified">
|
||||
<column id="76" parent="11" name="dateModified">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="78" parent="11" name="dateCreated">
|
||||
<column id="77" parent="11" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="79" parent="11" name="hash">
|
||||
<column id="78" parent="11" name="hash">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="80" parent="11" name="sqlite_autoindex_note_images_1">
|
||||
<index id="79" parent="11" name="sqlite_autoindex_note_images_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteImageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="81" parent="11" name="IDX_note_images_noteId_imageId">
|
||||
<index id="80" parent="11" name="IDX_note_images_noteId_imageId">
|
||||
<ColNames>noteId
|
||||
imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="82" parent="11" name="IDX_note_images_noteId">
|
||||
<index id="81" parent="11" name="IDX_note_images_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="83" parent="11" name="IDX_note_images_imageId">
|
||||
<index id="82" parent="11" name="IDX_note_images_imageId">
|
||||
<ColNames>imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="84" parent="11">
|
||||
<key id="83" parent="11">
|
||||
<ColNames>noteImageId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="85" parent="12" name="noteRevisionId">
|
||||
<column id="84" parent="12" name="noteRevisionId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="86" parent="12" name="noteId">
|
||||
<column id="85" parent="12" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="87" parent="12" name="title">
|
||||
<column id="86" parent="12" name="title">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="88" parent="12" name="content">
|
||||
<column id="87" parent="12" name="content">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="89" parent="12" name="isProtected">
|
||||
<column id="88" parent="12" name="isProtected">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="90" parent="12" name="dateModifiedFrom">
|
||||
<column id="89" parent="12" name="dateModifiedFrom">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="91" parent="12" name="dateModifiedTo">
|
||||
<column id="90" parent="12" name="dateModifiedTo">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="92" parent="12" name="type">
|
||||
<column id="91" parent="12" name="type">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="93" parent="12" name="mime">
|
||||
<column id="92" parent="12" name="mime">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="94" parent="12" name="hash">
|
||||
<column id="93" parent="12" name="hash">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="95" parent="12" name="sqlite_autoindex_note_revisions_1">
|
||||
<index id="94" parent="12" name="sqlite_autoindex_note_revisions_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteRevisionId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="96" parent="12" name="IDX_note_revisions_noteId">
|
||||
<index id="95" parent="12" name="IDX_note_revisions_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="97" parent="12" name="IDX_note_revisions_dateModifiedFrom">
|
||||
<index id="96" parent="12" name="IDX_note_revisions_dateModifiedFrom">
|
||||
<ColNames>dateModifiedFrom</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="98" parent="12" name="IDX_note_revisions_dateModifiedTo">
|
||||
<index id="97" parent="12" name="IDX_note_revisions_dateModifiedTo">
|
||||
<ColNames>dateModifiedTo</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="99" parent="12">
|
||||
<key id="98" parent="12">
|
||||
<ColNames>noteRevisionId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="100" parent="13" name="noteId">
|
||||
<column id="99" parent="13" name="noteId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="101" parent="13" name="title">
|
||||
<column id="100" parent="13" name="title">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>"unnamed"</DefaultExpression>
|
||||
</column>
|
||||
<column id="102" parent="13" name="content">
|
||||
<column id="101" parent="13" name="content">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="103" parent="13" name="isProtected">
|
||||
<column id="102" parent="13" name="isProtected">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="104" parent="13" name="isDeleted">
|
||||
<column id="103" parent="13" name="isDeleted">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="105" parent="13" name="dateCreated">
|
||||
<column id="104" parent="13" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="106" parent="13" name="dateModified">
|
||||
<column id="105" parent="13" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="107" parent="13" name="type">
|
||||
<column id="106" parent="13" name="type">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'text'</DefaultExpression>
|
||||
</column>
|
||||
<column id="108" parent="13" name="mime">
|
||||
<column id="107" parent="13" name="mime">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'text/html'</DefaultExpression>
|
||||
</column>
|
||||
<column id="109" parent="13" name="hash">
|
||||
<column id="108" parent="13" name="hash">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="110" parent="13" name="sqlite_autoindex_notes_1">
|
||||
<index id="109" parent="13" name="sqlite_autoindex_notes_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="111" parent="13" name="IDX_notes_type">
|
||||
<index id="110" parent="13" name="IDX_notes_type">
|
||||
<ColNames>type</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="112" parent="13">
|
||||
<key id="111" parent="13">
|
||||
<ColNames>noteId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="113" parent="14" name="name">
|
||||
<column id="112" parent="14" name="name">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="114" parent="14" name="value">
|
||||
<column id="113" parent="14" name="value">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="115" parent="14" name="dateModified">
|
||||
<column id="114" parent="14" name="dateModified">
|
||||
<Position>3</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
</column>
|
||||
<column id="116" parent="14" name="isSynced">
|
||||
<column id="115" parent="14" name="isSynced">
|
||||
<Position>4</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="117" parent="14" name="hash">
|
||||
<column id="116" parent="14" name="hash">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="118" parent="14" name="dateCreated">
|
||||
<column id="117" parent="14" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'1970-01-01T00:00:00.000Z'</DefaultExpression>
|
||||
</column>
|
||||
<index id="119" parent="14" name="sqlite_autoindex_options_1">
|
||||
<index id="118" parent="14" name="sqlite_autoindex_options_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>name</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="120" parent="14">
|
||||
<key id="119" parent="14">
|
||||
<ColNames>name</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="121" parent="15" name="branchId">
|
||||
<column id="120" parent="15" name="branchId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="122" parent="15" name="notePath">
|
||||
<column id="121" parent="15" name="notePath">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="123" parent="15" name="hash">
|
||||
<column id="122" parent="15" name="hash">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="124" parent="15" name="dateCreated">
|
||||
<column id="123" parent="15" name="dateCreated">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="125" parent="15" name="isDeleted">
|
||||
<column id="124" parent="15" name="isDeleted">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
</column>
|
||||
<index id="126" parent="15" name="sqlite_autoindex_recent_notes_1">
|
||||
<index id="125" parent="15" name="sqlite_autoindex_recent_notes_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>branchId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="127" parent="15">
|
||||
<key id="126" parent="15">
|
||||
<ColNames>branchId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="128" parent="16" name="relationId">
|
||||
<column id="127" parent="16" name="sourceId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="129" parent="16" name="sourceNoteId">
|
||||
<column id="128" parent="16" name="dateCreated">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="130" parent="16" name="name">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="131" parent="16" name="targetNoteId">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="132" parent="16" name="position">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="133" parent="16" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="134" parent="16" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="135" parent="16" name="isDeleted">
|
||||
<Position>8</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="136" parent="16" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="137" parent="16" name="sqlite_autoindex_relations_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>relationId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="138" parent="16" name="IDX_relation_sourceNoteId">
|
||||
<ColNames>sourceNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="139" parent="16" name="IDX_relation_targetNoteId">
|
||||
<ColNames>targetNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="140" parent="16">
|
||||
<ColNames>relationId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_relations_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="141" parent="17" name="sourceId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="142" parent="17" name="dateCreated">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="143" parent="17" name="sqlite_autoindex_source_ids_1">
|
||||
<index id="129" parent="16" name="sqlite_autoindex_source_ids_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>sourceId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="144" parent="17">
|
||||
<key id="130" parent="16">
|
||||
<ColNames>sourceId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="145" parent="18" name="type">
|
||||
<column id="131" parent="17" name="type">
|
||||
<Position>1</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="146" parent="18" name="name">
|
||||
<column id="132" parent="17" name="name">
|
||||
<Position>2</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="147" parent="18" name="tbl_name">
|
||||
<column id="133" parent="17" name="tbl_name">
|
||||
<Position>3</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="148" parent="18" name="rootpage">
|
||||
<column id="134" parent="17" name="rootpage">
|
||||
<Position>4</Position>
|
||||
<DataType>integer|0s</DataType>
|
||||
</column>
|
||||
<column id="149" parent="18" name="sql">
|
||||
<column id="135" parent="17" name="sql">
|
||||
<Position>5</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="150" parent="19" name="name">
|
||||
<column id="136" parent="18" name="name">
|
||||
<Position>1</Position>
|
||||
</column>
|
||||
<column id="151" parent="19" name="seq">
|
||||
<column id="137" parent="18" name="seq">
|
||||
<Position>2</Position>
|
||||
</column>
|
||||
<column id="152" parent="20" name="id">
|
||||
<column id="138" parent="19" name="id">
|
||||
<Position>1</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<SequenceIdentity>1</SequenceIdentity>
|
||||
</column>
|
||||
<column id="153" parent="20" name="entityName">
|
||||
<column id="139" parent="19" name="entityName">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="154" parent="20" name="entityId">
|
||||
<column id="140" parent="19" name="entityId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="155" parent="20" name="sourceId">
|
||||
<column id="141" parent="19" name="sourceId">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="156" parent="20" name="syncDate">
|
||||
<column id="142" parent="19" name="syncDate">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="157" parent="20" name="IDX_sync_entityName_entityId">
|
||||
<index id="143" parent="19" name="IDX_sync_entityName_entityId">
|
||||
<ColNames>entityName
|
||||
entityId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="158" parent="20" name="IDX_sync_syncDate">
|
||||
<index id="144" parent="19" name="IDX_sync_syncDate">
|
||||
<ColNames>syncDate</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="159" parent="20">
|
||||
<key id="145" parent="19">
|
||||
<ColNames>id</ColNames>
|
||||
<Primary>1</Primary>
|
||||
</key>
|
||||
|
||||
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="PROJECT" dialect="SQLite" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,5 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ $# -eq 0 ]] ; then
|
||||
echo "Missing argument of new version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$1
|
||||
|
||||
PKG_DIR=dist/trilium-linux-x64-server
|
||||
|
||||
mkdir $PKG_DIR
|
||||
@@ -13,4 +20,4 @@ cp node_modules/scrypt/build/Release/scrypt.node ${PKG_DIR}/
|
||||
|
||||
cd dist
|
||||
|
||||
7z a trilium-linux-x64-server.7z trilium-linux-x64-server
|
||||
7z a trilium-linux-x64-${VERSION}-server.7z trilium-linux-x64-server
|
||||
0
bin/push-docker-image.sh
Normal file → Executable file
0
bin/push-docker-image.sh
Normal file → Executable file
@@ -78,7 +78,7 @@ github-release upload \
|
||||
|
||||
echo "Packaging server version"
|
||||
|
||||
npm run build-pkg
|
||||
bin/build-pkg.sh $VERSION
|
||||
|
||||
github-release upload \
|
||||
--tag $TAG \
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
instanceName=
|
||||
|
||||
[Network]
|
||||
# port setting is relevant only for web deployments, desktop builds run on random free port
|
||||
port=8080
|
||||
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
|
||||
https=false
|
||||
|
||||
9
db/migrations/0108__new_backup_options.sql
Normal file
9
db/migrations/0108__new_backup_options.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
UPDATE options SET name = 'lastDailyBackupDate' WHERE name = 'lastBackupDate';
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('lastWeeklyBackupDate', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', 0);
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('lastMonthlyBackupDate', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', '2018-07-29T18:31:00.874Z', 0);
|
||||
|
||||
-- these options are not synced so no need to fix sync rows
|
||||
27
db/migrations/0109__create_attributes.sql
Normal file
27
db/migrations/0109__create_attributes.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
create table attributes
|
||||
(
|
||||
attributeId TEXT not null primary key,
|
||||
noteId TEXT not null,
|
||||
type TEXT not null,
|
||||
name TEXT not null,
|
||||
value TEXT default '' not null,
|
||||
position INT default 0 not null,
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null,
|
||||
hash TEXT default "" not null);
|
||||
|
||||
create index IDX_attributes_name_value
|
||||
on labels (name, value);
|
||||
|
||||
create index IDX_attributes_value
|
||||
on labels (value);
|
||||
|
||||
create index IDX_attributes_noteId
|
||||
on labels (noteId);
|
||||
|
||||
INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash)
|
||||
SELECT labelId, noteId, 'label', name, value, position, dateCreated, dateModified, isDeleted, hash FROM labels;
|
||||
|
||||
INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash)
|
||||
SELECT relationId, sourceNoteId, 'relation', name, targetNoteId, position, dateCreated, dateModified, isDeleted, hash FROM relations;
|
||||
1
db/migrations/0110__add_isInheritable_to_attributes.sql
Normal file
1
db/migrations/0110__add_isInheritable_to_attributes.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE attributes ADD isInheritable int DEFAULT 0 NULL;
|
||||
4
db/migrations/0111__cleanup_labels_and_relations.sql
Normal file
4
db/migrations/0111__cleanup_labels_and_relations.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
DROP TABLE relations;
|
||||
DROP TABLE labels;
|
||||
|
||||
DELETE FROM sync WHERE entityName = 'relations' OR entityName = 'labels';
|
||||
@@ -82,21 +82,6 @@ CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
|
||||
`noteId`,
|
||||
`parentNoteId`
|
||||
);
|
||||
CREATE TABLE labels
|
||||
(
|
||||
labelId TEXT not null primary key,
|
||||
noteId TEXT not null,
|
||||
name TEXT not null,
|
||||
value TEXT default '' not null,
|
||||
position INT default 0 not null,
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX IDX_labels_name_value
|
||||
on labels (name, value);
|
||||
CREATE INDEX IDX_labels_noteId
|
||||
on labels (noteId);
|
||||
CREATE TABLE IF NOT EXISTS "notes" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`title` TEXT NOT NULL DEFAULT "unnamed",
|
||||
@@ -134,19 +119,15 @@ CREATE TABLE IF NOT EXISTS "options"
|
||||
hash TEXT default "" not null,
|
||||
dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
|
||||
);
|
||||
CREATE TABLE relations
|
||||
CREATE TABLE attributes
|
||||
(
|
||||
relationId TEXT not null primary key,
|
||||
sourceNoteId TEXT not null,
|
||||
name TEXT not null,
|
||||
targetNoteId TEXT not null,
|
||||
isInheritable int DEFAULT 0 NULL,
|
||||
position INT default 0 not null,
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX IDX_relation_sourceNoteId
|
||||
on relations (sourceNoteId);
|
||||
CREATE INDEX IDX_relation_targetNoteId
|
||||
on relations (targetNoteId);
|
||||
attributeId TEXT not null primary key,
|
||||
noteId TEXT not null,
|
||||
type TEXT not null,
|
||||
name TEXT not null,
|
||||
value TEXT default '' not null,
|
||||
position INT default 0 not null,
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null,
|
||||
hash TEXT default "" not null, isInheritable int DEFAULT 0 NULL);
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
const electron = require('electron');
|
||||
const path = require('path');
|
||||
const config = require('./src/services/config');
|
||||
const log = require('./src/services/log');
|
||||
const url = require("url");
|
||||
const port = require('./src/services/port');
|
||||
|
||||
const app = electron.app;
|
||||
const globalShortcut = electron.globalShortcut;
|
||||
@@ -23,7 +23,7 @@ function onClosed() {
|
||||
mainWindow = null;
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
async function createMainWindow() {
|
||||
const win = new electron.BrowserWindow({
|
||||
width: 1200,
|
||||
height: 900,
|
||||
@@ -31,10 +31,8 @@ function createMainWindow() {
|
||||
icon: path.join(__dirname, 'src/public/images/app-icons/png/256x256.png')
|
||||
});
|
||||
|
||||
const port = config['Network']['port'] || '3000';
|
||||
|
||||
win.setMenu(null);
|
||||
win.loadURL('http://localhost:' + port);
|
||||
win.loadURL('http://localhost:' + await port);
|
||||
win.on('closed', onClosed);
|
||||
|
||||
win.webContents.on('new-window', (e, url) => {
|
||||
|
||||
21
package.json
21
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -36,16 +36,17 @@
|
||||
"electron-in-page-search": "^1.3.2",
|
||||
"express": "~4.16.3",
|
||||
"express-session": "^1.15.6",
|
||||
"fs-extra": "^6.0.1",
|
||||
"helmet": "^3.12.1",
|
||||
"fs-extra": "^7.0.0",
|
||||
"get-port": "^4.0.0",
|
||||
"helmet": "^3.13.0",
|
||||
"html": "^1.0.0",
|
||||
"image-type": "^3.0.0",
|
||||
"imagemin": "^5.3.1",
|
||||
"imagemin": "^6.0.0",
|
||||
"imagemin-giflossy": "^5.1.10",
|
||||
"imagemin-mozjpeg": "^7.0.0",
|
||||
"imagemin-pngquant": "^5.1.0",
|
||||
"imagemin-pngquant": "^6.0.0",
|
||||
"ini": "^1.3.5",
|
||||
"jimp": "^0.2.28",
|
||||
"jimp": "^0.3.0",
|
||||
"moment": "^2.22.2",
|
||||
"multer": "^1.3.1",
|
||||
"open": "0.0.5",
|
||||
@@ -62,15 +63,15 @@
|
||||
"sqlite": "^2.9.2",
|
||||
"tar-stream": "^1.6.1",
|
||||
"unescape": "^1.0.1",
|
||||
"ws": "^5.2.1",
|
||||
"ws": "^6.0.0",
|
||||
"xml2js": "^0.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^2.0.4",
|
||||
"electron": "^2.0.6",
|
||||
"electron-compile": "^6.4.3",
|
||||
"electron-packager": "^12.1.0",
|
||||
"electron-prebuilt-compile": "2.0.4",
|
||||
"electron-rebuild": "^1.8.1",
|
||||
"electron-prebuilt-compile": "2.0.6",
|
||||
"electron-rebuild": "^1.8.2",
|
||||
"lorem-ipsum": "^1.0.5",
|
||||
"tape": "^4.9.1",
|
||||
"xo": "^0.21.1",
|
||||
|
||||
@@ -11,6 +11,7 @@ const os = require('os');
|
||||
const sessionSecret = require('./services/session_secret');
|
||||
const cls = require('./services/cls');
|
||||
require('./entities/entity_constructor');
|
||||
require('./services/handlers');
|
||||
|
||||
const app = express();
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ class ApiToken extends Entity {
|
||||
static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
@@ -18,6 +16,8 @@ class ApiToken extends Entity {
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
77
src/entities/attribute.js
Normal file
77
src/entities/attribute.js
Normal file
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const repository = require('../services/repository');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
const sql = require('../services/sql');
|
||||
|
||||
class Attribute extends Entity {
|
||||
static get tableName() { return "attributes"; }
|
||||
static get primaryKeyName() { return "attributeId"; }
|
||||
static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable", "isDeleted", "dateCreated"]; }
|
||||
|
||||
constructor(row) {
|
||||
super(row);
|
||||
|
||||
this.isInheritable = !!this.isInheritable;
|
||||
|
||||
if (this.isDefinition()) {
|
||||
try {
|
||||
this.value = JSON.parse(this.value);
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
|
||||
}
|
||||
|
||||
async getTargetNote() {
|
||||
if (this.type !== 'relation') {
|
||||
throw new Error(`Attribute ${this.attributeId} is not relation`);
|
||||
}
|
||||
|
||||
if (!this.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.value]);
|
||||
}
|
||||
|
||||
isDefinition() {
|
||||
return this.type === 'label-definition' || this.type === 'relation-definition';
|
||||
}
|
||||
|
||||
async beforeSaving() {
|
||||
if (!this.value) {
|
||||
// null value isn't allowed
|
||||
this.value = "";
|
||||
}
|
||||
|
||||
if (this.position === undefined) {
|
||||
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [this.noteId]);
|
||||
}
|
||||
|
||||
if (!this.isInheritable) {
|
||||
this.isInheritable = false;
|
||||
}
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Attribute;
|
||||
@@ -9,15 +9,13 @@ class Branch extends Entity {
|
||||
static get tableName() { return "branches"; }
|
||||
static get primaryKeyName() { return "branchId"; }
|
||||
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
|
||||
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "dateModified", "isDeleted", "prefix"]; }
|
||||
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "isDeleted", "prefix"]; }
|
||||
|
||||
async getNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
|
||||
}
|
||||
|
||||
async beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.notePosition === undefined) {
|
||||
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
|
||||
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 1;
|
||||
@@ -31,7 +29,11 @@ class Branch extends Entity {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ class Entity {
|
||||
for (const key in row) {
|
||||
this[key] = row[key];
|
||||
}
|
||||
|
||||
if ('isDeleted' in this) {
|
||||
this.isDeleted = !!this.isDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
@@ -14,13 +18,21 @@ class Entity {
|
||||
this[this.constructor.primaryKeyName] = utils.newEntityId();
|
||||
}
|
||||
|
||||
const origHash = this.hash;
|
||||
|
||||
this.hash = this.generateHash();
|
||||
|
||||
this.isChanged = origHash !== this.hash;
|
||||
}
|
||||
|
||||
generateHash() {
|
||||
let contentToHash = "";
|
||||
|
||||
for (const propertyName of this.constructor.hashedProperties) {
|
||||
contentToHash += "|" + this[propertyName];
|
||||
}
|
||||
|
||||
this["hash"] = utils.hash(contentToHash).substr(0, 10);
|
||||
return utils.hash(contentToHash).substr(0, 10);
|
||||
}
|
||||
|
||||
async save() {
|
||||
|
||||
@@ -3,21 +3,37 @@ const NoteRevision = require('../entities/note_revision');
|
||||
const Image = require('../entities/image');
|
||||
const NoteImage = require('../entities/note_image');
|
||||
const Branch = require('../entities/branch');
|
||||
const Label = require('../entities/label');
|
||||
const Relation = require('../entities/relation');
|
||||
const Attribute = require('../entities/attribute');
|
||||
const RecentNote = require('../entities/recent_note');
|
||||
const ApiToken = require('../entities/api_token');
|
||||
const Option = require('../entities/option');
|
||||
const repository = require('../services/repository');
|
||||
|
||||
const TABLE_NAME_TO_ENTITY = {
|
||||
"attributes": Attribute,
|
||||
"images": Image,
|
||||
"note_images": NoteImage,
|
||||
"branches": Branch,
|
||||
"notes": Note,
|
||||
"note_revisions": NoteRevision,
|
||||
"recent_notes": RecentNote,
|
||||
"options": Option,
|
||||
"api_tokens": ApiToken
|
||||
};
|
||||
|
||||
function getEntityFromTableName(tableName) {
|
||||
if (!(tableName in TABLE_NAME_TO_ENTITY)) {
|
||||
throw new Error(`Entity for table ${tableName} not found!`);
|
||||
}
|
||||
|
||||
return TABLE_NAME_TO_ENTITY[tableName];
|
||||
}
|
||||
|
||||
function createEntityFromRow(row) {
|
||||
let entity;
|
||||
|
||||
if (row.labelId) {
|
||||
entity = new Label(row);
|
||||
}
|
||||
else if (row.relationId) {
|
||||
entity = new Relation(row);
|
||||
if (row.attributeId) {
|
||||
entity = new Attribute(row);
|
||||
}
|
||||
else if (row.noteRevisionId) {
|
||||
entity = new NoteRevision(row);
|
||||
@@ -50,8 +66,9 @@ function createEntityFromRow(row) {
|
||||
return entity;
|
||||
}
|
||||
|
||||
repository.setEntityConstructor(createEntityFromRow);
|
||||
|
||||
module.exports = {
|
||||
createEntityFromRow
|
||||
};
|
||||
createEntityFromRow,
|
||||
getEntityFromTableName
|
||||
};
|
||||
|
||||
repository.setEntityConstructor(module.exports);
|
||||
|
||||
@@ -6,11 +6,9 @@ const dateUtils = require('../services/date_utils');
|
||||
class Image extends Entity {
|
||||
static get tableName() { return "images"; }
|
||||
static get primaryKeyName() { return "imageId"; }
|
||||
static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateModified", "dateCreated"]; }
|
||||
static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateCreated"]; }
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
@@ -19,7 +17,11 @@ class Image extends Entity {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const repository = require('../services/repository');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
const sql = require('../services/sql');
|
||||
|
||||
class Label extends Entity {
|
||||
static get tableName() { return "labels"; }
|
||||
static get primaryKeyName() { return "labelId"; }
|
||||
static get hashedProperties() { return ["labelId", "noteId", "name", "value", "dateModified", "dateCreated"]; }
|
||||
|
||||
async getNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
|
||||
}
|
||||
|
||||
async beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (!this.value) {
|
||||
// null value isn't allowed
|
||||
this.value = "";
|
||||
}
|
||||
|
||||
if (this.position === undefined) {
|
||||
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM labels WHERE noteId = ?`, [this.noteId]);
|
||||
}
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Label;
|
||||
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const Attribute = require('./attribute');
|
||||
const protectedSessionService = require('../services/protected_session');
|
||||
const repository = require('../services/repository');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
@@ -8,11 +9,13 @@ const dateUtils = require('../services/date_utils');
|
||||
class Note extends Entity {
|
||||
static get tableName() { return "notes"; }
|
||||
static get primaryKeyName() { return "noteId"; }
|
||||
static get hashedProperties() { return ["noteId", "title", "content", "type", "dateModified", "isProtected", "isDeleted"]; }
|
||||
static get hashedProperties() { return ["noteId", "title", "content", "type", "isProtected", "isDeleted"]; }
|
||||
|
||||
constructor(row) {
|
||||
super(row);
|
||||
|
||||
this.isProtected = !!this.isProtected;
|
||||
|
||||
// check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
|
||||
if (this.isProtected && this.noteId) {
|
||||
protectedSessionService.decryptNote(this);
|
||||
@@ -30,6 +33,10 @@ class Note extends Entity {
|
||||
catch(e) {}
|
||||
}
|
||||
|
||||
isRoot() {
|
||||
return this.noteId === 'root';
|
||||
}
|
||||
|
||||
isJson() {
|
||||
return this.mime === "application/json";
|
||||
}
|
||||
@@ -59,30 +66,140 @@ class Note extends Entity {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getLabels() {
|
||||
return await repository.getEntities("SELECT * FROM labels WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
|
||||
async getOwnedAttributes() {
|
||||
return await repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
|
||||
}
|
||||
|
||||
// WARNING: this doesn't take into account the possibility to have multi-valued labels!
|
||||
async getLabelMap() {
|
||||
const map = {};
|
||||
|
||||
for (const label of await this.getLabels()) {
|
||||
map[label.name] = label.value;
|
||||
async getAttributes() {
|
||||
if (!this.__attributeCache) {
|
||||
await this.loadAttributesToCache();
|
||||
}
|
||||
|
||||
return map;
|
||||
return this.__attributeCache;
|
||||
}
|
||||
|
||||
invalidateAttributeCache() {
|
||||
this.__attributeCache = null;
|
||||
}
|
||||
|
||||
async loadAttributesToCache() {
|
||||
const attributes = await repository.getEntities(`
|
||||
WITH RECURSIVE
|
||||
tree(noteId, level) AS (
|
||||
SELECT ?, 0
|
||||
UNION
|
||||
SELECT branches.parentNoteId, tree.level + 1 FROM branches
|
||||
JOIN tree ON branches.noteId = tree.noteId
|
||||
JOIN notes ON notes.noteId = branches.parentNoteId
|
||||
WHERE notes.isDeleted = 0
|
||||
AND branches.isDeleted = 0
|
||||
),
|
||||
treeWithAttrs(noteId, level) AS (
|
||||
SELECT * FROM tree
|
||||
UNION
|
||||
SELECT attributes.value, treeWithAttrs.level + 1 FROM attributes
|
||||
JOIN treeWithAttrs ON treeWithAttrs.noteId = attributes.noteId
|
||||
WHERE attributes.isDeleted = 0
|
||||
AND attributes.type = 'relation'
|
||||
AND attributes.name = 'inheritAttributes'
|
||||
AND (attributes.noteId = ? OR attributes.isInheritable = 1)
|
||||
)
|
||||
SELECT attributes.* FROM attributes JOIN treeWithAttrs ON attributes.noteId = treeWithAttrs.noteId
|
||||
WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR attributes.noteId = ?)
|
||||
ORDER BY level, noteId, position`, [this.noteId, this.noteId, this.noteId]);
|
||||
// attributes are ordered so that "closest" attributes are first
|
||||
// we order by noteId so that attributes from same note stay together. Actual noteId ordering doesn't matter.
|
||||
|
||||
const filteredAttributes = attributes.filter((attr, index) => {
|
||||
if (attr.isDefinition()) {
|
||||
const firstDefinitionIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name);
|
||||
|
||||
// keep only if this element is the first definition for this type & name
|
||||
return firstDefinitionIndex === index;
|
||||
}
|
||||
else {
|
||||
const definitionAttr = attributes.find(el => el.type === attr.type + '-definition' && el.name === attr.name);
|
||||
|
||||
if (!definitionAttr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const definition = definitionAttr.value;
|
||||
|
||||
if (definition.multiplicityType === 'multivalue') {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
const firstAttrIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name);
|
||||
|
||||
// in case of single-valued attribute we'll keep it only if it's first (closest)
|
||||
return firstAttrIndex === index;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const attr of filteredAttributes) {
|
||||
attr.isOwned = attr.noteId === this.noteId;
|
||||
}
|
||||
|
||||
this.__attributeCache = filteredAttributes;
|
||||
}
|
||||
|
||||
async hasLabel(name) {
|
||||
const map = await this.getLabelMap();
|
||||
|
||||
return map.hasOwnProperty(name);
|
||||
return !!await this.getLabel(name);
|
||||
}
|
||||
|
||||
// WARNING: this doesn't take into account the possibility to have multi-valued labels!
|
||||
async getLabel(name) {
|
||||
return await repository.getEntity("SELECT * FROM labels WHERE noteId = ? AND name = ?", [this.noteId, name]);
|
||||
const attributes = await this.getAttributes();
|
||||
|
||||
return attributes.find(attr => attr.type === 'label' && attr.name === name);
|
||||
}
|
||||
|
||||
async getLabelValue(name) {
|
||||
const label = await this.getLabel(name);
|
||||
|
||||
return label ? label.value : null;
|
||||
}
|
||||
|
||||
async toggleLabel(enabled, name, value = "") {
|
||||
if (enabled) {
|
||||
await this.setLabel(name, value);
|
||||
}
|
||||
else {
|
||||
await this.removeLabel(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
async setLabel(name, value = "") {
|
||||
const attributes = await this.getOwnedAttributes();
|
||||
let label = attributes.find(attr => attr.type === 'label' && attr.value === value);
|
||||
|
||||
if (!label) {
|
||||
label = new Attribute({
|
||||
noteId: this.noteId,
|
||||
type: 'label',
|
||||
name: name,
|
||||
value: value
|
||||
});
|
||||
|
||||
await label.save();
|
||||
|
||||
this.invalidateAttributeCache();
|
||||
}
|
||||
}
|
||||
|
||||
async removeLabel(name, value = "") {
|
||||
const attributes = await this.getOwnedAttributes();
|
||||
|
||||
for (const attribute of attributes) {
|
||||
if (attribute.type === 'label' && (!value || value === attribute.value)) {
|
||||
attribute.isDeleted = true;
|
||||
await attribute.save();
|
||||
|
||||
this.invalidateAttributeCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getRevisions() {
|
||||
@@ -140,8 +257,6 @@ class Note extends Entity {
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isJson() && this.jsonContent) {
|
||||
this.content = JSON.stringify(this.jsonContent, null, '\t');
|
||||
}
|
||||
@@ -158,7 +273,11 @@ class Note extends Entity {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const dateUtils = require('../services/date_utils');
|
||||
class NoteImage extends Entity {
|
||||
static get tableName() { return "note_images"; }
|
||||
static get primaryKeyName() { return "noteImageId"; }
|
||||
static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateModified", "dateCreated"]; }
|
||||
static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateCreated"]; }
|
||||
|
||||
async getNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
|
||||
@@ -18,8 +18,6 @@ class NoteImage extends Entity {
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
@@ -28,7 +26,11 @@ class NoteImage extends Entity {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ const repository = require('../services/repository');
|
||||
class NoteRevision extends Entity {
|
||||
static get tableName() { return "note_revisions"; }
|
||||
static get primaryKeyName() { return "noteRevisionId"; }
|
||||
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "dateModifiedFrom", "dateModifiedTo"]; }
|
||||
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "isProtected", "dateModifiedFrom", "dateModifiedTo"]; }
|
||||
|
||||
constructor(row) {
|
||||
super(row);
|
||||
|
||||
this.isProtected = !!this.isProtected;
|
||||
|
||||
if (this.isProtected) {
|
||||
protectedSessionService.decryptNoteRevision(this);
|
||||
}
|
||||
@@ -22,11 +24,11 @@ class NoteRevision extends Entity {
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isProtected) {
|
||||
protectedSessionService.encryptNoteRevision(this);
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,18 @@ class Option extends Entity {
|
||||
static get primaryKeyName() { return "name"; }
|
||||
static get hashedProperties() { return ["name", "value"]; }
|
||||
|
||||
constructor(row) {
|
||||
super(row);
|
||||
|
||||
this.isSynced = !!this.isSynced;
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ class RecentNote extends Entity {
|
||||
static get hashedProperties() { return ["branchId", "notePath", "dateCreated", "isDeleted"]; }
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
@@ -18,6 +16,8 @@ class RecentNote extends Entity {
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const repository = require('../services/repository');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
const sql = require('../services/sql');
|
||||
|
||||
class Relation extends Entity {
|
||||
static get tableName() { return "relations"; }
|
||||
static get primaryKeyName() { return "relationId"; }
|
||||
static get hashedProperties() { return ["relationId", "sourceNoteId", "name", "targetNoteId", "isInheritable", "dateModified", "dateCreated"]; }
|
||||
|
||||
async getSourceNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.sourceNoteId]);
|
||||
}
|
||||
|
||||
async getTargetNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.targetNoteId]);
|
||||
}
|
||||
|
||||
async beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.position === undefined) {
|
||||
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM relations WHERE sourceNoteId = ?`, [this.sourceNoteId]);
|
||||
}
|
||||
|
||||
if (!this.isInheritable) {
|
||||
this.isInheritable = false;
|
||||
}
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Relation;
|
||||
318
src/public/javascripts/dialogs/attributes.js
Normal file
318
src/public/javascripts/dialogs/attributes.js
Normal file
@@ -0,0 +1,318 @@
|
||||
import noteDetailService from '../services/note_detail.js';
|
||||
import server from '../services/server.js';
|
||||
import infoService from "../services/info.js";
|
||||
import treeUtils from "../services/tree_utils.js";
|
||||
import linkService from "../services/link.js";
|
||||
|
||||
const $dialog = $("#attributes-dialog");
|
||||
const $saveAttributesButton = $("#save-attributes-button");
|
||||
const $ownedAttributesBody = $('#owned-attributes-table tbody');
|
||||
|
||||
const attributesModel = new AttributesModel();
|
||||
|
||||
function AttributesModel() {
|
||||
const self = this;
|
||||
|
||||
this.ownedAttributes = ko.observableArray();
|
||||
this.inheritedAttributes = ko.observableArray();
|
||||
|
||||
this.availableTypes = [
|
||||
{ text: "Label", value: "label" },
|
||||
{ text: "Label definition", value: "label-definition" },
|
||||
{ text: "Relation", value: "relation" },
|
||||
{ text: "Relation definition", value: "relation-definition" }
|
||||
];
|
||||
|
||||
this.availableLabelTypes = [
|
||||
{ text: "Text", value: "text" },
|
||||
{ text: "Number", value: "number" },
|
||||
{ text: "Boolean", value: "boolean" },
|
||||
{ text: "Date", value: "date" }
|
||||
];
|
||||
|
||||
this.multiplicityTypes = [
|
||||
{ text: "Single value", value: "singlevalue" },
|
||||
{ text: "Multi value", value: "multivalue" }
|
||||
];
|
||||
|
||||
this.typeChanged = function(data, event) {
|
||||
self.getTargetAttribute(event.target).valueHasMutated();
|
||||
};
|
||||
|
||||
this.labelTypeChanged = function(data, event) {
|
||||
self.getTargetAttribute(event.target).valueHasMutated();
|
||||
};
|
||||
|
||||
this.updateAttributePositions = function() {
|
||||
let position = 0;
|
||||
|
||||
// we need to update positions by searching in the DOM, because order of the
|
||||
// attributes in the viewmodel (self.ownedAttributes()) stays the same
|
||||
$ownedAttributesBody.find('input[name="position"]').each(function() {
|
||||
const attribute = self.getTargetAttribute(this);
|
||||
|
||||
attribute().position = position++;
|
||||
});
|
||||
};
|
||||
|
||||
async function showAttributes(attributes) {
|
||||
const ownedAttributes = attributes.filter(attr => attr.isOwned);
|
||||
|
||||
for (const attr of ownedAttributes) {
|
||||
attr.labelValue = attr.type === 'label' ? attr.value : '';
|
||||
attr.relationValue = attr.type === 'relation' ? (await treeUtils.getNoteTitle(attr.value) + " (" + attr.value + ")") : '';
|
||||
attr.labelDefinition = (attr.type === 'label-definition' && attr.value) ? attr.value : {
|
||||
labelType: "text",
|
||||
multiplicityType: "singlevalue",
|
||||
isPromoted: true
|
||||
};
|
||||
attr.relationDefinition = attr.type === ('relation-definition' && attr.value) ? attr.value : {
|
||||
multiplicityType: "singlevalue",
|
||||
isPromoted: true
|
||||
};
|
||||
|
||||
delete attr.value;
|
||||
}
|
||||
|
||||
self.ownedAttributes(ownedAttributes.map(ko.observable));
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
const inheritedAttributes = attributes.filter(attr => !attr.isOwned);
|
||||
|
||||
self.inheritedAttributes(inheritedAttributes);
|
||||
}
|
||||
|
||||
this.loadAttributes = async function() {
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const attributes = await server.get('notes/' + noteId + '/attributes');
|
||||
|
||||
await showAttributes(attributes);
|
||||
|
||||
// attribute might not be rendered immediatelly so could not focus
|
||||
setTimeout(() => $(".attribute-name:last").focus(), 100);
|
||||
|
||||
$ownedAttributesBody.sortable({
|
||||
handle: '.handle',
|
||||
containment: $ownedAttributesBody,
|
||||
update: this.updateAttributePositions
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteAttribute = function(data, event) {
|
||||
const attribute = self.getTargetAttribute(event.target);
|
||||
const attributeData = attribute();
|
||||
|
||||
if (attributeData) {
|
||||
attributeData.isDeleted = true;
|
||||
|
||||
attribute(attributeData);
|
||||
|
||||
addLastEmptyRow();
|
||||
}
|
||||
};
|
||||
|
||||
function isValid() {
|
||||
for (let attributes = self.ownedAttributes(), i = 0; i < attributes.length; i++) {
|
||||
if (self.isEmptyName(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.save = async function() {
|
||||
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
||||
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
|
||||
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||||
$saveAttributesButton.focus();
|
||||
|
||||
if (!isValid()) {
|
||||
alert("Please fix all validation errors and try saving again.");
|
||||
return;
|
||||
}
|
||||
|
||||
self.updateAttributePositions();
|
||||
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const attributesToSave = self.ownedAttributes()
|
||||
.map(attribute => attribute())
|
||||
.filter(attribute => attribute.attributeId !== "" || attribute.name !== "");
|
||||
|
||||
for (const attr of attributesToSave) {
|
||||
if (attr.type === 'label') {
|
||||
attr.value = attr.labelValue;
|
||||
}
|
||||
else if (attr.type === 'relation') {
|
||||
attr.value = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel(attr.relationValue));
|
||||
}
|
||||
else if (attr.type === 'label-definition') {
|
||||
attr.value = attr.labelDefinition;
|
||||
}
|
||||
else if (attr.type === 'relation-definition') {
|
||||
attr.value = attr.relationDefinition;
|
||||
}
|
||||
|
||||
delete attr.labelValue;
|
||||
delete attr.relationValue;
|
||||
delete attr.labelDefinition;
|
||||
delete attr.relationDefinition;
|
||||
}
|
||||
|
||||
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
|
||||
|
||||
await showAttributes(attributes);
|
||||
|
||||
infoService.showMessage("Attributes have been saved.");
|
||||
|
||||
noteDetailService.loadAttributes();
|
||||
};
|
||||
|
||||
function addLastEmptyRow() {
|
||||
const attributes = self.ownedAttributes().filter(attr => !attr().isDeleted);
|
||||
const last = attributes.length === 0 ? null : attributes[attributes.length - 1]();
|
||||
|
||||
if (!last || last.name.trim() !== "") {
|
||||
self.ownedAttributes.push(ko.observable({
|
||||
attributeId: '',
|
||||
type: 'label',
|
||||
name: '',
|
||||
labelValue: '',
|
||||
relationValue: '',
|
||||
isInheritable: false,
|
||||
isDeleted: false,
|
||||
position: 0,
|
||||
labelDefinition: {
|
||||
labelType: "text",
|
||||
multiplicityType: "singlevalue",
|
||||
isPromoted: true
|
||||
},
|
||||
relationDefinition: {
|
||||
multiplicityType: "singlevalue",
|
||||
isPromoted: true
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
this.attributeChanged = function (data, event) {
|
||||
addLastEmptyRow();
|
||||
|
||||
const attribute = self.getTargetAttribute(event.target);
|
||||
|
||||
attribute.valueHasMutated();
|
||||
};
|
||||
|
||||
this.isNotUnique = function(index) {
|
||||
const cur = self.ownedAttributes()[index]();
|
||||
|
||||
if (cur.name.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let attributes = self.ownedAttributes(), i = 0; i < attributes.length; i++) {
|
||||
const attribute = attributes[i]();
|
||||
|
||||
if (index !== i && cur.name === attribute.name && cur.type === attribute.type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.isEmptyName = function(index) {
|
||||
const cur = self.ownedAttributes()[index]();
|
||||
|
||||
return cur.name.trim() === "" && !cur.isDeleted && (cur.attributeId !== "" || cur.labelValue !== "" || cur.relationValue);
|
||||
};
|
||||
|
||||
this.getTargetAttribute = function(target) {
|
||||
const context = ko.contextFor(target);
|
||||
const index = context.$index();
|
||||
|
||||
return self.ownedAttributes()[index];
|
||||
}
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
await attributesModel.loadAttributes();
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 950,
|
||||
height: 500
|
||||
});
|
||||
}
|
||||
|
||||
ko.applyBindings(attributesModel, $dialog[0]);
|
||||
|
||||
$dialog.on('focus', '.attribute-name', function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
$(this).autocomplete({
|
||||
source: async (request, response) => {
|
||||
const attribute = attributesModel.getTargetAttribute(this);
|
||||
const type = (attribute().type === 'relation' || attribute().type === 'relation-definition') ? 'relation' : 'label';
|
||||
const names = await server.get('attributes/names/?type=' + type + '&query=' + encodeURIComponent(request.term));
|
||||
const result = names.map(name => {
|
||||
return {
|
||||
label: name,
|
||||
value: name
|
||||
}
|
||||
});
|
||||
|
||||
if (result.length > 0) {
|
||||
response(result);
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
label: "No results",
|
||||
value: "No results"
|
||||
}]);
|
||||
}
|
||||
},
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
$dialog.on('focus', '.label-value', async function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
const attributeName = $(this).parent().parent().find('.attribute-name').val();
|
||||
|
||||
if (attributeName.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName));
|
||||
|
||||
if (attributeValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: attributeValues.map(attribute => {
|
||||
return {
|
||||
attribute: attribute,
|
||||
value: attribute
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
export default {
|
||||
showDialog
|
||||
};
|
||||
@@ -1,222 +0,0 @@
|
||||
import noteDetailService from '../services/note_detail.js';
|
||||
import server from '../services/server.js';
|
||||
import infoService from "../services/info.js";
|
||||
|
||||
const $dialog = $("#labels-dialog");
|
||||
const $saveLabelsButton = $("#save-labels-button");
|
||||
const $labelsBody = $('#labels-table tbody');
|
||||
|
||||
const labelsModel = new LabelsModel();
|
||||
let labelNames = [];
|
||||
|
||||
function LabelsModel() {
|
||||
const self = this;
|
||||
|
||||
this.labels = ko.observableArray();
|
||||
|
||||
this.updateLabelPositions = function() {
|
||||
let position = 0;
|
||||
|
||||
// we need to update positions by searching in the DOM, because order of the
|
||||
// labels in the viewmodel (self.labels()) stays the same
|
||||
$labelsBody.find('input[name="position"]').each(function() {
|
||||
const label = self.getTargetLabel(this);
|
||||
|
||||
label().position = position++;
|
||||
});
|
||||
};
|
||||
|
||||
this.loadLabels = async function() {
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const labels = await server.get('notes/' + noteId + '/labels');
|
||||
|
||||
self.labels(labels.map(ko.observable));
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
labelNames = await server.get('labels/names');
|
||||
|
||||
// label might not be rendered immediatelly so could not focus
|
||||
setTimeout(() => $(".label-name:last").focus(), 100);
|
||||
|
||||
$labelsBody.sortable({
|
||||
handle: '.handle',
|
||||
containment: $labelsBody,
|
||||
update: this.updateLabelPositions
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteLabel = function(data, event) {
|
||||
const label = self.getTargetLabel(event.target);
|
||||
const labelData = label();
|
||||
|
||||
if (labelData) {
|
||||
labelData.isDeleted = 1;
|
||||
|
||||
label(labelData);
|
||||
|
||||
addLastEmptyRow();
|
||||
}
|
||||
};
|
||||
|
||||
function isValid() {
|
||||
for (let labels = self.labels(), i = 0; i < labels.length; i++) {
|
||||
if (self.isEmptyName(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.save = async function() {
|
||||
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
||||
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
|
||||
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||||
$saveLabelsButton.focus();
|
||||
|
||||
if (!isValid()) {
|
||||
alert("Please fix all validation errors and try saving again.");
|
||||
return;
|
||||
}
|
||||
|
||||
self.updateLabelPositions();
|
||||
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const labelsToSave = self.labels()
|
||||
.map(label => label())
|
||||
.filter(label => label.labelId !== "" || label.name !== "");
|
||||
|
||||
const labels = await server.put('notes/' + noteId + '/labels', labelsToSave);
|
||||
|
||||
self.labels(labels.map(ko.observable));
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
infoService.showMessage("Labels have been saved.");
|
||||
|
||||
noteDetailService.loadLabelList();
|
||||
};
|
||||
|
||||
function addLastEmptyRow() {
|
||||
const labels = self.labels().filter(attr => attr().isDeleted === 0);
|
||||
const last = labels.length === 0 ? null : labels[labels.length - 1]();
|
||||
|
||||
if (!last || last.name.trim() !== "" || last.value !== "") {
|
||||
self.labels.push(ko.observable({
|
||||
labelId: '',
|
||||
name: '',
|
||||
value: '',
|
||||
isDeleted: 0,
|
||||
position: 0
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
this.labelChanged = function (data, event) {
|
||||
addLastEmptyRow();
|
||||
|
||||
const label = self.getTargetLabel(event.target);
|
||||
|
||||
label.valueHasMutated();
|
||||
};
|
||||
|
||||
this.isNotUnique = function(index) {
|
||||
const cur = self.labels()[index]();
|
||||
|
||||
if (cur.name.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let labels = self.labels(), i = 0; i < labels.length; i++) {
|
||||
const label = labels[i]();
|
||||
|
||||
if (index !== i && cur.name === label.name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.isEmptyName = function(index) {
|
||||
const cur = self.labels()[index]();
|
||||
|
||||
return cur.name.trim() === "" && (cur.labelId !== "" || cur.value !== "");
|
||||
};
|
||||
|
||||
this.getTargetLabel = function(target) {
|
||||
const context = ko.contextFor(target);
|
||||
const index = context.$index();
|
||||
|
||||
return self.labels()[index];
|
||||
}
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
await labelsModel.loadLabels();
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 500
|
||||
});
|
||||
}
|
||||
|
||||
ko.applyBindings(labelsModel, $dialog[0]);
|
||||
|
||||
$dialog.on('focus', '.label-name', function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: labelNames.map(label => {
|
||||
return {
|
||||
label: label,
|
||||
value: label
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
$dialog.on('focus', '.label-value', async function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
const labelName = $(this).parent().parent().find('.label-name').val();
|
||||
|
||||
if (labelName.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelValues = await server.get('labels/values/' + encodeURIComponent(labelName));
|
||||
|
||||
if (labelValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: labelValues.map(label => {
|
||||
return {
|
||||
label: label,
|
||||
value: label
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
export default {
|
||||
showDialog
|
||||
};
|
||||
@@ -1,250 +0,0 @@
|
||||
import noteDetailService from '../services/note_detail.js';
|
||||
import server from '../services/server.js';
|
||||
import infoService from "../services/info.js";
|
||||
import linkService from "../services/link.js";
|
||||
import treeUtils from "../services/tree_utils.js";
|
||||
|
||||
const $dialog = $("#relations-dialog");
|
||||
const $saveRelationsButton = $("#save-relations-button");
|
||||
const $relationsBody = $('#relations-table tbody');
|
||||
|
||||
const relationsModel = new RelationsModel();
|
||||
let relationNames = [];
|
||||
|
||||
function RelationsModel() {
|
||||
const self = this;
|
||||
|
||||
this.relations = ko.observableArray();
|
||||
|
||||
this.updateRelationPositions = function() {
|
||||
let position = 0;
|
||||
|
||||
// we need to update positions by searching in the DOM, because order of the
|
||||
// relations in the viewmodel (self.relations()) stays the same
|
||||
$relationsBody.find('input[name="position"]').each(function() {
|
||||
const relation = self.getTargetRelation(this);
|
||||
|
||||
relation().position = position++;
|
||||
});
|
||||
};
|
||||
|
||||
async function showRelations(relations) {
|
||||
for (const relation of relations) {
|
||||
relation.targetNoteId = await treeUtils.getNoteTitle(relation.targetNoteId) + " (" + relation.targetNoteId + ")";
|
||||
}
|
||||
|
||||
self.relations(relations.map(ko.observable));
|
||||
}
|
||||
|
||||
this.loadRelations = async function() {
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const relations = await server.get('notes/' + noteId + '/relations');
|
||||
|
||||
await showRelations(relations);
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
relationNames = await server.get('relations/names');
|
||||
|
||||
// relation might not be rendered immediatelly so could not focus
|
||||
setTimeout(() => $(".relation-name:last").focus(), 100);
|
||||
|
||||
$relationsBody.sortable({
|
||||
handle: '.handle',
|
||||
containment: $relationsBody,
|
||||
update: this.updateRelationPositions
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteRelation = function(data, event) {
|
||||
const relation = self.getTargetRelation(event.target);
|
||||
const relationData = relation();
|
||||
|
||||
if (relationData) {
|
||||
relationData.isDeleted = 1;
|
||||
|
||||
relation(relationData);
|
||||
|
||||
addLastEmptyRow();
|
||||
}
|
||||
};
|
||||
|
||||
function isValid() {
|
||||
for (let relations = self.relations(), i = 0; i < relations.length; i++) {
|
||||
if (self.isEmptyName(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.save = async function() {
|
||||
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
||||
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
|
||||
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||||
$saveRelationsButton.focus();
|
||||
|
||||
if (!isValid()) {
|
||||
alert("Please fix all validation errors and try saving again.");
|
||||
return;
|
||||
}
|
||||
|
||||
self.updateRelationPositions();
|
||||
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const relationsToSave = self.relations()
|
||||
.map(relation => relation())
|
||||
.filter(relation => relation.relationId !== "" || relation.name !== "");
|
||||
|
||||
relationsToSave.forEach(relation => relation.targetNoteId = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel(relation.targetNoteId)));
|
||||
|
||||
console.log(relationsToSave);
|
||||
|
||||
const relations = await server.put('notes/' + noteId + '/relations', relationsToSave);
|
||||
|
||||
await showRelations(relations);
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
infoService.showMessage("Relations have been saved.");
|
||||
|
||||
noteDetailService.loadRelationList();
|
||||
};
|
||||
|
||||
function addLastEmptyRow() {
|
||||
const relations = self.relations().filter(attr => attr().isDeleted === 0);
|
||||
const last = relations.length === 0 ? null : relations[relations.length - 1]();
|
||||
|
||||
if (!last || last.name.trim() !== "" || last.targetNoteId !== "") {
|
||||
self.relations.push(ko.observable({
|
||||
relationId: '',
|
||||
name: '',
|
||||
targetNoteId: '',
|
||||
isInheritable: 0,
|
||||
isDeleted: 0,
|
||||
position: 0
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
this.relationChanged = function (data, event) {
|
||||
addLastEmptyRow();
|
||||
|
||||
const relation = self.getTargetRelation(event.target);
|
||||
|
||||
relation.valueHasMutated();
|
||||
};
|
||||
|
||||
this.isNotUnique = function(index) {
|
||||
const cur = self.relations()[index]();
|
||||
|
||||
if (cur.name.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let relations = self.relations(), i = 0; i < relations.length; i++) {
|
||||
const relation = relations[i]();
|
||||
|
||||
if (index !== i && cur.name === relation.name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.isEmptyName = function(index) {
|
||||
const cur = self.relations()[index]();
|
||||
|
||||
return cur.name.trim() === "" && (cur.relationId !== "" || cur.targetNoteId !== "");
|
||||
};
|
||||
|
||||
this.getTargetRelation = function(target) {
|
||||
const context = ko.contextFor(target);
|
||||
const index = context.$index();
|
||||
|
||||
return self.relations()[index];
|
||||
}
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
await relationsModel.loadRelations();
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 900,
|
||||
height: 500
|
||||
});
|
||||
}
|
||||
|
||||
ko.applyBindings(relationsModel, document.getElementById('relations-dialog'));
|
||||
|
||||
$dialog.on('focus', '.relation-name', function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: relationNames.map(relation => {
|
||||
return {
|
||||
label: relation,
|
||||
value: relation
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
async function initNoteAutocomplete($el) {
|
||||
if (!$el.hasClass("ui-autocomplete-input")) {
|
||||
await $el.autocomplete({
|
||||
source: async function (request, response) {
|
||||
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
|
||||
|
||||
if (result.length > 0) {
|
||||
response(result.map(row => {
|
||||
return {
|
||||
label: row.label,
|
||||
value: row.label + ' (' + row.value + ')'
|
||||
}
|
||||
}));
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
label: "No results",
|
||||
value: "No results"
|
||||
}]);
|
||||
}
|
||||
},
|
||||
minLength: 0,
|
||||
select: function (event, ui) {
|
||||
if (ui.item.value === 'No results') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$dialog.on('focus', '.relation-target-note-id', async function () {
|
||||
await initNoteAutocomplete($(this));
|
||||
});
|
||||
|
||||
$dialog.on('click', '.relations-show-recent-notes', async function () {
|
||||
const $autocomplete = $(this).parent().find('.relation-target-note-id');
|
||||
|
||||
await initNoteAutocomplete($autocomplete);
|
||||
|
||||
$autocomplete.autocomplete("search", "");
|
||||
});
|
||||
|
||||
export default {
|
||||
showDialog
|
||||
};
|
||||
@@ -7,6 +7,7 @@ class NoteShort {
|
||||
this.type = row.type;
|
||||
this.mime = row.mime;
|
||||
this.archived = row.archived;
|
||||
this.cssClass = row.cssClass;
|
||||
}
|
||||
|
||||
isJson() {
|
||||
|
||||
4
src/public/javascripts/services/bootstrap.js
vendored
4
src/public/javascripts/services/bootstrap.js
vendored
@@ -1,6 +1,6 @@
|
||||
import addLinkDialog from '../dialogs/add_link.js';
|
||||
import jumpToNoteDialog from '../dialogs/jump_to_note.js';
|
||||
import labelsDialog from '../dialogs/labels.js';
|
||||
import attributesDialog from '../dialogs/attributes.js';
|
||||
import noteRevisionsDialog from '../dialogs/note_revisions.js';
|
||||
import noteSourceDialog from '../dialogs/note_source.js';
|
||||
import recentChangesDialog from '../dialogs/recent_changes.js';
|
||||
@@ -35,6 +35,8 @@ import libraryLoader from "./library_loader.js";
|
||||
window.glob.getCurrentNode = treeService.getCurrentNode;
|
||||
window.glob.getHeaders = server.getHeaders;
|
||||
window.glob.showAddLinkDialog = addLinkDialog.showDialog;
|
||||
// this is required by CKEditor when uploading images
|
||||
window.glob.noteChanged = noteDetailService.noteChanged;
|
||||
|
||||
// required for ESLint plugin
|
||||
window.glob.getCurrentNote = noteDetailService.getCurrentNote;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import ScriptContext from "./script_context.js";
|
||||
import server from "./server.js";
|
||||
|
||||
async function getAndExecuteBundle(noteId, targetNote = null) {
|
||||
async function getAndExecuteBundle(noteId, originEntity = null) {
|
||||
const bundle = await server.get('script/bundle/' + noteId);
|
||||
|
||||
await executeBundle(bundle, targetNote);
|
||||
await executeBundle(bundle, originEntity);
|
||||
}
|
||||
|
||||
async function executeBundle(bundle, targetNote) {
|
||||
const apiContext = ScriptContext(bundle.note, bundle.allNotes, targetNote);
|
||||
async function executeBundle(bundle, originEntity) {
|
||||
const apiContext = ScriptContext(bundle.note, bundle.allNotes, originEntity);
|
||||
|
||||
return await (function () {
|
||||
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
|
||||
|
||||
@@ -11,8 +11,7 @@ import noteSourceDialog from "../dialogs/note_source.js";
|
||||
import recentChangesDialog from "../dialogs/recent_changes.js";
|
||||
import sqlConsoleDialog from "../dialogs/sql_console.js";
|
||||
import searchNotesService from "./search_notes.js";
|
||||
import labelsDialog from "../dialogs/labels.js";
|
||||
import relationsDialog from "../dialogs/relations.js";
|
||||
import attributesDialog from "../dialogs/attributes.js";
|
||||
import protectedSessionService from "./protected_session.js";
|
||||
|
||||
function registerEntrypoints() {
|
||||
@@ -38,11 +37,8 @@ function registerEntrypoints() {
|
||||
$("#toggle-search-button").click(searchNotesService.toggleSearch);
|
||||
utils.bindShortcut('ctrl+s', searchNotesService.toggleSearch);
|
||||
|
||||
$(".show-labels-button").click(labelsDialog.showDialog);
|
||||
utils.bindShortcut('alt+l', labelsDialog.showDialog);
|
||||
|
||||
$(".show-relations-button").click(relationsDialog.showDialog);
|
||||
utils.bindShortcut('alt+r', relationsDialog.showDialog);
|
||||
$(".show-attributes-button").click(attributesDialog.showDialog);
|
||||
utils.bindShortcut('alt+a', attributesDialog.showDialog);
|
||||
|
||||
$("#options-button").click(optionsDialog.showDialog);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ function getNotePathFromLabel(label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function createNoteLink(notePath, noteTitle) {
|
||||
async function createNoteLink(notePath, noteTitle = null) {
|
||||
if (!noteTitle) {
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
@@ -90,6 +90,18 @@ function addTextToEditor(text) {
|
||||
doc.enqueueChanges(() => editor.data.insertText(text), doc.selection);
|
||||
}
|
||||
|
||||
ko.bindingHandlers.noteLink = {
|
||||
init: async function(element, valueAccessor, allBindings, viewModel, bindingContext) {
|
||||
const noteId = ko.unwrap(valueAccessor());
|
||||
|
||||
if (noteId) {
|
||||
const link = await createNoteLink(noteId);
|
||||
|
||||
$(element).append(link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
|
||||
// of opening the link in new window/tab
|
||||
$(document).on('click', "a[action='note']", goToLink);
|
||||
|
||||
@@ -3,6 +3,7 @@ import infoService from "./info.js";
|
||||
|
||||
const $outstandingSyncsCount = $("#outstanding-syncs-count");
|
||||
|
||||
const syncMessageHandlers = [];
|
||||
const messageHandlers = [];
|
||||
|
||||
let ws;
|
||||
@@ -25,9 +26,17 @@ function subscribeToMessages(messageHandler) {
|
||||
messageHandlers.push(messageHandler);
|
||||
}
|
||||
|
||||
function subscribeToSyncMessages(messageHandler) {
|
||||
syncMessageHandlers.push(messageHandler);
|
||||
}
|
||||
|
||||
function handleMessage(event) {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
for (const messageHandler of messageHandlers) {
|
||||
messageHandler(message);
|
||||
}
|
||||
|
||||
if (message.type === 'sync') {
|
||||
lastPingTs = new Date().getTime();
|
||||
|
||||
@@ -39,8 +48,8 @@ function handleMessage(event) {
|
||||
|
||||
const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
|
||||
|
||||
for (const messageHandler of messageHandlers) {
|
||||
messageHandler(syncData);
|
||||
for (const syncMessageHandler of syncMessageHandlers) {
|
||||
syncMessageHandler(syncData);
|
||||
}
|
||||
|
||||
$outstandingSyncsCount.html(message.outstandingSyncs);
|
||||
@@ -73,26 +82,10 @@ setTimeout(() => {
|
||||
|
||||
lastSyncId = glob.maxSyncIdAtLoad;
|
||||
lastPingTs = new Date().getTime();
|
||||
let connectionBrokenNotification = null;
|
||||
|
||||
setInterval(async () => {
|
||||
if (new Date().getTime() - lastPingTs > 30000) {
|
||||
if (!connectionBrokenNotification) {
|
||||
connectionBrokenNotification = $.notify({
|
||||
// options
|
||||
message: "Lost connection to server"
|
||||
},{
|
||||
// options
|
||||
type: 'danger',
|
||||
delay: 100000000 // keep it until we explicitly close it
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (connectionBrokenNotification) {
|
||||
await connectionBrokenNotification.close();
|
||||
connectionBrokenNotification = null;
|
||||
|
||||
infoService.showMessage("Re-connected to server");
|
||||
console.log("Lost connection to server");
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
@@ -104,5 +97,6 @@ setTimeout(() => {
|
||||
|
||||
export default {
|
||||
logError,
|
||||
subscribeToMessages
|
||||
subscribeToMessages,
|
||||
subscribeToSyncMessages
|
||||
};
|
||||
54
src/public/javascripts/services/note_autocomplete.js
Normal file
54
src/public/javascripts/services/note_autocomplete.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import server from "./server.js";
|
||||
|
||||
async function initNoteAutocomplete($el) {
|
||||
if (!$el.hasClass("ui-autocomplete-input")) {
|
||||
const $showRecentNotesButton = $("<span>")
|
||||
.addClass("input-group-addon show-recent-notes-button")
|
||||
.prop("title", "Show recent notes");
|
||||
|
||||
$el.after($showRecentNotesButton);
|
||||
|
||||
$showRecentNotesButton.click(() => $el.autocomplete("search", ""));
|
||||
|
||||
await $el.autocomplete({
|
||||
appendTo: $el.parent().parent(),
|
||||
source: async function (request, response) {
|
||||
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
|
||||
|
||||
if (result.length > 0) {
|
||||
response(result.map(row => {
|
||||
return {
|
||||
label: row.label,
|
||||
value: row.label + ' (' + row.value + ')'
|
||||
}
|
||||
}));
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
label: "No results",
|
||||
value: "No results"
|
||||
}]);
|
||||
}
|
||||
},
|
||||
minLength: 0,
|
||||
change: function (event, ui) {
|
||||
$el.trigger("change");
|
||||
},
|
||||
select: function (event, ui) {
|
||||
if (ui.item.value === 'No results') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ko.bindingHandlers.noteAutocomplete = {
|
||||
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
|
||||
initNoteAutocomplete($(element));
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
initNoteAutocomplete
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import noteDetailFile from './note_detail_file.js';
|
||||
import noteDetailSearch from './note_detail_search.js';
|
||||
import noteDetailRender from './note_detail_render.js';
|
||||
import bundleService from "./bundle.js";
|
||||
import noteAutocompleteService from "./note_autocomplete.js";
|
||||
|
||||
const $noteTitle = $("#note-title");
|
||||
|
||||
@@ -26,12 +27,11 @@ const $unprotectButton = $("#unprotect-button");
|
||||
const $noteDetailWrapper = $("#note-detail-wrapper");
|
||||
const $noteDetailComponentWrapper = $("#note-detail-component-wrapper");
|
||||
const $noteIdDisplay = $("#note-id-display");
|
||||
const $labelList = $("#label-list");
|
||||
const $labelListInner = $("#label-list-inner");
|
||||
const $relationList = $("#relation-list");
|
||||
const $relationListInner = $("#relation-list-inner");
|
||||
const $attributeList = $("#attribute-list");
|
||||
const $attributeListInner = $("#attribute-list-inner");
|
||||
const $childrenOverview = $("#children-overview");
|
||||
const $scriptArea = $("#note-detail-script-area");
|
||||
const $promotedAttributesContainer = $("#note-detail-promoted-attributes");
|
||||
|
||||
let currentNote = null;
|
||||
|
||||
@@ -183,16 +183,14 @@ async function loadNoteDetail(noteId) {
|
||||
// after loading new note make sure editor is scrolled to the top
|
||||
$noteDetailWrapper.scrollTop(0);
|
||||
|
||||
const labels = await loadLabelList();
|
||||
|
||||
const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview');
|
||||
await showChildrenOverview(hideChildrenOverview);
|
||||
|
||||
await loadRelationList();
|
||||
|
||||
$scriptArea.html('');
|
||||
|
||||
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
|
||||
|
||||
const attributes = await loadAttributes();
|
||||
|
||||
const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview');
|
||||
await showChildrenOverview(hideChildrenOverview);
|
||||
}
|
||||
|
||||
async function showChildrenOverview(hideChildrenOverview) {
|
||||
@@ -220,48 +218,207 @@ async function showChildrenOverview(hideChildrenOverview) {
|
||||
$childrenOverview.show();
|
||||
}
|
||||
|
||||
async function loadLabelList() {
|
||||
async function loadAttributes() {
|
||||
$promotedAttributesContainer.empty();
|
||||
$attributeList.hide();
|
||||
|
||||
const noteId = getCurrentNoteId();
|
||||
|
||||
const labels = await server.get('notes/' + noteId + '/labels');
|
||||
const attributes = await server.get('notes/' + noteId + '/attributes');
|
||||
|
||||
$labelListInner.html('');
|
||||
const promoted = attributes.filter(attr => (attr.type === 'label-definition' || attr.type === 'relation-definition') && attr.value.isPromoted);
|
||||
|
||||
if (labels.length > 0) {
|
||||
for (const label of labels) {
|
||||
$labelListInner.append(utils.formatLabel(label) + " ");
|
||||
let idx = 1;
|
||||
|
||||
async function createRow(definitionAttr, valueAttr) {
|
||||
const definition = definitionAttr.value;
|
||||
const inputId = "promoted-input-" + idx;
|
||||
const $tr = $("<tr>");
|
||||
const $labelCell = $("<th>").append(valueAttr.name);
|
||||
const $input = $("<input>")
|
||||
.prop("id", inputId)
|
||||
.prop("tabindex", definitionAttr.position)
|
||||
.prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
|
||||
.prop("attribute-type", valueAttr.type)
|
||||
.prop("attribute-name", valueAttr.name)
|
||||
.prop("value", valueAttr.value)
|
||||
.addClass("form-control")
|
||||
.addClass("promoted-attribute-input");
|
||||
|
||||
idx++;
|
||||
|
||||
const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input));
|
||||
|
||||
const $actionCell = $("<td>");
|
||||
const $multiplicityCell = $("<td>");
|
||||
|
||||
$tr
|
||||
.append($labelCell)
|
||||
.append($inputCell)
|
||||
.append($actionCell)
|
||||
.append($multiplicityCell);
|
||||
|
||||
if (valueAttr.type === 'label') {
|
||||
if (definition.labelType === 'text') {
|
||||
$input.prop("type", "text");
|
||||
|
||||
// no need to await for this, can be done asynchronously
|
||||
server.get('attributes/values/' + encodeURIComponent(valueAttr.name)).then(attributeValues => {
|
||||
if (attributeValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$input.autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: attributeValues.map(attribute => {
|
||||
return {
|
||||
attribute: attribute,
|
||||
value: attribute
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
|
||||
$input.focus(() => $input.autocomplete("search", ""));
|
||||
});
|
||||
}
|
||||
else if (definition.labelType === 'number') {
|
||||
$input.prop("type", "number");
|
||||
}
|
||||
else if (definition.labelType === 'boolean') {
|
||||
$input.prop("type", "checkbox");
|
||||
|
||||
if (valueAttr.value === "true") {
|
||||
$input.prop("checked", "checked");
|
||||
}
|
||||
}
|
||||
else if (definition.labelType === 'date') {
|
||||
$input.prop("type", "text");
|
||||
|
||||
$input.datepicker({
|
||||
changeMonth: true,
|
||||
changeYear: true,
|
||||
dateFormat: "yy-mm-dd"
|
||||
});
|
||||
|
||||
const $todayButton = $("<button>").addClass("btn btn-small").text("Today").click(() => {
|
||||
$input.val(utils.formatDateISO(new Date()));
|
||||
$input.trigger("change");
|
||||
});
|
||||
|
||||
$actionCell.append($todayButton);
|
||||
}
|
||||
else {
|
||||
messagingService.logError("Unknown labelType=" + definitionAttr.labelType);
|
||||
}
|
||||
}
|
||||
else if (valueAttr.type === 'relation') {
|
||||
if (valueAttr.value) {
|
||||
$input.val((await treeUtils.getNoteTitle(valueAttr.value) + " (" + valueAttr.value + ")"));
|
||||
}
|
||||
|
||||
// no need to wait for this
|
||||
noteAutocompleteService.initNoteAutocomplete($input);
|
||||
}
|
||||
else {
|
||||
messagingService.logError("Unknown attribute type=" + valueAttr.type);
|
||||
return;
|
||||
}
|
||||
|
||||
$labelList.show();
|
||||
}
|
||||
else {
|
||||
$labelList.hide();
|
||||
}
|
||||
if (definition.multiplicityType === "multivalue") {
|
||||
const addButton = $("<span>")
|
||||
.addClass("glyphicon glyphicon-plus pointer")
|
||||
.prop("title", "Add new attribute")
|
||||
.click(async () => {
|
||||
const $new = await createRow(definitionAttr, {
|
||||
attributeId: "",
|
||||
type: valueAttr.type,
|
||||
name: definitionAttr.name,
|
||||
value: ""
|
||||
});
|
||||
|
||||
return labels;
|
||||
}
|
||||
$tr.after($new);
|
||||
|
||||
async function loadRelationList() {
|
||||
const noteId = getCurrentNoteId();
|
||||
$new.find('input').focus();
|
||||
});
|
||||
|
||||
const relations = await server.get('notes/' + noteId + '/relations');
|
||||
const removeButton = $("<span>")
|
||||
.addClass("glyphicon glyphicon-trash pointer")
|
||||
.prop("title", "Remove this attribute")
|
||||
.click(async () => {
|
||||
if (valueAttr.attributeId) {
|
||||
await server.remove("notes/" + noteId + "/attributes/" + valueAttr.attributeId);
|
||||
}
|
||||
|
||||
$relationListInner.html('');
|
||||
$tr.remove();
|
||||
});
|
||||
|
||||
if (relations.length > 0) {
|
||||
for (const relation of relations) {
|
||||
$relationListInner.append(relation.name + " = ");
|
||||
$relationListInner.append(await linkService.createNoteLink(relation.targetNoteId));
|
||||
$relationListInner.append(" ");
|
||||
$multiplicityCell.append(addButton).append(" ").append(removeButton);
|
||||
}
|
||||
|
||||
$relationList.show();
|
||||
}
|
||||
else {
|
||||
$relationList.hide();
|
||||
return $tr;
|
||||
}
|
||||
|
||||
return relations;
|
||||
if (promoted.length > 0) {
|
||||
const $tbody = $("<tbody>");
|
||||
|
||||
for (const definitionAttr of promoted) {
|
||||
const definitionType = definitionAttr.type;
|
||||
const valueType = definitionType.substr(0, definitionType.length - 11);
|
||||
|
||||
let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType);
|
||||
|
||||
if (valueAttrs.length === 0) {
|
||||
valueAttrs.push({
|
||||
attributeId: "",
|
||||
type: valueType,
|
||||
name: definitionAttr.name,
|
||||
value: ""
|
||||
});
|
||||
}
|
||||
|
||||
if (definitionAttr.value.multiplicityType === 'singlevalue') {
|
||||
valueAttrs = valueAttrs.slice(0, 1);
|
||||
}
|
||||
|
||||
for (const valueAttr of valueAttrs) {
|
||||
const $tr = await createRow(definitionAttr, valueAttr);
|
||||
|
||||
$tbody.append($tr);
|
||||
}
|
||||
}
|
||||
|
||||
// we replace the whole content in one step so there can't be any race conditions
|
||||
// (previously we saw promoted attributes doubling)
|
||||
$promotedAttributesContainer.empty().append($tbody);
|
||||
}
|
||||
else {
|
||||
$attributeListInner.html('');
|
||||
|
||||
if (attributes.length > 0) {
|
||||
for (const attribute of attributes) {
|
||||
if (attribute.type === 'label') {
|
||||
$attributeListInner.append(utils.formatLabel(attribute) + " ");
|
||||
}
|
||||
else if (attribute.type === 'relation') {
|
||||
$attributeListInner.append(attribute.name + "=");
|
||||
$attributeListInner.append(await linkService.createNoteLink(attribute.value));
|
||||
$attributeListInner.append(" ");
|
||||
}
|
||||
else if (attribute.type === 'label-definition' || attribute.type === 'relation-definition') {
|
||||
$attributeListInner.append(attribute.name + " definition ");
|
||||
}
|
||||
else {
|
||||
messagingService.logError("Unknown attr type: " + attribute.type);
|
||||
}
|
||||
}
|
||||
|
||||
$attributeList.show();
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
async function loadNote(noteId) {
|
||||
@@ -276,7 +433,7 @@ function focus() {
|
||||
getComponent(note.type).focus();
|
||||
}
|
||||
|
||||
messagingService.subscribeToMessages(syncData => {
|
||||
messagingService.subscribeToSyncMessages(syncData => {
|
||||
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getCurrentNoteId())) {
|
||||
infoService.showMessage('Reloading note because of background changes');
|
||||
|
||||
@@ -284,6 +441,35 @@ messagingService.subscribeToMessages(syncData => {
|
||||
}
|
||||
});
|
||||
|
||||
$promotedAttributesContainer.on('change', '.promoted-attribute-input', async event => {
|
||||
const $attr = $(event.target);
|
||||
|
||||
let value;
|
||||
|
||||
if ($attr.prop("type") === "checkbox") {
|
||||
value = $attr.is(':checked') ? "true" : "false";
|
||||
}
|
||||
else if ($attr.prop("attribute-type") === "relation") {
|
||||
if ($attr.val()) {
|
||||
value = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel($attr.val()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
value = $attr.val();
|
||||
}
|
||||
|
||||
const result = await server.put("notes/" + getCurrentNoteId() + "/attribute", {
|
||||
attributeId: $attr.prop("attribute-id"),
|
||||
type: $attr.prop("attribute-type"),
|
||||
name: $attr.prop("attribute-name"),
|
||||
value: value
|
||||
});
|
||||
|
||||
$attr.prop("attribute-id", result.attributeId);
|
||||
|
||||
infoService.showMessage("Attribute has been saved.");
|
||||
});
|
||||
|
||||
$(document).ready(() => {
|
||||
$noteTitle.on('input', () => {
|
||||
noteChanged();
|
||||
@@ -312,8 +498,7 @@ export default {
|
||||
getCurrentNoteId,
|
||||
newNoteCreated,
|
||||
focus,
|
||||
loadLabelList,
|
||||
loadRelationList,
|
||||
loadAttributes,
|
||||
saveNote,
|
||||
saveNoteIfChanged,
|
||||
noteChanged
|
||||
|
||||
@@ -32,7 +32,7 @@ async function show() {
|
||||
lint: true,
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
lineNumbers: true,
|
||||
tabindex: 2 // so that tab from title will lead to code editor focus
|
||||
tabindex: 100
|
||||
});
|
||||
|
||||
codeEditor.on('change', noteDetailService.noteChanged);
|
||||
|
||||
@@ -14,13 +14,13 @@ const $fileOpen = $("#file-open");
|
||||
async function show() {
|
||||
const currentNote = noteDetailService.getCurrentNote();
|
||||
|
||||
const labels = await server.get('notes/' + currentNote.noteId + '/labels');
|
||||
const labelMap = utils.toObject(labels, l => [l.name, l.value]);
|
||||
const attributes = await server.get('notes/' + currentNote.noteId + '/attributes');
|
||||
const attributeMap = utils.toObject(attributes, l => [l.name, l.value]);
|
||||
|
||||
$noteDetailFile.show();
|
||||
|
||||
$fileFileName.text(labelMap.original_file_name);
|
||||
$fileFileSize.text(labelMap.file_size + " bytes");
|
||||
$fileFileName.text(attributeMap.original_file_name);
|
||||
$fileFileSize.text(attributeMap.file_size + " bytes");
|
||||
$fileFileType.text(currentNote.mime);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,19 @@ import utils from './utils.js';
|
||||
import infoService from './info.js';
|
||||
import linkService from './link.js';
|
||||
|
||||
function ScriptApi(startNote, currentNote, targetNote = null) {
|
||||
function ScriptApi(startNote, currentNote, originEntity = null) {
|
||||
const $pluginButtons = $("#plugin-buttons");
|
||||
|
||||
async function activateNote(notePath) {
|
||||
await treeService.activateNode(notePath);
|
||||
}
|
||||
|
||||
async function activateNewNote(notePath) {
|
||||
await treeService.reload();
|
||||
|
||||
await treeService.activateNode(notePath, true);
|
||||
}
|
||||
|
||||
function addButtonToToolbar(buttonId, button) {
|
||||
$("#" + buttonId).remove();
|
||||
|
||||
@@ -44,7 +50,8 @@ function ScriptApi(startNote, currentNote, targetNote = null) {
|
||||
params: prepareParams(params),
|
||||
startNoteId: startNote.noteId,
|
||||
currentNoteId: currentNote.noteId,
|
||||
targetNoteId: targetNote ? targetNote.noteId : null
|
||||
originEntityName: originEntity ? originEntity.constructor.tableName : null,
|
||||
originEntityId: originEntity ? originEntity.noteId : null
|
||||
});
|
||||
|
||||
return ret.executionResult;
|
||||
@@ -53,16 +60,18 @@ function ScriptApi(startNote, currentNote, targetNote = null) {
|
||||
return {
|
||||
startNote: startNote,
|
||||
currentNote: currentNote,
|
||||
targetNote: targetNote,
|
||||
originEntity: originEntity,
|
||||
addButtonToToolbar,
|
||||
activateNote,
|
||||
activateNewNote,
|
||||
getInstanceName: () => window.glob.instanceName,
|
||||
runOnServer,
|
||||
formatDateISO: utils.formatDateISO,
|
||||
parseDate: utils.parseDate,
|
||||
showMessage: infoService.showMessage,
|
||||
showError: infoService.showError,
|
||||
reloadTree: treeService.reload,
|
||||
reloadTree: treeService.reload, // deprecated
|
||||
refreshTree: treeService.reload,
|
||||
createNoteLink: linkService.createNoteLink
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import ScriptApi from './script_api.js';
|
||||
import utils from './utils.js';
|
||||
|
||||
function ScriptContext(startNote, allNotes, targetNote = null) {
|
||||
function ScriptContext(startNote, allNotes, originEntity = null) {
|
||||
const modules = {};
|
||||
|
||||
return {
|
||||
modules: modules,
|
||||
notes: utils.toObject(allNotes, note => [note.noteId, note]),
|
||||
apis: utils.toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note, targetNote)]),
|
||||
apis: utils.toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note, originEntity)]),
|
||||
require: moduleNoteIds => {
|
||||
return moduleName => {
|
||||
const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
|
||||
|
||||
@@ -4,7 +4,7 @@ import linkService from "./link.js";
|
||||
|
||||
function setupTooltip() {
|
||||
$(document).tooltip({
|
||||
items: "#note-detail-wrapper a",
|
||||
items: "body a",
|
||||
content: function (callback) {
|
||||
let notePath = linkService.getNotePathFromLink($(this).attr("href"));
|
||||
|
||||
@@ -15,7 +15,19 @@ function setupTooltip() {
|
||||
if (notePath) {
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
noteDetailService.loadNote(noteId).then(note => callback(note.content));
|
||||
noteDetailService.loadNote(noteId).then(note => {
|
||||
if (!note.content.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.type === 'text') {
|
||||
callback(note.content);
|
||||
}
|
||||
else if (note.type === 'code') {
|
||||
callback($("<pre>").text(note.content).prop('outerHTML'));
|
||||
}
|
||||
// other types of notes don't have tooltip preview
|
||||
});
|
||||
}
|
||||
},
|
||||
close: function (event, ui) {
|
||||
|
||||
@@ -100,14 +100,22 @@ async function expandToNote(notePath, expandOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
async function activateNode(notePath) {
|
||||
async function activateNode(notePath, newNote) {
|
||||
utils.assertArguments(notePath);
|
||||
|
||||
const node = await expandToNote(notePath);
|
||||
|
||||
await node.setActive();
|
||||
if (newNote) {
|
||||
noteDetailService.newNoteCreated();
|
||||
}
|
||||
|
||||
// we use noFocus because when we reload the tree because of background changes
|
||||
// we don't want the reload event to steal focus from whatever was focused before
|
||||
await node.setActive(true, { noFocus: true });
|
||||
|
||||
clearSelectedNodes();
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,11 +291,11 @@ async function treeInitialized() {
|
||||
}
|
||||
|
||||
if (startNotePath) {
|
||||
await activateNode(startNotePath);
|
||||
const node = await activateNode(startNotePath);
|
||||
|
||||
// looks like this this doesn't work when triggered immediatelly after activating node
|
||||
// so waiting a second helps
|
||||
setTimeout(scrollToCurrentNote, 1000);
|
||||
setTimeout(() => node.makeVisible({scrollIntoView: true}), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +513,13 @@ async function showTree() {
|
||||
initFancyTree(tree);
|
||||
}
|
||||
|
||||
messagingService.subscribeToMessages(syncData => {
|
||||
messagingService.subscribeToMessages(message => {
|
||||
if (message.type === 'refresh-tree') {
|
||||
reload();
|
||||
}
|
||||
});
|
||||
|
||||
messagingService.subscribeToSyncMessages(syncData => {
|
||||
if (syncData.some(sync => sync.entityName === 'branches')
|
||||
|| syncData.some(sync => sync.entityName === 'notes')) {
|
||||
|
||||
|
||||
@@ -115,6 +115,10 @@ async function getExtraClasses(note) {
|
||||
extraClasses.push("multiple-parents");
|
||||
}
|
||||
|
||||
if (note.cssClass) {
|
||||
extraClasses.push(note.cssClass);
|
||||
}
|
||||
|
||||
extraClasses.push(note.type);
|
||||
|
||||
return extraClasses.join(" ");
|
||||
|
||||
@@ -2,6 +2,7 @@ import utils from "./utils.js";
|
||||
import Branch from "../entities/branch.js";
|
||||
import NoteShort from "../entities/note_short.js";
|
||||
import infoService from "./info.js";
|
||||
import messagingService from "./messaging.js";
|
||||
import server from "./server.js";
|
||||
|
||||
class TreeCache {
|
||||
@@ -48,12 +49,14 @@ class TreeCache {
|
||||
|
||||
return noteIds.map(noteId => {
|
||||
if (!this.notes[noteId]) {
|
||||
throw new Error(`Can't find note ${noteId}`);
|
||||
messagingService.logError(`Can't find note ${noteId}`);
|
||||
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
});
|
||||
}).filter(note => note !== null);
|
||||
}
|
||||
|
||||
/** @return NoteShort */
|
||||
|
||||
@@ -42,7 +42,7 @@ const keyBindings = {
|
||||
},
|
||||
"shift+up": node => {
|
||||
node.navigate($.ui.keyCode.UP, true).then(() => {
|
||||
const currentNode = getCurrentNode();
|
||||
const currentNode = treeService.getCurrentNode();
|
||||
|
||||
if (currentNode.isSelected()) {
|
||||
node.setSelected(false);
|
||||
|
||||
@@ -46,7 +46,7 @@ function isElectron() {
|
||||
function assertArguments() {
|
||||
for (const i in arguments) {
|
||||
if (!arguments[i]) {
|
||||
throw new Error(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
|
||||
console.trace(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
src/public/libraries/ckeditor/ckeditor.js
vendored
2
src/public/libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -308,13 +308,13 @@ div.ui-tooltip {
|
||||
|
||||
.cm-matchhighlight {background-color: #eeeeee}
|
||||
|
||||
#label-list, #relation-list {
|
||||
#label-list, #relation-list, #attribute-list {
|
||||
color: #777777;
|
||||
padding: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#label-list button, #relation-list button {
|
||||
#label-list button, #relation-list button, #attribute-list button {
|
||||
padding: 2px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -420,4 +420,26 @@ html.theme-dark img, html.theme-dark video {
|
||||
|
||||
html.theme-dark body {
|
||||
background: #191819;
|
||||
}
|
||||
|
||||
.ck.ck-block-toolbar-button {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
|
||||
#note-detail-promoted-attributes {
|
||||
max-width: 70%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#note-detail-promoted-attributes td, note-detail-promoted-attributes th {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.show-recent-notes-button {
|
||||
background: url('/images/icons/clock-16.png') no-repeat center;
|
||||
cursor: pointer;
|
||||
}
|
||||
117
src/routes/api/attributes.js
Normal file
117
src/routes/api/attributes.js
Normal file
@@ -0,0 +1,117 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('../../services/sql');
|
||||
const attributeService = require('../../services/attributes');
|
||||
const repository = require('../../services/repository');
|
||||
const Attribute = require('../../entities/attribute');
|
||||
|
||||
async function getEffectiveNoteAttributes(req) {
|
||||
const note = await repository.getNote(req.params.noteId);
|
||||
|
||||
return await note.getAttributes();
|
||||
}
|
||||
|
||||
async function updateNoteAttribute(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const body = req.body;
|
||||
|
||||
let attribute;
|
||||
if (body.attributeId) {
|
||||
attribute = await repository.getAttribute(body.attributeId);
|
||||
}
|
||||
else {
|
||||
attribute = new Attribute();
|
||||
attribute.noteId = noteId;
|
||||
attribute.name = body.name;
|
||||
attribute.type = body.type;
|
||||
}
|
||||
|
||||
if (attribute.noteId !== noteId) {
|
||||
return [400, `Attribute ${body.attributeId} is not owned by ${noteId}`];
|
||||
}
|
||||
|
||||
attribute.value = body.value;
|
||||
|
||||
await attribute.save();
|
||||
|
||||
return {
|
||||
attributeId: attribute.attributeId
|
||||
};
|
||||
}
|
||||
|
||||
async function deleteNoteAttribute(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const attributeId = req.params.attributeId;
|
||||
|
||||
const attribute = await repository.getAttribute(attributeId);
|
||||
|
||||
if (attribute) {
|
||||
if (attribute.noteId !== noteId) {
|
||||
return [400, `Attribute ${attributeId} is not owned by ${noteId}`];
|
||||
}
|
||||
|
||||
attribute.isDeleted = true;
|
||||
await attribute.save();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateNoteAttributes(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const attributes = req.body;
|
||||
|
||||
for (const attribute of attributes) {
|
||||
let attributeEntity;
|
||||
|
||||
if (attribute.attributeId) {
|
||||
attributeEntity = await repository.getAttribute(attribute.attributeId);
|
||||
|
||||
if (attributeEntity.noteId !== noteId) {
|
||||
return [400, `Attribute ${attributeEntity.noteId} is not owned by ${noteId}`];
|
||||
}
|
||||
}
|
||||
else {
|
||||
// if it was "created" and then immediatelly deleted, we just don't create it at all
|
||||
if (attribute.isDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
attributeEntity = new Attribute();
|
||||
attributeEntity.noteId = noteId;
|
||||
}
|
||||
|
||||
attributeEntity.type = attribute.type;
|
||||
attributeEntity.name = attribute.name;
|
||||
attributeEntity.value = attribute.value;
|
||||
attributeEntity.position = attribute.position;
|
||||
attributeEntity.isInheritable = attribute.isInheritable;
|
||||
attributeEntity.isDeleted = attribute.isDeleted;
|
||||
|
||||
await attributeEntity.save();
|
||||
}
|
||||
|
||||
const note = await repository.getNote(noteId);
|
||||
|
||||
return await note.getAttributes();
|
||||
}
|
||||
|
||||
async function getAttributeNames(req) {
|
||||
const type = req.query.type;
|
||||
const query = req.query.query;
|
||||
|
||||
return attributeService.getAttributeNames(type, query);
|
||||
}
|
||||
|
||||
async function getValuesForAttribute(req) {
|
||||
const attributeName = req.params.attributeName;
|
||||
|
||||
return await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND type = 'label' AND value != '' ORDER BY value", [attributeName]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
updateNoteAttributes,
|
||||
updateNoteAttribute,
|
||||
deleteNoteAttribute,
|
||||
getAttributeNames,
|
||||
getValuesForAttribute,
|
||||
getEffectiveNoteAttributes
|
||||
};
|
||||
@@ -113,15 +113,18 @@ async function exportToTar(branchId, res) {
|
||||
prefix: branch.prefix,
|
||||
type: note.type,
|
||||
mime: note.mime,
|
||||
labels: (await note.getLabels()).map(label => {
|
||||
attributes: (await note.getOwnedAttributes()).map(attribute => {
|
||||
return {
|
||||
name: label.name,
|
||||
value: label.value
|
||||
type: attribute.type,
|
||||
name: attribute.name,
|
||||
value: attribute.value,
|
||||
isInheritable: attribute.isInheritable,
|
||||
position: attribute.position
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
if (metadata.labels.find(label => label.name === 'excludeFromExport')) {
|
||||
if (metadata.attributes.find(attributes => attributes.type === 'label' && attributes.name === 'excludeFromExport')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const noteService = require('../../services/notes');
|
||||
const labelService = require('../../services/labels');
|
||||
const attributeService = require('../../services/attributes');
|
||||
const protectedSessionService = require('../../services/protected_session');
|
||||
const repository = require('../../services/repository');
|
||||
|
||||
@@ -26,8 +26,8 @@ async function uploadFile(req) {
|
||||
mime: file.mimetype
|
||||
});
|
||||
|
||||
await labelService.createLabel(note.noteId, "originalFileName", originalName);
|
||||
await labelService.createLabel(note.noteId, "fileSize", size);
|
||||
await attributeService.createLabel(note.noteId, "originalFileName", originalName);
|
||||
await attributeService.createLabel(note.noteId, "fileSize", size);
|
||||
|
||||
return {
|
||||
noteId: note.noteId
|
||||
@@ -47,8 +47,8 @@ async function downloadFile(req, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelMap = await note.getLabelMap();
|
||||
const fileName = labelMap.originalFileName || note.title;
|
||||
const originalFileName = await note.getLabel('originalFileName');
|
||||
const fileName = originalFileName.value || note.title;
|
||||
|
||||
res.setHeader('Content-Disposition', 'file; filename="' + fileName + '"');
|
||||
res.setHeader('Content-Type', note.mime);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const repository = require('../../services/repository');
|
||||
const labelService = require('../../services/labels');
|
||||
const attributeService = require('../../services/attributes');
|
||||
const noteService = require('../../services/notes');
|
||||
const Branch = require('../../entities/branch');
|
||||
const tar = require('tar-stream');
|
||||
@@ -187,8 +187,15 @@ async function importNotes(files, parentNoteId, noteIdMap) {
|
||||
|
||||
noteIdMap[file.meta.noteId] = note.noteId;
|
||||
|
||||
for (const label of file.meta.labels) {
|
||||
await labelService.createLabel(note.noteId, label.name, label.value);
|
||||
for (const attribute of file.meta.attributes) {
|
||||
await attributeService.createAttribute({
|
||||
noteId: note.noteId,
|
||||
type: attribute.type,
|
||||
name: attribute.name,
|
||||
value: attribute.value,
|
||||
isInheritable: attribute.isInheritable,
|
||||
position: attribute.position
|
||||
});
|
||||
}
|
||||
|
||||
if (file.children.length > 0) {
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('../../services/sql');
|
||||
const labelService = require('../../services/labels');
|
||||
const repository = require('../../services/repository');
|
||||
const Label = require('../../entities/label');
|
||||
|
||||
async function getNoteLabels(req) {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
return await repository.getEntities("SELECT * FROM labels WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]);
|
||||
}
|
||||
|
||||
async function updateNoteLabels(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const labels = req.body;
|
||||
|
||||
for (const label of labels) {
|
||||
let labelEntity;
|
||||
|
||||
if (label.labelId) {
|
||||
labelEntity = await repository.getLabel(label.labelId);
|
||||
}
|
||||
else {
|
||||
// if it was "created" and then immediatelly deleted, we just don't create it at all
|
||||
if (label.isDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
labelEntity = new Label();
|
||||
labelEntity.noteId = noteId;
|
||||
}
|
||||
|
||||
labelEntity.name = label.name;
|
||||
labelEntity.value = label.value;
|
||||
labelEntity.position = label.position;
|
||||
labelEntity.isDeleted = label.isDeleted;
|
||||
|
||||
await labelEntity.save();
|
||||
}
|
||||
|
||||
return await repository.getEntities("SELECT * FROM labels WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]);
|
||||
}
|
||||
|
||||
async function getAllLabelNames() {
|
||||
const names = await sql.getColumn("SELECT DISTINCT name FROM labels WHERE isDeleted = 0");
|
||||
|
||||
for (const label of labelService.BUILTIN_LABELS) {
|
||||
if (!names.includes(label)) {
|
||||
names.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
names.sort();
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
async function getValuesForLabel(req) {
|
||||
const labelName = req.params.labelName;
|
||||
|
||||
return await sql.getColumn("SELECT DISTINCT value FROM labels WHERE isDeleted = 0 AND name = ? AND value != '' ORDER BY value", [labelName]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNoteLabels,
|
||||
updateNoteLabels,
|
||||
getAllLabelNames,
|
||||
getValuesForLabel
|
||||
};
|
||||
@@ -65,7 +65,7 @@ async function loginToProtectedSession(req) {
|
||||
// this is set here so that event handlers have access to the protected session
|
||||
cls.namespace.set('protectedSessionId', protectedSessionId);
|
||||
|
||||
eventService.emit(eventService.ENTER_PROTECTED_SESSION);
|
||||
await eventService.emit(eventService.ENTER_PROTECTED_SESSION);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('../../services/sql');
|
||||
const relationService = require('../../services/relations');
|
||||
const repository = require('../../services/repository');
|
||||
const Relation = require('../../entities/relation');
|
||||
|
||||
async function getNoteRelations(req) {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
return await repository.getEntities("SELECT * FROM relations WHERE isDeleted = 0 AND sourceNoteId = ? ORDER BY position, dateCreated", [noteId]);
|
||||
}
|
||||
|
||||
async function updateNoteRelations(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const relations = req.body;
|
||||
|
||||
for (const relation of relations) {
|
||||
let relationEntity;
|
||||
|
||||
if (relation.relationId) {
|
||||
relationEntity = await repository.getRelation(relation.relationId);
|
||||
}
|
||||
else {
|
||||
// if it was "created" and then immediatelly deleted, we just don't create it at all
|
||||
if (relation.isDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
relationEntity = new Relation();
|
||||
relationEntity.sourceNoteId = noteId;
|
||||
}
|
||||
|
||||
relationEntity.name = relation.name;
|
||||
relationEntity.targetNoteId = relation.targetNoteId;
|
||||
relationEntity.isInheritable = relation.isInheritable;
|
||||
relationEntity.position = relation.position;
|
||||
relationEntity.isDeleted = relation.isDeleted;
|
||||
|
||||
await relationEntity.save();
|
||||
}
|
||||
|
||||
return await repository.getEntities("SELECT * FROM relations WHERE isDeleted = 0 AND sourceNoteId = ? ORDER BY position, dateCreated", [noteId]);
|
||||
}
|
||||
|
||||
async function getAllRelationNames() {
|
||||
const names = await sql.getColumn("SELECT DISTINCT name FROM relations WHERE isDeleted = 0");
|
||||
|
||||
for (const relationName of relationService.BUILTIN_RELATIONS) {
|
||||
if (!names.includes(relationName)) {
|
||||
names.push(relationName);
|
||||
}
|
||||
}
|
||||
|
||||
names.sort();
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNoteRelations,
|
||||
updateNoteRelations,
|
||||
getAllRelationNames
|
||||
};
|
||||
@@ -1,13 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
const labelService = require('../../services/labels');
|
||||
const scriptService = require('../../services/script');
|
||||
const relationService = require('../../services/relations');
|
||||
const attributeService = require('../../services/attributes');
|
||||
const repository = require('../../services/repository');
|
||||
|
||||
async function exec(req) {
|
||||
const result = await scriptService.executeScript(req.body.script, req.body.params, req.body.startNoteId,
|
||||
req.body.currentNoteId, req.body.targetNoteId);
|
||||
req.body.currentNoteId, req.body.originEntityName, req.body.originEntityId);
|
||||
|
||||
return { executionResult: result };
|
||||
}
|
||||
@@ -15,13 +14,13 @@ async function exec(req) {
|
||||
async function run(req) {
|
||||
const note = await repository.getNote(req.params.noteId);
|
||||
|
||||
const result = await scriptService.executeNote(req, note);
|
||||
const result = await scriptService.executeNote(note, note);
|
||||
|
||||
return { executionResult: result };
|
||||
}
|
||||
|
||||
async function getStartupBundles() {
|
||||
const notes = await labelService.getNotesWithLabel("run", "frontendStartup");
|
||||
const notes = await attributeService.getNotesWithLabel("run", "frontendStartup");
|
||||
|
||||
const bundles = [];
|
||||
|
||||
@@ -38,11 +37,12 @@ async function getStartupBundles() {
|
||||
|
||||
async function getRelationBundles(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const note = await repository.getNote(noteId);
|
||||
const relationName = req.params.relationName;
|
||||
|
||||
const relations = await relationService.getEffectiveRelations(noteId);
|
||||
const filtered = relations.filter(relation => relation.name === relationName);
|
||||
const targetNoteIds = filtered.map(relation => relation.targetNoteId);
|
||||
const attributes = await note.getAttributes();
|
||||
const filtered = attributes.filter(attr => attr.type === 'relation' && attr.name === relationName);
|
||||
const targetNoteIds = filtered.map(relation => relation.value);
|
||||
const uniqueNoteIds = Array.from(new Set(targetNoteIds));
|
||||
|
||||
const bundles = [];
|
||||
|
||||
@@ -9,6 +9,26 @@ async function getNotes(noteIds) {
|
||||
SELECT noteId, title, isProtected, type, mime
|
||||
FROM notes WHERE isDeleted = 0 AND noteId IN (???)`, noteIds);
|
||||
|
||||
const cssClassLabels = await sql.getManyRows(`
|
||||
SELECT noteId, value FROM attributes WHERE isDeleted = 0 AND type = 'label'
|
||||
AND name = 'cssClass' AND noteId IN (???)`, noteIds);
|
||||
|
||||
for (const label of cssClassLabels) {
|
||||
// FIXME: inefficient!
|
||||
const note = notes.find(note => note.noteId === label.noteId);
|
||||
|
||||
if (!note) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (note.cssClass) {
|
||||
note.cssClass += " " + label.value;
|
||||
}
|
||||
else {
|
||||
note.cssClass = label.value;
|
||||
}
|
||||
}
|
||||
|
||||
protectedSessionService.decryptNotes(notes);
|
||||
|
||||
notes.forEach(note => note.isProtected = !!note.isProtected);
|
||||
@@ -35,9 +55,9 @@ async function getTree() {
|
||||
JOIN tree ON branches.parentNoteId = tree.noteId
|
||||
WHERE tree.isExpanded = 1 AND branches.isDeleted = 0
|
||||
)
|
||||
SELECT branches.* FROM tree JOIN branches USING(noteId) ORDER BY branches.notePosition`);
|
||||
SELECT branches.* FROM tree JOIN branches USING(noteId) WHERE branches.isDeleted = 0 ORDER BY branches.notePosition`);
|
||||
|
||||
const noteIds = branches.map(b => b.noteId);
|
||||
const noteIds = Array.from(new Set(branches.map(b => b.noteId)));
|
||||
|
||||
const notes = await getNotes(noteIds);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const sourceIdService = require('../services/source_id');
|
||||
const sql = require('../services/sql');
|
||||
const labelService = require('../services/labels');
|
||||
const attributeService = require('../services/attributes');
|
||||
const config = require('../services/config');
|
||||
const optionService = require('../services/options');
|
||||
|
||||
@@ -18,7 +18,7 @@ async function index(req, res) {
|
||||
|
||||
async function getAppCss() {
|
||||
let css = '';
|
||||
const notes = labelService.getNotesWithLabel('appCss');
|
||||
const notes = attributeService.getNotesWithLabel('appCss');
|
||||
|
||||
for (const note of await notes) {
|
||||
css += `/* ${note.noteId} */
|
||||
|
||||
@@ -25,8 +25,7 @@ const sqlRoute = require('./api/sql');
|
||||
const anonymizationRoute = require('./api/anonymization');
|
||||
const cleanupRoute = require('./api/cleanup');
|
||||
const imageRoute = require('./api/image');
|
||||
const labelsRoute = require('./api/labels');
|
||||
const relationsRoute = require('./api/relations');
|
||||
const attributesRoute = require('./api/attributes');
|
||||
const scriptRoute = require('./api/script');
|
||||
const senderRoute = require('./api/sender');
|
||||
const filesRoute = require('./api/file_upload');
|
||||
@@ -133,14 +132,12 @@ function register(app) {
|
||||
|
||||
route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
|
||||
|
||||
apiRoute(GET, '/api/notes/:noteId/labels', labelsRoute.getNoteLabels);
|
||||
apiRoute(PUT, '/api/notes/:noteId/labels', labelsRoute.updateNoteLabels);
|
||||
apiRoute(GET, '/api/labels/names', labelsRoute.getAllLabelNames);
|
||||
apiRoute(GET, '/api/labels/values/:labelName', labelsRoute.getValuesForLabel);
|
||||
|
||||
apiRoute(GET, '/api/notes/:noteId/relations', relationsRoute.getNoteRelations);
|
||||
apiRoute(PUT, '/api/notes/:noteId/relations', relationsRoute.updateNoteRelations);
|
||||
apiRoute(GET, '/api/relations/names', relationsRoute.getAllRelationNames);
|
||||
apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes);
|
||||
apiRoute(PUT, '/api/notes/:noteId/attributes', attributesRoute.updateNoteAttributes);
|
||||
apiRoute(PUT, '/api/notes/:noteId/attribute', attributesRoute.updateNoteAttribute);
|
||||
apiRoute(DELETE, '/api/notes/:noteId/attributes/:attributeId', attributesRoute.deleteNoteAttribute);
|
||||
apiRoute(GET, '/api/attributes/names', attributesRoute.getAttributeNames);
|
||||
apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute);
|
||||
|
||||
route(GET, '/api/images/:imageId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage);
|
||||
route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddleware], imageRoute.uploadImage, apiResultHandler);
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
|
||||
const APP_DB_VERSION = 107;
|
||||
const APP_DB_VERSION = 111;
|
||||
const SYNC_VERSION = 1;
|
||||
|
||||
module.exports = {
|
||||
|
||||
87
src/services/attributes.js
Normal file
87
src/services/attributes.js
Normal file
@@ -0,0 +1,87 @@
|
||||
"use strict";
|
||||
|
||||
const repository = require('./repository');
|
||||
const sql = require('./sql');
|
||||
const utils = require('./utils');
|
||||
const Attribute = require('../entities/attribute');
|
||||
|
||||
const BUILTIN_ATTRIBUTES = [
|
||||
// label names
|
||||
{ type: 'label', name: 'disableVersioning' },
|
||||
{ type: 'label', name: 'calendarRoot' },
|
||||
{ type: 'label', name: 'archived' },
|
||||
{ type: 'label', name: 'excludeFromExport' },
|
||||
{ type: 'label', name: 'run' },
|
||||
{ type: 'label', name: 'manualTransactionHandling' },
|
||||
{ type: 'label', name: 'disableInclusion' },
|
||||
{ type: 'label', name: 'appCss' },
|
||||
{ type: 'label', name: 'hideChildrenOverview' },
|
||||
|
||||
// relation names
|
||||
{ type: 'relation', name: 'runOnNoteView' },
|
||||
{ type: 'relation', name: 'runOnNoteTitleChange' },
|
||||
{ type: 'relation', name: 'runOnAttributeChange' },
|
||||
{ type: 'relation', name: 'inheritAttributes' }
|
||||
];
|
||||
|
||||
async function getNotesWithLabel(name, value) {
|
||||
let notes;
|
||||
|
||||
if (value !== undefined) {
|
||||
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
|
||||
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
|
||||
}
|
||||
else {
|
||||
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
|
||||
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ?`, [name]);
|
||||
}
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
async function getNoteWithLabel(name, value) {
|
||||
const notes = await getNotesWithLabel(name, value);
|
||||
|
||||
return notes.length > 0 ? notes[0] : null;
|
||||
}
|
||||
|
||||
async function createLabel(noteId, name, value = "") {
|
||||
return await createAttribute({
|
||||
noteId: noteId,
|
||||
type: 'label',
|
||||
name: name,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
async function createAttribute(attribute) {
|
||||
return await new Attribute(attribute).save();
|
||||
}
|
||||
|
||||
async function getAttributeNames(type, nameLike) {
|
||||
const names = await sql.getColumn(
|
||||
`SELECT DISTINCT name
|
||||
FROM attributes
|
||||
WHERE isDeleted = 0
|
||||
AND type = ?
|
||||
AND name LIKE '%${utils.sanitizeSql(nameLike)}%'`, [ type ]);
|
||||
|
||||
for (const attribute of BUILTIN_ATTRIBUTES) {
|
||||
if (attribute.type === type && !names.includes(attribute.name)) {
|
||||
names.push(attribute.name);
|
||||
}
|
||||
}
|
||||
|
||||
names.sort();
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNotesWithLabel,
|
||||
getNoteWithLabel,
|
||||
createLabel,
|
||||
createAttribute,
|
||||
getAttributeNames,
|
||||
BUILTIN_ATTRIBUTES
|
||||
};
|
||||
@@ -10,49 +10,32 @@ const syncMutexService = require('./sync_mutex');
|
||||
const cls = require('./cls');
|
||||
|
||||
async function regularBackup() {
|
||||
const now = new Date();
|
||||
const lastBackupDate = dateUtils.parseDateTime(await optionService.getOption('lastBackupDate'));
|
||||
await periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
|
||||
|
||||
console.log(lastBackupDate);
|
||||
await periodBackup('lastWeeklyBackupDate', 'weekly', 7 * 24 * 3600);
|
||||
|
||||
if (now.getTime() - lastBackupDate.getTime() > 43200 * 1000) {
|
||||
await backupNow();
|
||||
}
|
||||
|
||||
await cleanupOldBackups();
|
||||
await periodBackup('lastMonthlyBackupDate', 'monthly', 30 * 24 * 3600);
|
||||
}
|
||||
|
||||
async function backupNow() {
|
||||
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
|
||||
async function periodBackup(optionName, fileName, periodInSeconds) {
|
||||
const now = new Date();
|
||||
const lastDailyBackupDate = dateUtils.parseDateTime(await optionService.getOption(optionName));
|
||||
|
||||
if (now.getTime() - lastDailyBackupDate.getTime() > periodInSeconds * 1000) {
|
||||
await backupNow(fileName);
|
||||
|
||||
await optionService.setOption(optionName, dateUtils.nowDate());
|
||||
}
|
||||
}
|
||||
|
||||
async function backupNow(name) {
|
||||
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
|
||||
await syncMutexService.doExclusively(async () => {
|
||||
const backupFile = dataDir.BACKUP_DIR + "/" + "backup-" + dateUtils.getDateTimeForFile() + ".db";
|
||||
const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`;
|
||||
|
||||
fs.copySync(dataDir.DOCUMENT_PATH, backupFile);
|
||||
|
||||
log.info("Created backup at " + backupFile);
|
||||
|
||||
await optionService.setOption('lastBackupDate', dateUtils.nowDate());
|
||||
});
|
||||
}
|
||||
|
||||
async function cleanupOldBackups() {
|
||||
const now = new Date();
|
||||
|
||||
fs.readdirSync(dataDir.BACKUP_DIR).forEach(file => {
|
||||
const match = file.match(/backup-([0-9 -:]+)\.db/);
|
||||
|
||||
if (match) {
|
||||
const date_str = match[1];
|
||||
|
||||
const date = Date.parse(date_str);
|
||||
|
||||
if (now.getTime() - date.getTime() > 30 * 24 * 3600 * 1000) {
|
||||
log.info("Removing old backup - " + file);
|
||||
|
||||
fs.unlink(dataDir.BACKUP_DIR + "/" + file);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2018-07-30T08:18:25+02:00", buildRevision: "2ff7a890bceec873a082e496afb349b93f787985" };
|
||||
module.exports = { buildDate:"2018-08-14T14:19:37+02:00", buildRevision: "fec157444787ad3dbe01a5052cb01e5374bdcb79" };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = function(labelFilters) {
|
||||
module.exports = function(attributeFilters) {
|
||||
const joins = [];
|
||||
const joinParams = [];
|
||||
let where = '1';
|
||||
@@ -6,31 +6,31 @@ module.exports = function(labelFilters) {
|
||||
|
||||
let i = 1;
|
||||
|
||||
for (const filter of labelFilters) {
|
||||
joins.push(`LEFT JOIN labels AS label${i} ON label${i}.noteId = notes.noteId AND label${i}.name = ?`);
|
||||
for (const filter of attributeFilters) {
|
||||
joins.push(`LEFT JOIN attributes AS attribute${i} ON attribute${i}.noteId = notes.noteId AND attribute${i}.name = ? AND attribute${i}.isDeleted = 0`);
|
||||
joinParams.push(filter.name);
|
||||
|
||||
where += " " + filter.relation + " ";
|
||||
|
||||
if (filter.operator === 'exists') {
|
||||
where += `label${i}.labelId IS NOT NULL`;
|
||||
where += `attribute${i}.attributeId IS NOT NULL`;
|
||||
}
|
||||
else if (filter.operator === 'not-exists') {
|
||||
where += `label${i}.labelId IS NULL`;
|
||||
where += `attribute${i}.attributeId IS NULL`;
|
||||
}
|
||||
else if (filter.operator === '=' || filter.operator === '!=') {
|
||||
where += `label${i}.value ${filter.operator} ?`;
|
||||
where += `attribute${i}.value ${filter.operator} ?`;
|
||||
whereParams.push(filter.value);
|
||||
}
|
||||
else if ([">", ">=", "<", "<="].includes(filter.operator)) {
|
||||
const floatParam = parseFloat(filter.value);
|
||||
|
||||
if (isNaN(floatParam)) {
|
||||
where += `label${i}.value ${filter.operator} ?`;
|
||||
where += `attribute${i}.value ${filter.operator} ?`;
|
||||
whereParams.push(filter.value);
|
||||
}
|
||||
else {
|
||||
where += `CAST(label${i}.value AS DECIMAL) ${filter.operator} ?`;
|
||||
where += `CAST(attribute${i}.value AS DECIMAL) ${filter.operator} ?`;
|
||||
whereParams.push(floatParam);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
const sql = require('./sql');
|
||||
const syncTable = require('./sync_table');
|
||||
const tree = require('./tree');
|
||||
const treeService = require('./tree');
|
||||
const noteService = require('./notes');
|
||||
const repository = require('./repository');
|
||||
const Branch = require('../entities/branch');
|
||||
|
||||
async function cloneNoteToParent(noteId, parentNoteId, prefix) {
|
||||
const validationResult = await tree.validateParentChild(parentNoteId, noteId);
|
||||
const validationResult = await treeService.validateParentChild(parentNoteId, noteId);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return validationResult;
|
||||
@@ -24,10 +26,42 @@ async function cloneNoteToParent(noteId, parentNoteId, prefix) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function cloneNoteAfter(noteId, afterBranchId) {
|
||||
const afterNote = await tree.getBranch(afterBranchId);
|
||||
async function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
|
||||
const validationResult = await treeService.validateParentChild(parentNoteId, noteId);
|
||||
|
||||
const validationResult = await tree.validateParentChild(afterNote.parentNoteId, noteId);
|
||||
if (!validationResult.success) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
await new Branch({
|
||||
noteId: noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
prefix: prefix,
|
||||
isExpanded: 0
|
||||
}).save();
|
||||
}
|
||||
|
||||
async function ensureNoteIsAbsentFromParent(noteId, parentNoteId) {
|
||||
const branch = await repository.getEntity(`SELECT * FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0`, [noteId, parentNoteId]);
|
||||
|
||||
if (branch) {
|
||||
await noteService.deleteNote(branch);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleNoteInParent(present, noteId, parentNoteId, prefix) {
|
||||
if (present) {
|
||||
await ensureNoteIsPresentInParent(noteId, parentNoteId, prefix);
|
||||
}
|
||||
else {
|
||||
await ensureNoteIsAbsentFromParent(noteId, parentNoteId);
|
||||
}
|
||||
}
|
||||
|
||||
async function cloneNoteAfter(noteId, afterBranchId) {
|
||||
const afterNote = await treeService.getBranch(afterBranchId);
|
||||
|
||||
const validationResult = await treeService.validateParentChild(afterNote.parentNoteId, noteId);
|
||||
|
||||
if (!validationResult.result) {
|
||||
return validationResult;
|
||||
@@ -52,5 +86,8 @@ async function cloneNoteAfter(noteId, afterBranchId) {
|
||||
|
||||
module.exports = {
|
||||
cloneNoteToParent,
|
||||
ensureNoteIsPresentInParent,
|
||||
ensureNoteIsAbsentFromParent,
|
||||
toggleNoteInParent,
|
||||
cloneNoteAfter
|
||||
};
|
||||
@@ -3,7 +3,6 @@
|
||||
const sql = require('./sql');
|
||||
const sqlInit = require('./sql_init');
|
||||
const log = require('./log');
|
||||
const utils = require('./utils');
|
||||
const messagingService = require('./messaging');
|
||||
const syncMutexService = require('./sync_mutex');
|
||||
const cls = require('./cls');
|
||||
@@ -223,7 +222,7 @@ async function runAllChecks() {
|
||||
await runSyncRowChecks("recent_notes", "branchId", errorList);
|
||||
await runSyncRowChecks("images", "imageId", errorList);
|
||||
await runSyncRowChecks("note_images", "noteImageId", errorList);
|
||||
await runSyncRowChecks("labels", "labelId", errorList);
|
||||
await runSyncRowChecks("attributes", "attributeId", errorList);
|
||||
await runSyncRowChecks("api_tokens", "apiTokenId", errorList);
|
||||
|
||||
if (errorList.length === 0) {
|
||||
|
||||
@@ -9,8 +9,8 @@ const ApiToken = require('../entities/api_token');
|
||||
const Branch = require('../entities/branch');
|
||||
const Image = require('../entities/image');
|
||||
const Note = require('../entities/note');
|
||||
const Attribute = require('../entities/attribute');
|
||||
const NoteImage = require('../entities/note_image');
|
||||
const Label = require('../entities/label');
|
||||
const NoteRevision = require('../entities/note_revision');
|
||||
const RecentNote = require('../entities/recent_note');
|
||||
const Option = require('../entities/option');
|
||||
@@ -40,7 +40,7 @@ async function getHashes() {
|
||||
options: await getHash(Option, "isSynced = 1"),
|
||||
images: await getHash(Image),
|
||||
note_images: await getHash(NoteImage),
|
||||
labels: await getHash(Label),
|
||||
attributes: await getHash(Attribute),
|
||||
api_tokens: await getHash(ApiToken)
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('./sql');
|
||||
const noteService = require('./notes');
|
||||
const labelService = require('./labels');
|
||||
const attributeService = require('./attributes');
|
||||
const dateUtils = require('./date_utils');
|
||||
const repository = require('./repository');
|
||||
|
||||
@@ -32,7 +31,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
|
||||
|
||||
async function getRootCalendarNote() {
|
||||
// some caching here could be useful (e.g. in CLS)
|
||||
let rootNote = await labelService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
|
||||
let rootNote = await attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
|
||||
|
||||
if (!rootNote) {
|
||||
rootNote = (await noteService.createNewNote('root', {
|
||||
@@ -41,7 +40,8 @@ async function getRootCalendarNote() {
|
||||
isProtected: false
|
||||
})).note;
|
||||
|
||||
await labelService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL);
|
||||
await attributeService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL);
|
||||
await attributeService.createLabel(rootNote.noteId, 'sorted');
|
||||
}
|
||||
|
||||
return rootNote;
|
||||
@@ -50,7 +50,7 @@ async function getRootCalendarNote() {
|
||||
async function getYearNote(dateTimeStr, rootNote) {
|
||||
const yearStr = dateTimeStr.substr(0, 4);
|
||||
|
||||
let yearNote = await labelService.getNoteWithLabel(YEAR_LABEL, yearStr);
|
||||
let yearNote = await attributeService.getNoteWithLabel(YEAR_LABEL, yearStr);
|
||||
|
||||
if (!yearNote) {
|
||||
yearNote = await getNoteStartingWith(rootNote.noteId, yearStr);
|
||||
@@ -59,7 +59,8 @@ async function getYearNote(dateTimeStr, rootNote) {
|
||||
yearNote = await createNote(rootNote.noteId, yearStr);
|
||||
}
|
||||
|
||||
await labelService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr);
|
||||
await attributeService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr);
|
||||
await attributeService.createLabel(yearNote.noteId, 'sorted');
|
||||
}
|
||||
|
||||
return yearNote;
|
||||
@@ -69,7 +70,7 @@ async function getMonthNote(dateTimeStr, rootNote) {
|
||||
const monthStr = dateTimeStr.substr(0, 7);
|
||||
const monthNumber = dateTimeStr.substr(5, 2);
|
||||
|
||||
let monthNote = await labelService.getNoteWithLabel(MONTH_LABEL, monthStr);
|
||||
let monthNote = await attributeService.getNoteWithLabel(MONTH_LABEL, monthStr);
|
||||
|
||||
if (!monthNote) {
|
||||
const yearNote = await getYearNote(dateTimeStr, rootNote);
|
||||
@@ -84,7 +85,8 @@ async function getMonthNote(dateTimeStr, rootNote) {
|
||||
monthNote = await createNote(yearNote.noteId, noteTitle);
|
||||
}
|
||||
|
||||
await labelService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
|
||||
await attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
|
||||
await attributeService.createLabel(monthNote.noteId, 'sorted');
|
||||
}
|
||||
|
||||
return monthNote;
|
||||
@@ -96,7 +98,7 @@ async function getDateNote(dateTimeStr) {
|
||||
const dateStr = dateTimeStr.substr(0, 10);
|
||||
const dayNumber = dateTimeStr.substr(8, 2);
|
||||
|
||||
let dateNote = await labelService.getNoteWithLabel(DATE_LABEL, dateStr);
|
||||
let dateNote = await attributeService.getNoteWithLabel(DATE_LABEL, dateStr);
|
||||
|
||||
if (!dateNote) {
|
||||
const monthNote = await getMonthNote(dateTimeStr, rootNote);
|
||||
@@ -111,7 +113,7 @@ async function getDateNote(dateTimeStr) {
|
||||
dateNote = await createNote(monthNote.noteId, noteTitle);
|
||||
}
|
||||
|
||||
await labelService.createLabel(dateNote.noteId, DATE_LABEL, dateStr);
|
||||
await attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr);
|
||||
}
|
||||
|
||||
return dateNote;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
const log = require('./log');
|
||||
|
||||
const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED";
|
||||
const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
|
||||
const ENTITY_CHANGED = "ENTITY_CHANGED";
|
||||
|
||||
@@ -8,13 +11,18 @@ function subscribe(eventType, listener) {
|
||||
eventListeners[eventType].push(listener);
|
||||
}
|
||||
|
||||
function emit(eventType, data) {
|
||||
async function emit(eventType, data) {
|
||||
const listeners = eventListeners[eventType];
|
||||
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
// not awaiting for async processing
|
||||
listener(data);
|
||||
try {
|
||||
await listener(data);
|
||||
}
|
||||
catch (e) {
|
||||
log.error("Listener threw error: " + e.stack);
|
||||
// we won't stop execution because of listener
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +31,7 @@ module.exports = {
|
||||
subscribe,
|
||||
emit,
|
||||
// event types:
|
||||
NOTE_TITLE_CHANGED,
|
||||
ENTER_PROTECTED_SESSION,
|
||||
ENTITY_CHANGED
|
||||
};
|
||||
40
src/services/handlers.js
Normal file
40
src/services/handlers.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const eventService = require('./events');
|
||||
const scriptService = require('./script');
|
||||
const treeService = require('./tree');
|
||||
const messagingService = require('./messaging');
|
||||
const repository = require('./repository');
|
||||
|
||||
async function runAttachedRelations(note, relationName, originEntity) {
|
||||
const attributes = await note.getAttributes();
|
||||
const runRelations = attributes.filter(relation => relation.type === 'relation' && relation.name === relationName);
|
||||
|
||||
for (const relation of runRelations) {
|
||||
const scriptNote = await relation.getTargetNote();
|
||||
|
||||
await scriptService.executeNote(scriptNote, originEntity);
|
||||
}
|
||||
}
|
||||
|
||||
eventService.subscribe(eventService.NOTE_TITLE_CHANGED, async note => {
|
||||
await runAttachedRelations(note, 'runOnNoteTitleChange', note);
|
||||
|
||||
if (!note.isRoot()) {
|
||||
const parents = await note.getParentNotes();
|
||||
|
||||
for (const parent of parents) {
|
||||
if (await parent.hasLabel("sorted")) {
|
||||
await treeService.sortNotesAlphabetically(parent.noteId);
|
||||
|
||||
messagingService.sendMessageToAllClients({ type: 'refresh-tree' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityId, entityName }) => {
|
||||
if (entityName === 'attributes') {
|
||||
const attribute = await repository.getEntityFromName(entityName, entityId);
|
||||
|
||||
await runAttachedRelations(await attribute.getNote(), 'runOnAttributeChange', attribute);
|
||||
}
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const repository = require('./repository');
|
||||
const Label = require('../entities/label');
|
||||
|
||||
const BUILTIN_LABELS = [
|
||||
'disableVersioning',
|
||||
'calendarRoot',
|
||||
'archived',
|
||||
'excludeFromExport',
|
||||
'run',
|
||||
'manualTransactionHandling',
|
||||
'disableInclusion',
|
||||
'appCss',
|
||||
'hideChildrenOverview'
|
||||
];
|
||||
|
||||
async function getNotesWithLabel(name, value) {
|
||||
let notes;
|
||||
|
||||
if (value !== undefined) {
|
||||
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN labels USING(noteId)
|
||||
WHERE notes.isDeleted = 0 AND labels.isDeleted = 0 AND labels.name = ? AND labels.value = ?`, [name, value]);
|
||||
}
|
||||
else {
|
||||
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN labels USING(noteId)
|
||||
WHERE notes.isDeleted = 0 AND labels.isDeleted = 0 AND labels.name = ?`, [name]);
|
||||
}
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
async function getNoteWithLabel(name, value) {
|
||||
const notes = await getNotesWithLabel(name, value);
|
||||
|
||||
return notes.length > 0 ? notes[0] : null;
|
||||
}
|
||||
|
||||
async function createLabel(noteId, name, value = "") {
|
||||
return await new Label({
|
||||
noteId: noteId,
|
||||
name: name,
|
||||
value: value
|
||||
}).save();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNotesWithLabel,
|
||||
getNoteWithLabel,
|
||||
createLabel,
|
||||
BUILTIN_LABELS
|
||||
};
|
||||
@@ -10,7 +10,7 @@ async function migrate() {
|
||||
const migrations = [];
|
||||
|
||||
// backup before attempting migration
|
||||
await backupService.backupNow();
|
||||
await backupService.backupNow("before-migration");
|
||||
|
||||
const currentDbVersion = parseInt(await optionService.getOption('dbVersion'));
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ let protectedNoteTitles;
|
||||
let noteIds;
|
||||
let childParentToBranchId = {};
|
||||
const childToParent = {};
|
||||
const archived = {};
|
||||
let archived = {};
|
||||
|
||||
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
|
||||
let prefixes = {};
|
||||
@@ -30,17 +30,13 @@ async function load() {
|
||||
childParentToBranchId[`${rel.noteId}-${rel.parentNoteId}`] = rel.branchId;
|
||||
}
|
||||
|
||||
const hiddenLabels = await sql.getColumn(`SELECT noteId FROM labels WHERE isDeleted = 0 AND name = 'archived'`);
|
||||
|
||||
for (const noteId of hiddenLabels) {
|
||||
archived[noteId] = true;
|
||||
}
|
||||
archived = await sql.getMap(`SELECT noteId, isInheritable FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name = 'archived'`);
|
||||
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
function findNotes(query) {
|
||||
if (!noteTitles || query.length <= 2) {
|
||||
if (!noteTitles || !query.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -54,7 +50,8 @@ function findNotes(query) {
|
||||
}
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
if (archived[noteId]) {
|
||||
// for leaf note it doesn't matter if "archived" label inheritable or not
|
||||
if (noteId in archived) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -64,7 +61,8 @@ function findNotes(query) {
|
||||
}
|
||||
|
||||
for (const parentNoteId of parents) {
|
||||
if (archived[parentNoteId]) {
|
||||
// for parent note archived needs to be inheritable
|
||||
if (archived[parentNoteId] === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -111,7 +109,7 @@ function search(noteId, tokens, path, results) {
|
||||
}
|
||||
|
||||
const parents = childToParent[noteId];
|
||||
if (!parents) {
|
||||
if (!parents || noteId === 'root') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,11 +118,12 @@ function search(noteId, tokens, path, results) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentNoteId === 'root' || archived[parentNoteId]) {
|
||||
// archived must be inheritable
|
||||
if (archived[parentNoteId] === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const title = getNoteTitle(noteId, parentNoteId);
|
||||
const title = getNoteTitle(noteId, parentNoteId).toLowerCase();
|
||||
const foundTokens = [];
|
||||
|
||||
for (const token of tokens) {
|
||||
@@ -198,7 +197,8 @@ function getSomePath(noteId, path) {
|
||||
}
|
||||
|
||||
for (const parentNoteId of parents) {
|
||||
if (archived[parentNoteId]) {
|
||||
// archived applies here only if inheritable
|
||||
if (archived[parentNoteId] === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -265,19 +265,19 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId
|
||||
childParentToBranchId[branch.noteId + '-' + branch.parentNoteId] = branch.branchId;
|
||||
}
|
||||
}
|
||||
else if (entityName === 'labels') {
|
||||
const label = await repository.getLabel(entityId);
|
||||
else if (entityName === 'attributes') {
|
||||
const attribute = await repository.getAttribute(entityId);
|
||||
|
||||
if (label.name === 'archived') {
|
||||
if (attribute.type === 'label' && attribute.name === 'archived') {
|
||||
// we're not using label object directly, since there might be other non-deleted archived label
|
||||
const hideLabel = await repository.getEntity(`SELECT * FROM labels WHERE isDeleted = 0
|
||||
AND name = 'archived' AND noteId = ?`, [label.noteId]);
|
||||
const hideLabel = await repository.getEntity(`SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label'
|
||||
AND name = 'archived' AND noteId = ?`, [attribute.noteId]);
|
||||
|
||||
if (hideLabel) {
|
||||
archived[label.noteId] = true;
|
||||
archived[attribute.noteId] = hideLabel.isInheritable ? 1 : 0;
|
||||
}
|
||||
else {
|
||||
delete archived[label.noteId];
|
||||
delete archived[attribute.noteId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ const sql = require('./sql');
|
||||
const optionService = require('./options');
|
||||
const dateUtils = require('./date_utils');
|
||||
const syncTableService = require('./sync_table');
|
||||
const labelService = require('./labels');
|
||||
const attributeService = require('./attributes');
|
||||
const eventService = require('./events');
|
||||
const repository = require('./repository');
|
||||
const Note = require('../entities/note');
|
||||
const NoteImage = require('../entities/note_image');
|
||||
@@ -34,6 +35,10 @@ async function getNewNotePosition(parentNoteId, noteData) {
|
||||
return newNotePos;
|
||||
}
|
||||
|
||||
async function triggerNoteTitleChanged(note) {
|
||||
await eventService.emit(eventService.NOTE_TITLE_CHANGED, note);
|
||||
}
|
||||
|
||||
async function createNewNote(parentNoteId, noteData) {
|
||||
const newNotePos = await getNewNotePosition(parentNoteId, noteData);
|
||||
|
||||
@@ -60,6 +65,8 @@ async function createNewNote(parentNoteId, noteData) {
|
||||
isExpanded: 0
|
||||
}).save();
|
||||
|
||||
await triggerNoteTitleChanged(note);
|
||||
|
||||
return {
|
||||
note,
|
||||
branch
|
||||
@@ -86,12 +93,17 @@ async function createNote(parentNoteId, title, content = "", extraOptions = {})
|
||||
|
||||
const {note, branch} = await createNewNote(parentNoteId, noteData);
|
||||
|
||||
if (extraOptions.labels) {
|
||||
for (const labelName in extraOptions.labels) {
|
||||
await labelService.createLabel(note.noteId, labelName, extraOptions.labels[labelName]);
|
||||
}
|
||||
for (const attr of extraOptions.attributes || []) {
|
||||
await attributeService.createAttribute({
|
||||
noteId: note.noteId,
|
||||
type: attr.type,
|
||||
name: attr.name,
|
||||
value: attr.value
|
||||
});
|
||||
}
|
||||
|
||||
await triggerNoteTitleChanged(note);
|
||||
|
||||
return {note, branch};
|
||||
}
|
||||
|
||||
@@ -159,8 +171,6 @@ async function saveNoteImages(note) {
|
||||
}
|
||||
|
||||
async function saveNoteRevision(note) {
|
||||
const labelsMap = await note.getLabelMap();
|
||||
|
||||
const now = new Date();
|
||||
const noteRevisionSnapshotTimeInterval = parseInt(await optionService.getOption('noteRevisionSnapshotTimeInterval'));
|
||||
|
||||
@@ -172,7 +182,7 @@ async function saveNoteRevision(note) {
|
||||
const msSinceDateCreated = now.getTime() - dateUtils.parseDateTime(note.dateCreated).getTime();
|
||||
|
||||
if (note.type !== 'file'
|
||||
&& labelsMap.disableVersioning !== 'true'
|
||||
&& await note.hasLabel('disableVersioning')
|
||||
&& !existingnoteRevisionId
|
||||
&& msSinceDateCreated >= noteRevisionSnapshotTimeInterval * 1000) {
|
||||
|
||||
@@ -183,7 +193,7 @@ async function saveNoteRevision(note) {
|
||||
content: note.content,
|
||||
type: note.type,
|
||||
mime: note.mime,
|
||||
isProtected: 0, // will be fixed in the protectNoteRevisions() call
|
||||
isProtected: false, // will be fixed in the protectNoteRevisions() call
|
||||
dateModifiedFrom: note.dateModified,
|
||||
dateModifiedTo: dateUtils.nowDate()
|
||||
}).save();
|
||||
@@ -200,18 +210,24 @@ async function updateNote(noteId, noteUpdates) {
|
||||
|
||||
await saveNoteRevision(note);
|
||||
|
||||
const noteTitleChanged = note.title !== noteUpdates.title;
|
||||
|
||||
note.title = noteUpdates.title;
|
||||
note.setContent(noteUpdates.content);
|
||||
note.isProtected = noteUpdates.isProtected;
|
||||
await note.save();
|
||||
|
||||
if (noteTitleChanged) {
|
||||
await triggerNoteTitleChanged(note);
|
||||
}
|
||||
|
||||
await saveNoteImages(note);
|
||||
|
||||
await protectNoteRevisions(note);
|
||||
}
|
||||
|
||||
async function deleteNote(branch) {
|
||||
if (!branch || branch.isDeleted === 1) {
|
||||
if (!branch || branch.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ async function initSyncedOptions(username, password) {
|
||||
|
||||
async function initNotSyncedOptions(initialized, startNotePath = 'root', syncServerHost = '', syncProxy = '') {
|
||||
await optionService.createOption('startNotePath', startNotePath, false);
|
||||
await optionService.createOption('lastBackupDate', dateUtils.nowDate(), false);
|
||||
await optionService.createOption('lastDailyBackupDate', dateUtils.nowDate(), false);
|
||||
await optionService.createOption('lastWeeklyBackupDate', dateUtils.nowDate(), false);
|
||||
await optionService.createOption('lastMonthlyBackupDate', dateUtils.nowDate(), false);
|
||||
await optionService.createOption('dbVersion', appInfo.dbVersion, false);
|
||||
|
||||
await optionService.createOption('lastSyncedPull', 0, false);
|
||||
|
||||
10
src/services/port.js
Normal file
10
src/services/port.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const getPort = require('get-port');
|
||||
const config = require('./config');
|
||||
const utils = require('./utils');
|
||||
|
||||
if (utils.isElectron()) {
|
||||
module.exports = getPort();
|
||||
}
|
||||
else {
|
||||
module.exports = Promise.resolve(config['Network']['port'] || '3000');
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const repository = require('./repository');
|
||||
const Relation = require('../entities/relation');
|
||||
|
||||
const BUILTIN_RELATIONS = [
|
||||
'runOnNoteView'
|
||||
];
|
||||
|
||||
async function getNotesWithRelation(name, targetNoteId) {
|
||||
let notes;
|
||||
|
||||
if (targetNoteId !== undefined) {
|
||||
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN relations ON notes.noteId = relations.sourceNoteId
|
||||
WHERE notes.isDeleted = 0 AND relations.isDeleted = 0 AND relations.name = ? AND relations.targetNoteId = ?`, [name, targetNoteId]);
|
||||
}
|
||||
else {
|
||||
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN relations ON notes.noteId = relations.sourceNoteId
|
||||
WHERE notes.isDeleted = 0 AND relations.isDeleted = 0 AND relations.name = ?`, [name]);
|
||||
}
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
async function getNoteWithRelation(name, value) {
|
||||
const notes = await getNotesWithRelation(name, value);
|
||||
|
||||
return notes.length > 0 ? notes[0] : null;
|
||||
}
|
||||
|
||||
async function createRelation(sourceNoteId, name, targetNoteId) {
|
||||
return await new Relation({
|
||||
sourceNoteId: sourceNoteId,
|
||||
name: name,
|
||||
targetNoteId: targetNoteId
|
||||
}).save();
|
||||
}
|
||||
|
||||
async function getEffectiveRelations(noteId) {
|
||||
return await repository.getEntities(`
|
||||
WITH RECURSIVE tree(noteId) AS (
|
||||
SELECT ?
|
||||
UNION
|
||||
SELECT branches.parentNoteId FROM branches
|
||||
JOIN tree ON branches.noteId = tree.noteId
|
||||
JOIN notes ON notes.noteId = branches.parentNoteId
|
||||
WHERE notes.isDeleted = 0 AND branches.isDeleted = 0
|
||||
)
|
||||
SELECT relations.* FROM relations JOIN tree ON relations.sourceNoteId = tree.noteId
|
||||
WHERE relations.isDeleted = 0 AND (relations.isInheritable = 1 OR relations.sourceNoteId = ?)`, [noteId, noteId]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BUILTIN_RELATIONS,
|
||||
getNotesWithRelation,
|
||||
getNoteWithRelation,
|
||||
createRelation,
|
||||
getEffectiveRelations
|
||||
};
|
||||
@@ -9,10 +9,20 @@ async function setEntityConstructor(constructor) {
|
||||
entityConstructor = constructor;
|
||||
}
|
||||
|
||||
async function getEntityFromName(entityName, entityId) {
|
||||
if (!entityName || !entityId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const constructor = entityConstructor.getEntityFromTableName(entityName);
|
||||
|
||||
return await getEntity(`SELECT * FROM ${constructor.tableName} WHERE ${constructor.primaryKeyName} = ?`, [entityId]);
|
||||
}
|
||||
|
||||
async function getEntities(query, params = []) {
|
||||
const rows = await sql.getRows(query, params);
|
||||
|
||||
return rows.map(entityConstructor);
|
||||
return rows.map(entityConstructor.createEntityFromRow);
|
||||
}
|
||||
|
||||
async function getEntity(query, params = []) {
|
||||
@@ -22,7 +32,7 @@ async function getEntity(query, params = []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return entityConstructor(row);
|
||||
return entityConstructor.createEntityFromRow(row);
|
||||
}
|
||||
|
||||
async function getNote(noteId) {
|
||||
@@ -37,12 +47,8 @@ async function getImage(imageId) {
|
||||
return await getEntity("SELECT * FROM images WHERE imageId = ?", [imageId]);
|
||||
}
|
||||
|
||||
async function getLabel(labelId) {
|
||||
return await getEntity("SELECT * FROM labels WHERE labelId = ?", [labelId]);
|
||||
}
|
||||
|
||||
async function getRelation(relationId) {
|
||||
return await getEntity("SELECT * FROM relations WHERE relationId = ?", [relationId]);
|
||||
async function getAttribute(attributeId) {
|
||||
return await getEntity("SELECT * FROM attributes WHERE attributeId = ?", [attributeId]);
|
||||
}
|
||||
|
||||
async function getOption(name) {
|
||||
@@ -57,26 +63,36 @@ async function updateEntity(entity) {
|
||||
const clone = Object.assign({}, entity);
|
||||
|
||||
delete clone.jsonContent;
|
||||
delete clone.isOwned;
|
||||
delete clone.isChanged;
|
||||
delete clone.__attributeCache;
|
||||
|
||||
for (const key in clone) {
|
||||
// !isBuffer is for images and attachments
|
||||
if (clone[key] !== null && typeof clone[key] === 'object' && !Buffer.isBuffer(clone[key])) {
|
||||
clone[key] = JSON.stringify(clone[key]);
|
||||
}
|
||||
}
|
||||
|
||||
await sql.transactional(async () => {
|
||||
await sql.replace(entity.constructor.tableName, clone);
|
||||
|
||||
const primaryKey = entity[entity.constructor.primaryKeyName];
|
||||
|
||||
if (entity.constructor.tableName !== 'options' || entity.isSynced) {
|
||||
if (entity.isChanged && (entity.constructor.tableName !== 'options' || entity.isSynced)) {
|
||||
await syncTableService.addEntitySync(entity.constructor.tableName, primaryKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getEntityFromName,
|
||||
getEntities,
|
||||
getEntity,
|
||||
getNote,
|
||||
getBranch,
|
||||
getImage,
|
||||
getLabel,
|
||||
getRelation,
|
||||
getAttribute,
|
||||
getOption,
|
||||
updateEntity,
|
||||
setEntityConstructor
|
||||
|
||||
@@ -7,16 +7,17 @@ async function runNotesWithLabel(runAttrValue) {
|
||||
const notes = await repository.getEntities(`
|
||||
SELECT notes.*
|
||||
FROM notes
|
||||
JOIN labels ON labels.noteId = notes.noteId
|
||||
AND labels.isDeleted = 0
|
||||
AND labels.name = 'run'
|
||||
AND labels.value = ?
|
||||
JOIN attributes ON attributes.noteId = notes.noteId
|
||||
AND attributes.isDeleted = 0
|
||||
AND attributes.type = 'label'
|
||||
AND attributes.name = 'run'
|
||||
AND attributes.value = ?
|
||||
WHERE
|
||||
notes.type = 'code'
|
||||
AND notes.isDeleted = 0`, [runAttrValue]);
|
||||
|
||||
for (const note of notes) {
|
||||
scriptService.executeNote(note);
|
||||
scriptService.executeNote(note, note);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ const repository = require('./repository');
|
||||
const cls = require('./cls');
|
||||
const sourceIdService = require('./source_id');
|
||||
|
||||
async function executeNote(note) {
|
||||
async function executeNote(note, originEntity) {
|
||||
if (!note.isJavaScript()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bundle = await getScriptBundle(note);
|
||||
|
||||
await executeBundle(bundle);
|
||||
await executeBundle(bundle, note, originEntity);
|
||||
}
|
||||
|
||||
async function executeBundle(bundle, startNote, targetNote = null) {
|
||||
async function executeBundle(bundle, startNote, originEntity = null) {
|
||||
if (!startNote) {
|
||||
// this is the default case, the only exception is when we want to preserve frontend startNote
|
||||
startNote = bundle.note;
|
||||
@@ -23,13 +23,13 @@ async function executeBundle(bundle, startNote, targetNote = null) {
|
||||
// last \r\n is necessary if script contains line comment on its last line
|
||||
const script = "async function() {\r\n" + bundle.script + "\r\n}";
|
||||
|
||||
const ctx = new ScriptContext(startNote, bundle.allNotes, targetNote);
|
||||
const ctx = new ScriptContext(startNote, bundle.allNotes, originEntity);
|
||||
|
||||
if (await bundle.note.hasLabel('manualTransactionHandling')) {
|
||||
return await execute(ctx, script, '');
|
||||
}
|
||||
else {
|
||||
return await sql.transactional(async () => execute(ctx, script, ''));
|
||||
return await sql.transactional(async () => await execute(ctx, script, ''));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ async function executeBundle(bundle, startNote, targetNote = null) {
|
||||
* This method preserves frontend startNode - that's why we start execution from currentNote and override
|
||||
* bundle's startNote.
|
||||
*/
|
||||
async function executeScript(script, params, startNoteId, currentNoteId, targetNoteId) {
|
||||
async function executeScript(script, params, startNoteId, currentNoteId, originEntityName, originEntityId) {
|
||||
const startNote = await repository.getNote(startNoteId);
|
||||
const currentNote = await repository.getNote(currentNoteId);
|
||||
const targetNote = await repository.getNote(targetNoteId);
|
||||
const originEntity = await repository.getEntityFromName(originEntityName, originEntityId);
|
||||
|
||||
currentNote.content = `return await (${script}\r\n)(${getParams(params)})`;
|
||||
currentNote.type = 'code';
|
||||
@@ -48,7 +48,7 @@ async function executeScript(script, params, startNoteId, currentNoteId, targetN
|
||||
|
||||
const bundle = await getScriptBundle(currentNote);
|
||||
|
||||
return await executeBundle(bundle, startNote, targetNote);
|
||||
return await executeBundle(bundle, startNote, originEntity);
|
||||
}
|
||||
|
||||
async function execute(ctx, script, paramsStr) {
|
||||
|
||||
@@ -3,16 +3,19 @@ const noteService = require('./notes');
|
||||
const sql = require('./sql');
|
||||
const utils = require('./utils');
|
||||
const dateUtils = require('./date_utils');
|
||||
const labelService = require('./labels');
|
||||
const attributeService = require('./attributes');
|
||||
const dateNoteService = require('./date_notes');
|
||||
const treeService = require('./tree');
|
||||
const config = require('./config');
|
||||
const repository = require('./repository');
|
||||
const axios = require('axios');
|
||||
const cloningService = require('./cloning');
|
||||
const messagingService = require('./messaging');
|
||||
|
||||
function ScriptContext(startNote, allNotes, targetNote = null) {
|
||||
function ScriptContext(startNote, allNotes, originEntity = null) {
|
||||
this.modules = {};
|
||||
this.notes = utils.toObject(allNotes, note => [note.noteId, note]);
|
||||
this.apis = utils.toObject(allNotes, note => [note.noteId, new ScriptApi(startNote, note, targetNote)]);
|
||||
this.apis = utils.toObject(allNotes, note => [note.noteId, new ScriptApi(startNote, note, originEntity)]);
|
||||
this.require = moduleNoteIds => {
|
||||
return moduleName => {
|
||||
const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
|
||||
@@ -27,10 +30,10 @@ function ScriptContext(startNote, allNotes, targetNote = null) {
|
||||
};
|
||||
}
|
||||
|
||||
function ScriptApi(startNote, currentNote, targetNote) {
|
||||
function ScriptApi(startNote, currentNote, originEntity) {
|
||||
this.startNote = startNote;
|
||||
this.currentNote = currentNote;
|
||||
this.targetNote = targetNote;
|
||||
this.originEntity = originEntity;
|
||||
|
||||
this.axios = axios;
|
||||
|
||||
@@ -44,15 +47,19 @@ function ScriptApi(startNote, currentNote, targetNote) {
|
||||
|
||||
this.getNote = repository.getNote;
|
||||
this.getBranch = repository.getBranch;
|
||||
this.getLabel = repository.getLabel;
|
||||
this.getRelation = repository.getRelation;
|
||||
this.getAttribute = repository.getAttribute;
|
||||
this.getImage = repository.getImage;
|
||||
this.getEntity = repository.getEntity;
|
||||
this.getEntities = repository.getEntities;
|
||||
|
||||
this.createLabel = labelService.createLabel;
|
||||
this.getNotesWithLabel = labelService.getNotesWithLabel;
|
||||
this.getNoteWithLabel = labelService.getNoteWithLabel;
|
||||
this.createAttribute = attributeService.createAttribute;
|
||||
this.getNotesWithLabel = attributeService.getNotesWithLabel;
|
||||
this.getNoteWithLabel = attributeService.getNoteWithLabel;
|
||||
|
||||
this.ensureNoteIsPresentInParent = cloningService.ensureNoteIsPresentInParent;
|
||||
this.ensureNoteIsAbsentFromParent = cloningService.ensureNoteIsAbsentFromParent;
|
||||
|
||||
this.toggleNoteInParent = cloningService.toggleNoteInParent;
|
||||
|
||||
this.createNote = noteService.createNote;
|
||||
|
||||
@@ -61,7 +68,13 @@ function ScriptApi(startNote, currentNote, targetNote) {
|
||||
this.getRootCalendarNote = dateNoteService.getRootCalendarNote;
|
||||
this.getDateNote = dateNoteService.getDateNote;
|
||||
|
||||
this.sortNotesAlphabetically = treeService.sortNotesAlphabetically;
|
||||
|
||||
this.setNoteToParent = treeService.setNoteToParent;
|
||||
|
||||
this.transactional = sql.transactional;
|
||||
|
||||
this.refreshTree = () => messagingService.sendMessageToAllClients({ type: 'refresh-tree' });
|
||||
}
|
||||
|
||||
module.exports = ScriptContext;
|
||||
@@ -235,8 +235,6 @@ const primaryKeys = {
|
||||
"recent_notes": "branchId",
|
||||
"images": "imageId",
|
||||
"note_images": "noteImageId",
|
||||
"labels": "labelId",
|
||||
"relations": "relationId",
|
||||
"api_tokens": "apiTokenId",
|
||||
"options": "name"
|
||||
};
|
||||
@@ -304,31 +302,20 @@ async function setLastSyncedPush(lastSyncedPush) {
|
||||
}
|
||||
|
||||
async function updatePushStats() {
|
||||
const lastSyncedPush = await optionService.getOption('lastSyncedPush');
|
||||
if (await syncOptions.isSyncSetup()) {
|
||||
const lastSyncedPush = await optionService.getOption('lastSyncedPush');
|
||||
|
||||
stats.outstandingPushes = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
|
||||
stats.outstandingPushes = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
|
||||
}
|
||||
}
|
||||
|
||||
sqlInit.dbReady.then(async () => {
|
||||
if (await syncOptions.isSyncSetup()) {
|
||||
log.info("Setting up sync to " + await syncOptions.getSyncServerHost() + " with timeout " + await syncOptions.getSyncTimeout());
|
||||
setInterval(cls.wrap(sync), 60000);
|
||||
|
||||
const syncProxy = await syncOptions.getSyncProxy();
|
||||
// kickoff initial sync immediately
|
||||
setTimeout(cls.wrap(sync), 1000);
|
||||
|
||||
if (syncProxy) {
|
||||
log.info("Sync proxy: " + syncProxy);
|
||||
}
|
||||
|
||||
setInterval(cls.wrap(sync), 60000);
|
||||
|
||||
// kickoff initial sync immediately
|
||||
setTimeout(cls.wrap(sync), 1000);
|
||||
|
||||
setInterval(cls.wrap(updatePushStats), 1000);
|
||||
}
|
||||
else {
|
||||
log.info("Sync server not configured, sync timer not running.")
|
||||
}
|
||||
setInterval(cls.wrap(updatePushStats), 1000);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const sql = require('./sql');
|
||||
const sourceIdService = require('./source_id');
|
||||
const dateUtils = require('./date_utils');
|
||||
const syncOptions = require('./sync_options');
|
||||
const log = require('./log');
|
||||
const cls = require('./cls');
|
||||
const eventService = require('./events');
|
||||
@@ -38,12 +37,8 @@ async function addNoteImageSync(noteImageId, sourceId) {
|
||||
await addEntitySync("note_images", noteImageId, sourceId);
|
||||
}
|
||||
|
||||
async function addLabelSync(labelId, sourceId) {
|
||||
await addEntitySync("labels", labelId, sourceId);
|
||||
}
|
||||
|
||||
async function addRelationSync(relationId, sourceId) {
|
||||
await addEntitySync("relations", relationId, sourceId);
|
||||
async function addAttributeSync(attributeId, sourceId) {
|
||||
await addEntitySync("attributes", attributeId, sourceId);
|
||||
}
|
||||
|
||||
async function addApiTokenSync(apiTokenId, sourceId) {
|
||||
@@ -58,7 +53,7 @@ async function addEntitySync(entityName, entityId, sourceId) {
|
||||
sourceId: sourceId || cls.getSourceId() || sourceIdService.getCurrentSourceId()
|
||||
});
|
||||
|
||||
eventService.emit(eventService.ENTITY_CHANGED, {
|
||||
await eventService.emit(eventService.ENTITY_CHANGED, {
|
||||
entityName,
|
||||
entityId
|
||||
});
|
||||
@@ -104,8 +99,7 @@ async function fillAllSyncRows() {
|
||||
await fillSyncRows("recent_notes", "branchId");
|
||||
await fillSyncRows("images", "imageId");
|
||||
await fillSyncRows("note_images", "noteImageId");
|
||||
await fillSyncRows("labels", "labelId");
|
||||
await fillSyncRows("relations", "relationId");
|
||||
await fillSyncRows("attributes", "attributeId");
|
||||
await fillSyncRows("api_tokens", "apiTokenId");
|
||||
await fillSyncRows("options", "name", 'isSynced = 1');
|
||||
}
|
||||
@@ -119,8 +113,7 @@ module.exports = {
|
||||
addRecentNoteSync,
|
||||
addImageSync,
|
||||
addNoteImageSync,
|
||||
addLabelSync,
|
||||
addRelationSync,
|
||||
addAttributeSync,
|
||||
addApiTokenSync,
|
||||
addEntitySync,
|
||||
cleanupSyncRowsForMissingEntities,
|
||||
|
||||
@@ -30,11 +30,8 @@ async function updateEntity(sync, entity, sourceId) {
|
||||
else if (entityName === 'note_images') {
|
||||
await updateNoteImage(entity, sourceId);
|
||||
}
|
||||
else if (entityName === 'labels') {
|
||||
await updateLabel(entity, sourceId);
|
||||
}
|
||||
else if (entityName === 'relations') {
|
||||
await updateRelation(entity, sourceId);
|
||||
else if (entityName === 'attributes') {
|
||||
await updateAttribute(entity, sourceId);
|
||||
}
|
||||
else if (entityName === 'api_tokens') {
|
||||
await updateApiToken(entity, sourceId);
|
||||
@@ -174,31 +171,17 @@ async function updateNoteImage(entity, sourceId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateLabel(entity, sourceId) {
|
||||
const origLabel = await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [entity.labelId]);
|
||||
async function updateAttribute(entity, sourceId) {
|
||||
const origAttribute = await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [entity.attributeId]);
|
||||
|
||||
if (!origLabel || origLabel.dateModified <= entity.dateModified) {
|
||||
if (!origAttribute || origAttribute.dateModified <= entity.dateModified) {
|
||||
await sql.transactional(async () => {
|
||||
await sql.replace("labels", entity);
|
||||
await sql.replace("attributes", entity);
|
||||
|
||||
await syncTableService.addLabelSync(entity.labelId, sourceId);
|
||||
await syncTableService.addAttributeSync(entity.attributeId, sourceId);
|
||||
});
|
||||
|
||||
log.info("Update/sync label " + entity.labelId);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRelation(entity, sourceId) {
|
||||
const origRelation = await sql.getRow("SELECT * FROM relations WHERE relationId = ?", [entity.relationId]);
|
||||
|
||||
if (!origRelation || origRelation.dateModified <= entity.dateModified) {
|
||||
await sql.transactional(async () => {
|
||||
await sql.replace("relations", entity);
|
||||
|
||||
await syncTableService.addRelationSync(entity.relationId, sourceId);
|
||||
});
|
||||
|
||||
log.info("Update/sync relation " + entity.relationId);
|
||||
log.info("Update/sync attribute " + entity.attributeId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('./sql');
|
||||
const repository = require('./repository');
|
||||
const Branch = require('../entities/branch');
|
||||
const syncTableService = require('./sync_table');
|
||||
const protectedSessionService = require('./protected_session');
|
||||
|
||||
@@ -99,8 +101,32 @@ async function sortNotesAlphabetically(parentNoteId) {
|
||||
});
|
||||
}
|
||||
|
||||
async function setNoteToParent(noteId, prefix, parentNoteId) {
|
||||
// case where there might be more such branches is ignored. It's expected there should be just one
|
||||
const branch = await repository.getEntity("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ? AND prefix = ?", [noteId, prefix]);
|
||||
|
||||
if (branch) {
|
||||
if (!parentNoteId) {
|
||||
branch.isDeleted = true;
|
||||
}
|
||||
else {
|
||||
branch.parentNoteId = parentNoteId;
|
||||
}
|
||||
|
||||
await branch.save();
|
||||
}
|
||||
else if (parentNoteId) {
|
||||
await new Branch({
|
||||
noteId: noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
prefix: prefix
|
||||
}).save();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateParentChild,
|
||||
getBranch,
|
||||
sortNotesAlphabetically
|
||||
sortNotesAlphabetically,
|
||||
setNoteToParent
|
||||
};
|
||||
@@ -168,9 +168,8 @@
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a id="show-note-revisions-button">Note revisions</a></li>
|
||||
<li><a class="show-labels-button"><kbd>Alt+L</kbd> Labels</a></li>
|
||||
<li><a class="show-relations-button"><kbd>Alt+R</kbd> Relations</a></li>
|
||||
<li><a id="show-note-revisions-button">Revisions</a></li>
|
||||
<li><a class="show-attributes-button"><kbd>Alt+A</kbd> Attributes</a></li>
|
||||
<li><a id="show-source-button">HTML source</a></li>
|
||||
<li><a id="upload-file-button">Upload file</a></li>
|
||||
</ul>
|
||||
@@ -182,8 +181,10 @@
|
||||
<div id="note-detail-wrapper">
|
||||
<div id="note-detail-script-area"></div>
|
||||
|
||||
<table id="note-detail-promoted-attributes"></table>
|
||||
|
||||
<div id="note-detail-component-wrapper">
|
||||
<div id="note-detail-text" class="note-detail-component" tabindex="2"></div>
|
||||
<div id="note-detail-text" class="note-detail-component" tabindex="10000"></div>
|
||||
|
||||
<div id="note-detail-search" class="note-detail-component">
|
||||
<div style="display: flex; align-items: center;">
|
||||
@@ -256,18 +257,10 @@
|
||||
|
||||
<div id="children-overview"></div>
|
||||
|
||||
<div id="labels-and-relations">
|
||||
<span id="label-list">
|
||||
<button class="btn btn-sm show-labels-button">Labels:</button>
|
||||
<div id="attribute-list">
|
||||
<button class="btn btn-sm show-attributes-button">Attributes:</button>
|
||||
|
||||
<span id="label-list-inner"></span>
|
||||
</span>
|
||||
|
||||
<span id="relation-list">
|
||||
<button class="btn btn-sm show-relations-button">Relations:</button>
|
||||
|
||||
<span id="relation-list-inner"></span>
|
||||
</span>
|
||||
<span id="attribute-list-inner"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -293,8 +286,6 @@
|
||||
|
||||
<div class="input-group">
|
||||
<input id="note-autocomplete" class="form-control" placeholder="search for note by its name" style="width: 100%;">
|
||||
|
||||
<span class="input-group-addon" id="add-link-show-recent-notes" title="Show recent notes" style="background: url('/images/icons/clock-16.png') no-repeat center; cursor: pointer;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -554,101 +545,110 @@
|
||||
<textarea id="note-source" readonly="readonly"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="labels-dialog" title="Note labels" style="display: none; padding: 20px;">
|
||||
<form data-bind="submit: save">
|
||||
<div style="text-align: center">
|
||||
<button class="btn btn-large" style="width: 200px;" id="save-labels-button" type="submit">Save changes <kbd>enter</kbd></button>
|
||||
</div>
|
||||
|
||||
<div style="height: 97%; overflow: auto">
|
||||
<table id="labels-table" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: labels">
|
||||
<tr data-bind="if: isDeleted == 0">
|
||||
<td class="handle">
|
||||
<span class="glyphicon glyphicon-resize-vertical"></span>
|
||||
<input type="hidden" name="position" data-bind="value: position"/>
|
||||
</td>
|
||||
<!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
|
||||
<td data-bind="text: labelId" style="width: 150px;"></td>
|
||||
<td>
|
||||
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
|
||||
<input type="text" class="label-name form-control" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.labelChanged }"/>
|
||||
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate label.</div>
|
||||
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Label name can't be empty.</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="label-value form-control" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.labelChanged }" style="width: 300px"/>
|
||||
</td>
|
||||
<td title="Delete" style="padding: 13px; cursor: pointer;">
|
||||
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteLabel"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="relations-dialog" title="Note relations" style="display: none; padding: 20px;">
|
||||
<div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;">
|
||||
<form data-bind="submit: save">
|
||||
<div style="text-align: center">
|
||||
<button class="btn btn-large" style="width: 200px;" id="save-relations-button" type="submit">Save changes <kbd>enter</kbd></button>
|
||||
<button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save changes <kbd>enter</kbd></button>
|
||||
</div>
|
||||
|
||||
<div style="height: 97%; overflow: auto">
|
||||
<table id="relations-table" class="table">
|
||||
<table id="owned-attributes-table" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>ID</th>
|
||||
<th>Relation name</th>
|
||||
<th>Target note</th>
|
||||
<th>Type</th>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
<th>Inheritable</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: relations">
|
||||
<tr data-bind="if: isDeleted == 0">
|
||||
<td class="handle">
|
||||
<span class="glyphicon glyphicon-resize-vertical"></span>
|
||||
<input type="hidden" name="position" data-bind="value: position"/>
|
||||
</td>
|
||||
<!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
|
||||
<td data-bind="text: relationId" style="width: 150px;"></td>
|
||||
<td>
|
||||
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
|
||||
<input type="text" class="relation-name form-control" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.relationChanged }"/>
|
||||
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate relation.</div>
|
||||
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Relation name can't be empty.</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input class="form-control relation-target-note-id"
|
||||
placeholder="search for note by its name"
|
||||
data-bind="value: targetNoteId, valueUpdate: 'blur', event: { blur: $parent.relationChanged }"
|
||||
style="width: 300px;">
|
||||
<tbody data-bind="foreach: ownedAttributes">
|
||||
<tr data-bind="if: !isDeleted">
|
||||
<td class="handle">
|
||||
<span class="glyphicon glyphicon-resize-vertical"></span>
|
||||
<input type="hidden" name="position" data-bind="value: position"/>
|
||||
</td>
|
||||
<td>
|
||||
<select data-bind="options: $parent.availableTypes, optionsText: 'text', optionsValue: 'value', value: type, event: { change: $parent.typeChanged }"></select>
|
||||
</td>
|
||||
<td>
|
||||
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
|
||||
<input type="text" class="attribute-name form-control" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }"/>
|
||||
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate attribute.</div>
|
||||
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Attribute name can't be empty.</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="label-value form-control" data-bind="visible: type == 'label', value: labelValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/>
|
||||
|
||||
<span class="input-group-addon relations-show-recent-notes" title="Show recent notes" style="background: url('/images/icons/clock-16.png') no-repeat center; cursor: pointer;"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td title="Inheritable relations are automatically inherited to the child notes">
|
||||
<input type="checkbox" value="1" data-bind="checked: isInheritable" />
|
||||
</td>
|
||||
<td title="Delete" style="padding: 13px; cursor: pointer;">
|
||||
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteRelation"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="relation-value input-group" data-bind="visible: type == 'relation'" style="width: 300px;">
|
||||
<input class="form-control relation-target-note-id"
|
||||
placeholder="search for note by its name"
|
||||
data-bind="noteAutocomplete, value: relationValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }">
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: type == 'label-definition'">
|
||||
<select data-bind="options: $parent.availableLabelTypes, optionsText: 'text', optionsValue: 'value', value: labelDefinition.labelType"></select>
|
||||
|
||||
<select data-bind="options: $parent.multiplicityTypes, optionsText: 'text', optionsValue: 'value', value: labelDefinition.multiplicityType"></select>
|
||||
|
||||
<input type="checkbox" value="true" data-bind="checked: labelDefinition.isPromoted" /> Promoted
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: type == 'relation-definition'">
|
||||
<select data-bind="options: $parent.multiplicityTypes, optionsText: 'text', optionsValue: 'value', value: relationDefinition.multiplicityType"></select>
|
||||
|
||||
<input type="checkbox" value="true" data-bind="checked: relationDefinition.isPromoted" /> Promoted
|
||||
</div>
|
||||
</td>
|
||||
<td title="Inheritable relations are automatically inherited to the child notes">
|
||||
<input type="checkbox" value="1" data-bind="checked: isInheritable" />
|
||||
</td>
|
||||
<td title="Delete" style="padding: 13px; cursor: pointer;">
|
||||
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteAttribute"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div data-bind="if: inheritedAttributes().length > 0">
|
||||
<h4>Inherited attributes</h4>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
<th>Owning note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: inheritedAttributes">
|
||||
<tr>
|
||||
<td data-bind="text: type"></td>
|
||||
<td data-bind="text: name"></td>
|
||||
<td>
|
||||
<span data-bind="if: type == 'label'">
|
||||
<span data-bind="text: value"></span>
|
||||
</span>
|
||||
<span data-bind="if: type == 'relation'">
|
||||
<span data-bind="noteLink: value"></span>
|
||||
</span>
|
||||
<span data-bind="if: type == 'label-definition'">
|
||||
<span data-bind="text: value.labelType"></span>
|
||||
<span data-bind="text: value.multiplicityType"></span>
|
||||
promoted: <span data-bind="text: value.isPromoted"></span>
|
||||
</span>
|
||||
<span data-bind="if: type == 'relation-definition'">
|
||||
<span data-bind="text: value.multiplicityType"></span>
|
||||
promoted: <span data-bind="text: value.isPromoted"></span>
|
||||
</span>
|
||||
<td data-bind="noteLink: noteId"></td>
|
||||
</tr>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
106
src/www
106
src/www
@@ -18,70 +18,53 @@ const log = require('./services/log');
|
||||
const appInfo = require('./services/app_info');
|
||||
const messagingService = require('./services/messaging');
|
||||
const utils = require('./services/utils');
|
||||
const sqlInit = require('./services/sql_init.js');
|
||||
const sqlInit = require('./services/sql_init');
|
||||
const port = require('./services/port');
|
||||
|
||||
const port = normalizePort(config['Network']['port'] || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
let httpServer;
|
||||
|
||||
if (config['Network']['https']) {
|
||||
const options = {
|
||||
key: fs.readFileSync(config['Network']['keyPath']),
|
||||
cert: fs.readFileSync(config['Network']['certPath'])
|
||||
};
|
||||
async function startTrilium() {
|
||||
const usedPort = await port;
|
||||
|
||||
httpServer = https.createServer(options, app);
|
||||
app.set('port', usedPort);
|
||||
|
||||
log.info("App HTTPS server starting up at port " + port);
|
||||
}
|
||||
else {
|
||||
httpServer = http.createServer(app);
|
||||
if (config['Network']['https']) {
|
||||
const options = {
|
||||
key: fs.readFileSync(config['Network']['keyPath']),
|
||||
cert: fs.readFileSync(config['Network']['certPath'])
|
||||
};
|
||||
|
||||
log.info("App HTTP server starting up at port " + port);
|
||||
}
|
||||
httpServer = https.createServer(options, app);
|
||||
|
||||
log.info(JSON.stringify(appInfo, null, 2));
|
||||
log.info("App HTTPS server starting up at port " + usedPort);
|
||||
}
|
||||
else {
|
||||
httpServer = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
httpServer.keepAliveTimeout = 120000 * 5;
|
||||
httpServer.listen(port);
|
||||
httpServer.on('error', onError);
|
||||
httpServer.on('listening', onListening);
|
||||
|
||||
sqlInit.dbReady.then(() => messagingService.init(httpServer, sessionParser));
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const electronRouting = require('./routes/electron');
|
||||
electronRouting(app);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
const port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
log.info("App HTTP server starting up at port " + usedPort);
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
log.info(JSON.stringify(appInfo, null, 2));
|
||||
|
||||
return false;
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
httpServer.keepAliveTimeout = 120000 * 5;
|
||||
httpServer.listen(usedPort);
|
||||
httpServer.on('error', onError);
|
||||
httpServer.on('listening', () => debug('Listening on port' + httpServer.address().port));
|
||||
|
||||
sqlInit.dbReady.then(() => messagingService.init(httpServer, sessionParser));
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const electronRouting = require('./routes/electron');
|
||||
electronRouting(app);
|
||||
}
|
||||
}
|
||||
|
||||
startTrilium();
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
@@ -91,36 +74,19 @@ function onError(error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
console.error('Port requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
console.error('Port is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
const addr = httpServer.address();
|
||||
const bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
|
||||
debug('Listening on ' + bind);
|
||||
}
|
||||
Reference in New Issue
Block a user