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

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

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

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

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

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,10 +8,18 @@ class Option extends Entity {
static get primaryKeyName() { return "name"; }
static get hashedProperties() { return ["name", "value"]; }
constructor(row) {
super(row);
this.isSynced = !!this.isSynced;
}
beforeSaving() {
super.beforeSaving();
this.dateModified = dateUtils.nowDate();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}

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;
}
$input.autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in autocomplete.js
source: attributeValues.map(attribute => {
return {
attribute: attribute,
value: attribute
}
}),
minLength: 0
});
$input.focus(() => $input.autocomplete("search", ""));
});
}
else if (definition.labelType === 'number') {
$input.prop("type", "number");
}
else if (definition.labelType === 'boolean') {
$input.prop("type", "checkbox");
if (valueAttr.value === "true") {
$input.prop("checked", "checked");
}
}
else if (definition.labelType === 'date') {
$input.prop("type", "text");
$input.datepicker({
changeMonth: true,
changeYear: true,
dateFormat: "yy-mm-dd"
});
const $todayButton = $("<button>").addClass("btn btn-small").text("Today").click(() => {
$input.val(utils.formatDateISO(new Date()));
$input.trigger("change");
});
$actionCell.append($todayButton);
}
else {
messagingService.logError("Unknown labelType=" + definitionAttr.labelType);
}
}
else if (valueAttr.type === 'relation') {
if (valueAttr.value) {
$input.val((await treeUtils.getNoteTitle(valueAttr.value) + " (" + valueAttr.value + ")"));
}
// no need to wait for this
noteAutocompleteService.initNoteAutocomplete($input);
}
else {
messagingService.logError("Unknown attribute type=" + valueAttr.type);
return;
}
$labelList.show();
}
else {
$labelList.hide();
}
if (definition.multiplicityType === "multivalue") {
const addButton = $("<span>")
.addClass("glyphicon glyphicon-plus pointer")
.prop("title", "Add new attribute")
.click(async () => {
const $new = await createRow(definitionAttr, {
attributeId: "",
type: valueAttr.type,
name: definitionAttr.name,
value: ""
});
return labels;
}
$tr.after($new);
async function loadRelationList() {
const noteId = getCurrentNoteId();
$new.find('input').focus();
});
const relations = await server.get('notes/' + noteId + '/relations');
const removeButton = $("<span>")
.addClass("glyphicon glyphicon-trash pointer")
.prop("title", "Remove this attribute")
.click(async () => {
if (valueAttr.attributeId) {
await server.remove("notes/" + noteId + "/attributes/" + valueAttr.attributeId);
}
$relationListInner.html('');
$tr.remove();
});
if (relations.length > 0) {
for (const relation of relations) {
$relationListInner.append(relation.name + " = ");
$relationListInner.append(await linkService.createNoteLink(relation.targetNoteId));
$relationListInner.append(" ");
$multiplicityCell.append(addButton).append(" &nbsp; ").append(removeButton);
}
$relationList.show();
}
else {
$relationList.hide();
return $tr;
}
return relations;
if (promoted.length > 0) {
const $tbody = $("<tbody>");
for (const definitionAttr of promoted) {
const definitionType = definitionAttr.type;
const valueType = definitionType.substr(0, definitionType.length - 11);
let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType);
if (valueAttrs.length === 0) {
valueAttrs.push({
attributeId: "",
type: valueType,
name: definitionAttr.name,
value: ""
});
}
if (definitionAttr.value.multiplicityType === 'singlevalue') {
valueAttrs = valueAttrs.slice(0, 1);
}
for (const valueAttr of valueAttrs) {
const $tr = await createRow(definitionAttr, valueAttr);
$tbody.append($tr);
}
}
// we replace the whole content in one step so there can't be any race conditions
// (previously we saw promoted attributes doubling)
$promotedAttributesContainer.empty().append($tbody);
}
else {
$attributeListInner.html('');
if (attributes.length > 0) {
for (const attribute of attributes) {
if (attribute.type === 'label') {
$attributeListInner.append(utils.formatLabel(attribute) + " ");
}
else if (attribute.type === 'relation') {
$attributeListInner.append(attribute.name + "=");
$attributeListInner.append(await linkService.createNoteLink(attribute.value));
$attributeListInner.append(" ");
}
else if (attribute.type === 'label-definition' || attribute.type === 'relation-definition') {
$attributeListInner.append(attribute.name + " definition ");
}
else {
messagingService.logError("Unknown attr type: " + attribute.type);
}
}
$attributeList.show();
}
}
return attributes;
}
async function loadNote(noteId) {
@@ -276,7 +433,7 @@ function focus() {
getComponent(note.type).focus();
}
messagingService.subscribeToMessages(syncData => {
messagingService.subscribeToSyncMessages(syncData => {
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getCurrentNoteId())) {
infoService.showMessage('Reloading note because of background changes');
@@ -284,6 +441,35 @@ messagingService.subscribeToMessages(syncData => {
}
});
$promotedAttributesContainer.on('change', '.promoted-attribute-input', async event => {
const $attr = $(event.target);
let value;
if ($attr.prop("type") === "checkbox") {
value = $attr.is(':checked') ? "true" : "false";
}
else if ($attr.prop("attribute-type") === "relation") {
if ($attr.val()) {
value = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel($attr.val()));
}
}
else {
value = $attr.val();
}
const result = await server.put("notes/" + getCurrentNoteId() + "/attribute", {
attributeId: $attr.prop("attribute-id"),
type: $attr.prop("attribute-type"),
name: $attr.prop("attribute-name"),
value: value
});
$attr.prop("attribute-id", result.attributeId);
infoService.showMessage("Attribute has been saved.");
});
$(document).ready(() => {
$noteTitle.on('input', () => {
noteChanged();
@@ -312,8 +498,7 @@ export default {
getCurrentNoteId,
newNoteCreated,
focus,
loadLabelList,
loadRelationList,
loadAttributes,
saveNote,
saveNoteIfChanged,
noteChanged

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;
}
@@ -420,4 +420,26 @@ html.theme-dark img, html.theme-dark video {
html.theme-dark body {
background: #191819;
}
.ck.ck-block-toolbar-button {
transform: translateX(10px);
}
#note-detail-promoted-attributes {
max-width: 70%;
margin: auto;
}
#note-detail-promoted-attributes td, note-detail-promoted-attributes th {
padding: 5px;
}
.pointer {
cursor: pointer;
}
.show-recent-notes-button {
background: url('/images/icons/clock-16.png') no-repeat center;
cursor: pointer;
}

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() {
const now = new Date();
const lastBackupDate = dateUtils.parseDateTime(await optionService.getOption('lastBackupDate'));
await periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
console.log(lastBackupDate);
await periodBackup('lastWeeklyBackupDate', 'weekly', 7 * 24 * 3600);
if (now.getTime() - lastBackupDate.getTime() > 43200 * 1000) {
await backupNow();
}
await cleanupOldBackups();
await periodBackup('lastMonthlyBackupDate', 'monthly', 30 * 24 * 3600);
}
async function backupNow() {
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
async function periodBackup(optionName, fileName, periodInSeconds) {
const now = new Date();
const lastDailyBackupDate = dateUtils.parseDateTime(await optionService.getOption(optionName));
if (now.getTime() - lastDailyBackupDate.getTime() > periodInSeconds * 1000) {
await backupNow(fileName);
await optionService.setOption(optionName, dateUtils.nowDate());
}
}
async function backupNow(name) {
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
await syncMutexService.doExclusively(async () => {
const backupFile = dataDir.BACKUP_DIR + "/" + "backup-" + dateUtils.getDateTimeForFile() + ".db";
const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`;
fs.copySync(dataDir.DOCUMENT_PATH, backupFile);
log.info("Created backup at " + backupFile);
await optionService.setOption('lastBackupDate', dateUtils.nowDate());
});
}
async function cleanupOldBackups() {
const now = new Date();
fs.readdirSync(dataDir.BACKUP_DIR).forEach(file => {
const match = file.match(/backup-([0-9 -:]+)\.db/);
if (match) {
const date_str = match[1];
const date = Date.parse(date_str);
if (now.getTime() - date.getTime() > 30 * 24 * 3600 * 1000) {
log.info("Removing old backup - " + file);
fs.unlink(dataDir.BACKUP_DIR + "/" + file);
}
}
});
}

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() {
const lastSyncedPush = await optionService.getOption('lastSyncedPush');
if (await syncOptions.isSyncSetup()) {
const lastSyncedPush = await optionService.getOption('lastSyncedPush');
stats.outstandingPushes = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
stats.outstandingPushes = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
}
}
sqlInit.dbReady.then(async () => {
if (await syncOptions.isSyncSetup()) {
log.info("Setting up sync to " + await syncOptions.getSyncServerHost() + " with timeout " + await syncOptions.getSyncTimeout());
setInterval(cls.wrap(sync), 60000);
const syncProxy = await syncOptions.getSyncProxy();
// kickoff initial sync immediately
setTimeout(cls.wrap(sync), 1000);
if (syncProxy) {
log.info("Sync proxy: " + syncProxy);
}
setInterval(cls.wrap(sync), 60000);
// kickoff initial sync immediately
setTimeout(cls.wrap(sync), 1000);
setInterval(cls.wrap(updatePushStats), 1000);
}
else {
log.info("Sync server not configured, sync timer not running.")
}
setInterval(cls.wrap(updatePushStats), 1000);
});
module.exports = {

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;">
<form data-bind="submit: save">
<div style="text-align: center">
<button class="btn btn-large" style="width: 200px;" id="save-labels-button" type="submit">Save changes <kbd>enter</kbd></button>
</div>
<div style="height: 97%; overflow: auto">
<table id="labels-table" class="table">
<thead>
<tr>
<th></th>
<th>ID</th>
<th>Name</th>
<th>Value</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: labels">
<tr data-bind="if: isDeleted == 0">
<td class="handle">
<span class="glyphicon glyphicon-resize-vertical"></span>
<input type="hidden" name="position" data-bind="value: position"/>
</td>
<!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
<td data-bind="text: labelId" style="width: 150px;"></td>
<td>
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
<input type="text" class="label-name form-control" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.labelChanged }"/>
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate label.</div>
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Label name can't be empty.</div>
</td>
<td>
<input type="text" class="label-value form-control" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.labelChanged }" style="width: 300px"/>
</td>
<td title="Delete" style="padding: 13px; cursor: pointer;">
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteLabel"></span>
</td>
</tr>
</tbody>
</table>
</div>
</form>
</div>
<div id="relations-dialog" title="Note relations" style="display: none; padding: 20px;">
<div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;">
<form data-bind="submit: save">
<div style="text-align: center">
<button class="btn btn-large" style="width: 200px;" id="save-relations-button" type="submit">Save changes <kbd>enter</kbd></button>
<button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save changes <kbd>enter</kbd></button>
</div>
<div style="height: 97%; overflow: auto">
<table id="relations-table" class="table">
<table id="owned-attributes-table" class="table">
<thead>
<tr>
<th></th>
<th>ID</th>
<th>Relation name</th>
<th>Target note</th>
<th>Type</th>
<th>Name</th>
<th>Value</th>
<th>Inheritable</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: relations">
<tr data-bind="if: isDeleted == 0">
<td class="handle">
<span class="glyphicon glyphicon-resize-vertical"></span>
<input type="hidden" name="position" data-bind="value: position"/>
</td>
<!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
<td data-bind="text: relationId" style="width: 150px;"></td>
<td>
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
<input type="text" class="relation-name form-control" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.relationChanged }"/>
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate relation.</div>
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Relation name can't be empty.</div>
</td>
<td>
<div class="input-group">
<input class="form-control relation-target-note-id"
placeholder="search for note by its name"
data-bind="value: targetNoteId, valueUpdate: 'blur', event: { blur: $parent.relationChanged }"
style="width: 300px;">
<tbody data-bind="foreach: ownedAttributes">
<tr data-bind="if: !isDeleted">
<td class="handle">
<span class="glyphicon glyphicon-resize-vertical"></span>
<input type="hidden" name="position" data-bind="value: position"/>
</td>
<td>
<select data-bind="options: $parent.availableTypes, optionsText: 'text', optionsValue: 'value', value: type, event: { change: $parent.typeChanged }"></select>
</td>
<td>
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
<input type="text" class="attribute-name form-control" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }"/>
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate attribute.</div>
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Attribute name can't be empty.</div>
</td>
<td>
<input type="text" class="label-value form-control" data-bind="visible: type == 'label', value: labelValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/>
<span class="input-group-addon relations-show-recent-notes" title="Show recent notes" style="background: url('/images/icons/clock-16.png') no-repeat center; cursor: pointer;"></span>
</div>
</td>
<td title="Inheritable relations are automatically inherited to the child notes">
<input type="checkbox" value="1" data-bind="checked: isInheritable" />
</td>
<td title="Delete" style="padding: 13px; cursor: pointer;">
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteRelation"></span>
</td>
</tr>
<div class="relation-value input-group" data-bind="visible: type == 'relation'" style="width: 300px;">
<input class="form-control relation-target-note-id"
placeholder="search for note by its name"
data-bind="noteAutocomplete, value: relationValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }">
</div>
<div data-bind="visible: type == 'label-definition'">
<select data-bind="options: $parent.availableLabelTypes, optionsText: 'text', optionsValue: 'value', value: labelDefinition.labelType"></select>
<select data-bind="options: $parent.multiplicityTypes, optionsText: 'text', optionsValue: 'value', value: labelDefinition.multiplicityType"></select>
<input type="checkbox" value="true" data-bind="checked: labelDefinition.isPromoted" /> Promoted
</div>
<div data-bind="visible: type == 'relation-definition'">
<select data-bind="options: $parent.multiplicityTypes, optionsText: 'text', optionsValue: 'value', value: relationDefinition.multiplicityType"></select>
<input type="checkbox" value="true" data-bind="checked: relationDefinition.isPromoted" /> Promoted
</div>
</td>
<td title="Inheritable relations are automatically inherited to the child notes">
<input type="checkbox" value="1" data-bind="checked: isInheritable" />
</td>
<td title="Delete" style="padding: 13px; cursor: pointer;">
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteAttribute"></span>
</td>
</tr>
</tbody>
</table>
<div data-bind="if: inheritedAttributes().length > 0">
<h4>Inherited attributes</h4>
<table class="table">
<thead>
<tr>
<th>Type</th>
<th>Name</th>
<th>Value</th>
<th>Owning note</th>
</tr>
</thead>
<tbody data-bind="foreach: inheritedAttributes">
<tr>
<td data-bind="text: type"></td>
<td data-bind="text: name"></td>
<td>
<span data-bind="if: type == 'label'">
<span data-bind="text: value"></span>
</span>
<span data-bind="if: type == 'relation'">
<span data-bind="noteLink: value"></span>
</span>
<span data-bind="if: type == 'label-definition'">
<span data-bind="text: value.labelType"></span>
<span data-bind="text: value.multiplicityType"></span>
promoted: <span data-bind="text: value.isPromoted"></span>
</span>
<span data-bind="if: type == 'relation-definition'">
<span data-bind="text: value.multiplicityType"></span>
promoted: <span data-bind="text: value.isPromoted"></span>
</span>
<td data-bind="noteLink: noteId"></td>
</tr>
</tr>
</tbody>
</table>
</div>
</div>
</form>
</div>

106
src/www
View File

@@ -18,70 +18,53 @@ const log = require('./services/log');
const appInfo = require('./services/app_info');
const messagingService = require('./services/messaging');
const utils = require('./services/utils');
const sqlInit = require('./services/sql_init.js');
const sqlInit = require('./services/sql_init');
const port = require('./services/port');
const port = normalizePort(config['Network']['port'] || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
let httpServer;
if (config['Network']['https']) {
const options = {
key: fs.readFileSync(config['Network']['keyPath']),
cert: fs.readFileSync(config['Network']['certPath'])
};
async function startTrilium() {
const usedPort = await port;
httpServer = https.createServer(options, app);
app.set('port', usedPort);
log.info("App HTTPS server starting up at port " + port);
}
else {
httpServer = http.createServer(app);
if (config['Network']['https']) {
const options = {
key: fs.readFileSync(config['Network']['keyPath']),
cert: fs.readFileSync(config['Network']['certPath'])
};
log.info("App HTTP server starting up at port " + port);
}
httpServer = https.createServer(options, app);
log.info(JSON.stringify(appInfo, null, 2));
log.info("App HTTPS server starting up at port " + usedPort);
}
else {
httpServer = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
httpServer.keepAliveTimeout = 120000 * 5;
httpServer.listen(port);
httpServer.on('error', onError);
httpServer.on('listening', onListening);
sqlInit.dbReady.then(() => messagingService.init(httpServer, sessionParser));
if (utils.isElectron()) {
const electronRouting = require('./routes/electron');
electronRouting(app);
}
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
const port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
log.info("App HTTP server starting up at port " + usedPort);
}
if (port >= 0) {
// port number
return port;
}
log.info(JSON.stringify(appInfo, null, 2));
return false;
/**
* Listen on provided port, on all network interfaces.
*/
httpServer.keepAliveTimeout = 120000 * 5;
httpServer.listen(usedPort);
httpServer.on('error', onError);
httpServer.on('listening', () => debug('Listening on port' + httpServer.address().port));
sqlInit.dbReady.then(() => messagingService.init(httpServer, sessionParser));
if (utils.isElectron()) {
const electronRouting = require('./routes/electron');
electronRouting(app);
}
}
startTrilium();
/**
* Event listener for HTTP server "error" event.
*/
@@ -91,36 +74,19 @@ function onError(error) {
throw error;
}
const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
console.error('Port requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
console.error('Port is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
const addr = httpServer.address();
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}