Compare commits

...

58 Commits

Author SHA1 Message Date
azivner
7c9ac488e8 release 0.19.0 2018-08-14 14:19:37 +02:00
azivner
fec1574447 fixed import with attributes 2018-08-14 14:17:10 +02:00
azivner
f7587de452 fixes to multivalued input tabindex 2018-08-14 13:50:04 +02:00
azivner
41a6e777ea attributes coming from inheritAttributes are inherited only if the inheritAttributes relation itself is inheritable 2018-08-14 13:02:17 +02:00
azivner
8fb0de900b createNote API now accepts attributes instead of just labels 2018-08-14 12:54:58 +02:00
azivner
a40bf71fd4 connection lost error is now logged only to the console, it was too annoying while being mostly harmless 2018-08-14 11:42:29 +02:00
azivner
2a53bb03ae fix autocomplete casing issue with first level notes 2018-08-13 21:01:14 +02:00
azivner
a684879b91 primitive attribute caching inside note entity, fixes #149 2018-08-13 17:16:06 +02:00
azivner
ddbd4f73c8 attributes can be inherited through special relation "inheritAttributes" 2018-08-13 17:05:16 +02:00
azivner
b0ed790edf fix multivalue issue of not appearing when no attribute has been saved yet 2018-08-13 15:58:37 +02:00
azivner
3424406ff1 script API changes for task management #140 2018-08-13 13:53:08 +02:00
azivner
ce5c385c15 fix search by attributes was finding also deleted attributes 2018-08-13 11:06:17 +02:00
azivner
cd9eef32b0 support for cssClass label on note 2018-08-13 10:59:31 +02:00
azivner
12d82e3b33 listener exception doesn't stop execution 2018-08-13 09:49:39 +02:00
azivner
f071d3f651 fix validation issue + attribute not triggering change event on delete 2018-08-13 09:39:07 +02:00
azivner
297b536ebc promoted attributes have tabindex following the title and before note content 2018-08-13 09:07:21 +02:00
azivner
7cca2d9247 realoding tree doesn't steal focus (important for promoted attribute changes with attached scripts) 2018-08-13 08:42:37 +02:00
azivner
36dc802d16 updated schema.sql 2018-08-13 07:57:23 +02:00
azivner
c78ddb70cb all events are now synchronous 2018-08-12 20:07:02 +02:00
azivner
9fb0599c45 entities are now changed only if entity hash changed which will limit number of events emitted 2018-08-12 20:04:48 +02:00
azivner
13f524fb39 ENTITY_CHANGED event is emitted synchronously 2018-08-12 13:03:59 +02:00
azivner
27be3b4c90 fixes in tree loading 2018-08-12 12:59:38 +02:00
azivner
af4ea66742 fix shift-up selection, fixes #146 2018-08-11 20:02:48 +02:00
azivner
0f42c396f3 image upload fixes + some API changes 2018-08-11 19:45:55 +02:00
azivner
9e96272eb3 fixed runOnAttributeChange event 2018-08-10 14:31:57 +02:00
azivner
965dbcbc9a renamed workEntity to originEntity 2018-08-10 13:30:20 +02:00
azivner
7ac109e7f7 fix label => attributes omissions 2018-08-09 20:55:16 +02:00
azivner
ac25770c0e added runOnAttributeChange event 2018-08-09 20:08:00 +02:00
azivner
5b15424498 archived label now respects isInheritable flag, fixes #145 2018-08-08 16:14:35 +02:00
azivner
f1240c26bf more cleanup of labels and relations from backend, dropping tables from db 2018-08-07 13:44:51 +02:00
azivner
1c0fd243d1 cleanup of labels and relations from backend 2018-08-07 13:33:10 +02:00
azivner
3491235533 cleanup of labels & relations frontend code 2018-08-07 12:48:11 +02:00
azivner
5f36856571 * refactoring of repository layer to represent booleans as true/false instead of 1/0
* show list of inherited attributes, fixes #136
* properly work with inheritance
2018-08-07 11:38:00 +02:00
azivner
d3e44b37e9 autocomplete for promoted text labels 2018-08-06 22:52:49 +02:00
azivner
90e9297ec5 promoted relation attributes now work correctly, refactoring of note autocomplete code 2018-08-06 22:29:03 +02:00
azivner
c568ef2f8a nice icons for add / remove attribute 2018-08-06 17:53:13 +02:00
azivner
fcf6141cde support for promoted multi value attributes 2018-08-06 17:24:35 +02:00
azivner
21551d7b77 implemented date promoted attribute 2018-08-06 15:58:59 +02:00
azivner
12031d369f displaying and saving number and boolean promoted attributes 2018-08-06 15:23:22 +02:00
azivner
b44c523845 basic support for saving promoted attributes 2018-08-06 14:43:42 +02:00
azivner
49989695ff fix relations in attributes 2018-08-06 11:30:37 +02:00
azivner
a55d3530e9 attribute list on the bottom if there are no promoted attributes 2018-08-06 09:41:01 +02:00
azivner
2aab3ad281 fixes in attribute persistence + WIP on display of promoted attrs 2018-08-06 08:59:26 +02:00
azivner
194ce4f10f fixed UI for relation definition 2018-08-05 20:48:56 +02:00
azivner
2089c32839 attribute UI & saving now fully working 2018-08-05 20:08:56 +02:00
azivner
f437be7af0 attribute definition work in progress 2018-08-03 22:56:49 +02:00
azivner
96dc56098d ckeditor upgrade to 11.0.1 with blocktoolbar 2018-08-03 22:56:23 +02:00
azivner
61987e46f7 work in progress on attributes UI - unification of labels and relations now mostly works 2018-08-03 13:06:56 +02:00
azivner
509093b755 added "type" to attribute dialog, name autocomplete servers according to the choice 2018-08-03 11:11:57 +02:00
azivner
097114c0f2 basic entities for attributes (unification of labels and relations) 2018-08-02 22:48:21 +02:00
azivner
040f9185f8 electron 2.0.6 and some other minor library upgrades 2018-08-02 19:55:20 +02:00
azivner
6dc934abbe refactored targetNote to workNote in the ScriptContext which was very confusing with relation's targetNote 2018-08-01 10:12:54 +02:00
azivner
2d24bf81dd added new label "sorted" which will keep children notes alphabetically sorted, fixes #82 2018-08-01 09:26:02 +02:00
azivner
9452fc236b electron build uses random free port, fixes #142 2018-07-31 19:50:18 +02:00
azivner
365c37604b code note in tooltip needs to be wrapped in <pre> to keep formatting, fixes #137 2018-07-30 16:55:20 +02:00
azivner
01c7e58d47 check if sync is configured every minute, not just at app startup, fixes #138 2018-07-30 16:45:34 +02:00
azivner
d3d49923b1 changed backup to simple scheme with one daily, one weekly and one monthly backup, fixes #15 2018-07-30 16:40:50 +02:00
azivner
263ac299d0 fix pkg building 2018-07-30 14:18:43 +02:00
91 changed files with 1995 additions and 1669 deletions

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataSource name="document.db">
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.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>&quot;&quot;</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>&apos;&apos;</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>&quot;&quot;</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>&quot;&quot;</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>&apos;1970-01-01T00:00:00.000Z&apos;</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>&quot;&quot;</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>&apos;&apos;</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>&quot;&quot;</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>&quot;&quot;</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>&apos;&apos;</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>&apos;&apos;</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>&quot;&quot;</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>&quot;unnamed&quot;</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>&quot;&quot;</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>&apos;text&apos;</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>&apos;text/html&apos;</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>&quot;&quot;</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>&quot;&quot;</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>&apos;1970-01-01T00:00:00.000Z&apos;</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>&quot;&quot;</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>&quot;&quot;</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
View 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>

View File

@@ -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
View File

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

View File

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

View 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

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

View File

@@ -0,0 +1 @@
ALTER TABLE attributes ADD isInheritable int DEFAULT 0 NULL;

View File

@@ -0,0 +1,4 @@
DROP TABLE relations;
DROP TABLE labels;
DELETE FROM sync WHERE entityName = 'relations' OR entityName = 'labels';

View File

@@ -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,
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
targetNoteId TEXT not null,
isInheritable int DEFAULT 0 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_relation_sourceNoteId
on relations (sourceNoteId);
CREATE INDEX IDX_relation_targetNoteId
on relations (targetNoteId);
isDeleted INT not null,
hash TEXT default "" not null, isInheritable int DEFAULT 0 NULL);

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,8 +29,12 @@ class Branch extends Entity {
this.dateCreated = dateUtils.nowDate();
}
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}
module.exports = Branch;

View File

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

View File

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

View File

@@ -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,8 +17,12 @@ class Image extends Entity {
this.dateCreated = dateUtils.nowDate();
}
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}
module.exports = Image;

View File

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

View File

@@ -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,8 +273,12 @@ class Note extends Entity {
this.dateCreated = dateUtils.nowDate();
}
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}
module.exports = Note;

View File

@@ -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,8 +26,12 @@ class NoteImage extends Entity {
this.dateCreated = dateUtils.nowDate();
}
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}
module.exports = NoteImage;

View File

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

View File

@@ -8,11 +8,19 @@ 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();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}
module.exports = Option;

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ class NoteShort {
this.type = row.type;
this.mime = row.mime;
this.archived = row.archived;
this.cssClass = row.cssClass;
}
isJson() {

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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;
}
$labelList.show();
$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 {
$labelList.hide();
messagingService.logError("Unknown labelType=" + definitionAttr.labelType);
}
}
else if (valueAttr.type === 'relation') {
if (valueAttr.value) {
$input.val((await treeUtils.getNoteTitle(valueAttr.value) + " (" + valueAttr.value + ")"));
}
return labels;
}
async function loadRelationList() {
const noteId = getCurrentNoteId();
const relations = await server.get('notes/' + noteId + '/relations');
$relationListInner.html('');
if (relations.length > 0) {
for (const relation of relations) {
$relationListInner.append(relation.name + " = ");
$relationListInner.append(await linkService.createNoteLink(relation.targetNoteId));
$relationListInner.append(" ");
}
$relationList.show();
// no need to wait for this
noteAutocompleteService.initNoteAutocomplete($input);
}
else {
$relationList.hide();
messagingService.logError("Unknown attribute type=" + valueAttr.type);
return;
}
return relations;
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: ""
});
$tr.after($new);
$new.find('input').focus();
});
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);
}
$tr.remove();
});
$multiplicityCell.append(addButton).append(" &nbsp; ").append(removeButton);
}
return $tr;
}
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 */

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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;
}
@@ -421,3 +421,25 @@ 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;
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = [];

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -10,49 +10,32 @@ const syncMutexService = require('./sync_mutex');
const cls = require('./cls');
async function regularBackup() {
await periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
await periodBackup('lastWeeklyBackupDate', 'weekly', 7 * 24 * 3600);
await periodBackup('lastMonthlyBackupDate', 'monthly', 30 * 24 * 3600);
}
async function periodBackup(optionName, fileName, periodInSeconds) {
const now = new Date();
const lastBackupDate = dateUtils.parseDateTime(await optionService.getOption('lastBackupDate'));
const lastDailyBackupDate = dateUtils.parseDateTime(await optionService.getOption(optionName));
console.log(lastBackupDate);
if (now.getTime() - lastDailyBackupDate.getTime() > periodInSeconds * 1000) {
await backupNow(fileName);
if (now.getTime() - lastBackupDate.getTime() > 43200 * 1000) {
await backupNow();
await optionService.setOption(optionName, dateUtils.nowDate());
}
}
await cleanupOldBackups();
}
async function backupNow() {
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);
}
}
});
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() {
if (await syncOptions.isSyncSetup()) {
const lastSyncedPush = await optionService.getOption('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());
const syncProxy = await syncOptions.getSyncProxy();
if (syncProxy) {
log.info("Sync proxy: " + syncProxy);
}
sqlInit.dbReady.then(async () => {
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.")
}
});
module.exports = {

View File

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

View File

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

View File

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

View File

@@ -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;">
<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-labels-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="labels-table" class="table">
<table id="owned-attributes-table" class="table">
<thead>
<tr>
<th></th>
<th>ID</th>
<th>Type</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;">
<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>
</div>
<div style="height: 97%; overflow: auto">
<table id="relations-table" class="table">
<thead>
<tr>
<th></th>
<th>ID</th>
<th>Relation name</th>
<th>Target note</th>
<th>Inheritable</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: relations">
<tr data-bind="if: isDeleted == 0">
<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>
<!-- 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>
<select data-bind="options: $parent.availableTypes, optionsText: 'text', optionsValue: 'value', value: type, event: { change: $parent.typeChanged }"></select>
</td>
<td>
<div class="input-group">
<!-- 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"/>
<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="value: targetNoteId, valueUpdate: 'blur', event: { blur: $parent.relationChanged }"
style="width: 300px;">
data-bind="noteAutocomplete, value: relationValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }">
</div>
<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 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.deleteRelation"></span>
<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>

62
src/www
View File

@@ -18,16 +18,16 @@ 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;
async function startTrilium() {
const usedPort = await port;
app.set('port', usedPort);
if (config['Network']['https']) {
const options = {
key: fs.readFileSync(config['Network']['keyPath']),
@@ -36,12 +36,12 @@ if (config['Network']['https']) {
httpServer = https.createServer(options, app);
log.info("App HTTPS server starting up at port " + port);
log.info("App HTTPS server starting up at port " + usedPort);
}
else {
httpServer = http.createServer(app);
log.info("App HTTP server starting up at port " + port);
log.info("App HTTP server starting up at port " + usedPort);
}
log.info(JSON.stringify(appInfo, null, 2));
@@ -51,9 +51,9 @@ log.info(JSON.stringify(appInfo, null, 2));
*/
httpServer.keepAliveTimeout = 120000 * 5;
httpServer.listen(port);
httpServer.listen(usedPort);
httpServer.on('error', onError);
httpServer.on('listening', onListening);
httpServer.on('listening', () => debug('Listening on port' + httpServer.address().port));
sqlInit.dbReady.then(() => messagingService.init(httpServer, sessionParser));
@@ -61,26 +61,9 @@ 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;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
startTrilium();
/**
* Event listener for HTTP server "error" event.
@@ -91,19 +74,15 @@ 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;
@@ -111,16 +90,3 @@ function onError(error) {
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);
}