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