Compare commits
	
		
			132 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					3d185a5178 | ||
| 
						 | 
					2ff7a890bc | ||
| 
						 | 
					2eb1a9705f | ||
| 
						 | 
					ed1381103a | ||
| 
						 | 
					170d317589 | ||
| 
						 | 
					ededc063df | ||
| 
						 | 
					986eace1be | ||
| 
						 | 
					29086d8dfe | ||
| 
						 | 
					9b3f3fde05 | ||
| 
						 | 
					6a50afd952 | ||
| 
						 | 
					697eee2706 | ||
| 
						 | 
					8a95afd756 | ||
| 
						 | 
					4d6eda8fe6 | ||
| 
						 | 
					e4f459fa2b | ||
| 
						 | 
					f578e001b0 | ||
| 
						 | 
					2a08aef885 | ||
| 
						 | 
					7564bf388c | ||
| 
						 | 
					7e4d70259f | ||
| 
						 | 
					5b98c1c0f3 | ||
| 
						 | 
					02dc7b199b | ||
| 
						 | 
					d39cdbfada | ||
| 
						 | 
					50bb4a47ee | ||
| 
						 | 
					a4627f2ddb | ||
| 
						 | 
					c8253caae9 | ||
| 
						 | 
					0ece9bd1be | ||
| 
						 | 
					b6935abcc9 | ||
| 
						 | 
					37ab7b4641 | ||
| 
						 | 
					013714cb5c | ||
| 
						 | 
					1fe7c62f5a | ||
| 
						 | 
					a06618d851 | ||
| 
						 | 
					e7460ca3a9 | ||
| 
						 | 
					073300bbcd | ||
| 
						 | 
					a201661ce5 | ||
| 
						 | 
					6235a3c886 | ||
| 
						 | 
					3972c27e7a | ||
| 
						 | 
					14cffbbe62 | ||
| 
						 | 
					599c3c04af | ||
| 
						 | 
					f1412b631d | ||
| 
						 | 
					41908050bb | ||
| 
						 | 
					f07033423c | ||
| 
						 | 
					daf96fcbf2 | ||
| 
						 | 
					2bca94529e | ||
| 
						 | 
					b2c9a0da21 | ||
| 
						 | 
					7a9542b4fc | ||
| 
						 | 
					3a95c9e1bc | ||
| 
						 | 
					3d2ef6be01 | ||
| 
						 | 
					d67246699a | ||
| 
						 | 
					14c704d6db | ||
| 
						 | 
					4c8eeb2e6f | ||
| 
						 | 
					c1b245c8b1 | ||
| 
						 | 
					74202d67bb | ||
| 
						 | 
					26066f39b1 | ||
| 
						 | 
					b255cf190c | ||
| 
						 | 
					bc77b143b0 | ||
| 
						 | 
					9f0ff6ae7a | ||
| 
						 | 
					736704c7d6 | ||
| 
						 | 
					654c116c58 | ||
| 
						 | 
					89a5cab98f | ||
| 
						 | 
					c39d0be8cd | ||
| 
						 | 
					e75b4cd848 | ||
| 
						 | 
					378e8f35e5 | ||
| 
						 | 
					bdb5e2f13f | ||
| 
						 | 
					8211bed449 | ||
| 
						 | 
					b243632483 | ||
| 
						 | 
					e4d2513451 | ||
| 
						 | 
					385144451b | ||
| 
						 | 
					c8c533844e | ||
| 
						 | 
					0e69f0c079 | ||
| 
						 | 
					aee60c444f | ||
| 
						 | 
					e7a504c66b | ||
| 
						 | 
					45d9c7164c | ||
| 
						 | 
					bd913a63a8 | ||
| 
						 | 
					5a1938c078 | ||
| 
						 | 
					015cd68756 | ||
| 
						 | 
					76c0e5b2b8 | ||
| 
						 | 
					0f8f707acd | ||
| 
						 | 
					083cccea28 | ||
| 
						 | 
					31b76b23ce | ||
| 
						 | 
					af529f82e5 | ||
| 
						 | 
					fc6669d254 | ||
| 
						 | 
					c07785be67 | ||
| 
						 | 
					80d2457b23 | ||
| 
						 | 
					5dde2752d2 | ||
| 
						 | 
					8bf4633cd0 | ||
| 
						 | 
					bd66b8a1c8 | ||
| 
						 | 
					be51e533fc | ||
| 
						 | 
					f47ae12019 | ||
| 
						 | 
					cab54a458f | ||
| 
						 | 
					a30734f1bc | ||
| 
						 | 
					7ad9f7b129 | ||
| 
						 | 
					40a32e6826 | ||
| 
						 | 
					ab0486aaf1 | ||
| 
						 | 
					874593a167 | ||
| 
						 | 
					03bf33630e | ||
| 
						 | 
					933cce1b94 | ||
| 
						 | 
					4a6ff573f8 | ||
| 
						 | 
					1a737f7d19 | ||
| 
						 | 
					cb69914f09 | ||
| 
						 | 
					a372cbb2df | ||
| 
						 | 
					0ce5caefe8 | ||
| 
						 | 
					94dabb81f6 | ||
| 
						 | 
					cd45bcfd03 | ||
| 
						 | 
					49a53f7a45 | ||
| 
						 | 
					9fa6c0918c | ||
| 
						 | 
					e8d089e37e | ||
| 
						 | 
					a931ce25fa | ||
| 
						 | 
					b507abb4f7 | ||
| 
						 | 
					66e7c6de62 | ||
| 
						 | 
					4ce5ea9886 | ||
| 
						 | 
					8c54b62f07 | ||
| 
						 | 
					85eb50ed0f | ||
| 
						 | 
					5ffd621e9d | ||
| 
						 | 
					df93cb09da | ||
| 
						 | 
					bbf04209f0 | ||
| 
						 | 
					834bfa39c7 | ||
| 
						 | 
					52b445f70b | ||
| 
						 | 
					7b9b4fbb0c | ||
| 
						 | 
					5af0ba1fcb | ||
| 
						 | 
					85a9748291 | ||
| 
						 | 
					b4005a7ffe | ||
| 
						 | 
					82de1c88d4 | ||
| 
						 | 
					1687ed7e0b | ||
| 
						 | 
					c8b9c7d936 | ||
| 
						 | 
					d57057ba28 | ||
| 
						 | 
					66cee8daa4 | ||
| 
						 | 
					afd7df0942 | ||
| 
						 | 
					bd6ae33d32 | ||
| 
						 | 
					70660a0d68 | ||
| 
						 | 
					cdad18551a | ||
| 
						 | 
					592c51d1a5 | ||
| 
						 | 
					6a57b8a7e7 | ||
| 
						 | 
					7a94e21c54 | 
							
								
								
									
										4
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
			
		||||
node_modules
 | 
			
		||||
npm-debug.log
 | 
			
		||||
dist
 | 
			
		||||
.idea
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<dataSource name="document.db">
 | 
			
		||||
  <database-model serializer="dbm" rdbms="SQLITE" format-version="4.8">
 | 
			
		||||
  <database-model serializer="dbm" rdbms="SQLITE" format-version="4.9">
 | 
			
		||||
    <root id="1">
 | 
			
		||||
      <ServerVersion>3.16.1</ServerVersion>
 | 
			
		||||
    </root>
 | 
			
		||||
@@ -21,571 +21,718 @@
 | 
			
		||||
    <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="source_ids"/>
 | 
			
		||||
    <table id="17" parent="2" name="sqlite_master">
 | 
			
		||||
    <table id="16" parent="2" name="relations"/>
 | 
			
		||||
    <table id="17" parent="2" name="source_ids"/>
 | 
			
		||||
    <table id="18" parent="2" name="sqlite_master">
 | 
			
		||||
      <System>1</System>
 | 
			
		||||
    </table>
 | 
			
		||||
    <table id="18" parent="2" name="sqlite_sequence">
 | 
			
		||||
    <table id="19" parent="2" name="sqlite_sequence">
 | 
			
		||||
      <System>1</System>
 | 
			
		||||
    </table>
 | 
			
		||||
    <table id="19" parent="2" name="sync"/>
 | 
			
		||||
    <column id="20" parent="6" name="apiTokenId">
 | 
			
		||||
    <table id="20" parent="2" name="sync"/>
 | 
			
		||||
    <column id="21" parent="6" name="apiTokenId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="21" parent="6" name="token">
 | 
			
		||||
    <column id="22" parent="6" name="token">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="22" parent="6" name="dateCreated">
 | 
			
		||||
    <column id="23" parent="6" name="dateCreated">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="23" parent="6" name="isDeleted">
 | 
			
		||||
    <column id="24" parent="6" name="isDeleted">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="24" parent="6" name="sqlite_autoindex_api_tokens_1">
 | 
			
		||||
    <column id="25" 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">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>apiTokenId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="25" parent="6">
 | 
			
		||||
    <key id="27" parent="6">
 | 
			
		||||
      <ColNames>apiTokenId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="26" parent="7" name="branchId">
 | 
			
		||||
    <column id="28" parent="7" name="branchId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="27" parent="7" name="noteId">
 | 
			
		||||
    <column id="29" parent="7" name="noteId">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="28" parent="7" name="parentNoteId">
 | 
			
		||||
    <column id="30" parent="7" name="parentNoteId">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="29" parent="7" name="notePosition">
 | 
			
		||||
    <column id="31" parent="7" name="notePosition">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>INTEGER|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="30" parent="7" name="prefix">
 | 
			
		||||
    <column id="32" parent="7" name="prefix">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="31" parent="7" name="isExpanded">
 | 
			
		||||
    <column id="33" parent="7" name="isExpanded">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>BOOLEAN|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="32" parent="7" name="isDeleted">
 | 
			
		||||
    <column id="34" parent="7" name="isDeleted">
 | 
			
		||||
      <Position>7</Position>
 | 
			
		||||
      <DataType>INTEGER|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="33" parent="7" name="dateModified">
 | 
			
		||||
    <column id="35" parent="7" name="dateModified">
 | 
			
		||||
      <Position>8</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="34" parent="7" name="sqlite_autoindex_branches_1">
 | 
			
		||||
    <column id="36" parent="7" name="hash">
 | 
			
		||||
      <Position>9</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>""</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="37" parent="7" 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">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>branchId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="35" parent="7" name="IDX_branches_noteId_parentNoteId">
 | 
			
		||||
    <index id="39" parent="7" name="IDX_branches_noteId_parentNoteId">
 | 
			
		||||
      <ColNames>noteId
 | 
			
		||||
parentNoteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="36" parent="7" name="IDX_branches_noteId">
 | 
			
		||||
    <index id="40" parent="7" name="IDX_branches_noteId">
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="37" parent="7">
 | 
			
		||||
    <index id="41" parent="7" name="IDX_branches_parentNoteId">
 | 
			
		||||
      <ColNames>parentNoteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="42" parent="7">
 | 
			
		||||
      <ColNames>branchId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="38" parent="8" name="id">
 | 
			
		||||
    <column id="43" parent="8" name="eventId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>INTEGER|0s</DataType>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <SequenceIdentity>1</SequenceIdentity>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="39" parent="8" name="noteId">
 | 
			
		||||
    <column id="44" parent="8" name="noteId">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="40" parent="8" name="comment">
 | 
			
		||||
    <column id="45" parent="8" name="comment">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="41" parent="8" name="dateAdded">
 | 
			
		||||
    <column id="46" parent="8" name="dateCreated">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <key id="42" parent="8">
 | 
			
		||||
      <ColNames>id</ColNames>
 | 
			
		||||
    <index id="47" parent="8" name="sqlite_autoindex_event_log_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>eventId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="48" parent="8">
 | 
			
		||||
      <ColNames>eventId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_event_log_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="43" parent="9" name="imageId">
 | 
			
		||||
    <column id="49" parent="9" name="imageId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="44" parent="9" name="format">
 | 
			
		||||
    <column id="50" parent="9" name="format">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="45" parent="9" name="checksum">
 | 
			
		||||
    <column id="51" parent="9" name="checksum">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="46" parent="9" name="name">
 | 
			
		||||
    <column id="52" parent="9" name="name">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="47" parent="9" name="data">
 | 
			
		||||
    <column id="53" parent="9" name="data">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>BLOB|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="48" parent="9" name="isDeleted">
 | 
			
		||||
    <column id="54" parent="9" name="isDeleted">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="49" parent="9" name="dateModified">
 | 
			
		||||
    <column id="55" parent="9" name="dateModified">
 | 
			
		||||
      <Position>7</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="50" parent="9" name="dateCreated">
 | 
			
		||||
    <column id="56" parent="9" name="dateCreated">
 | 
			
		||||
      <Position>8</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="51" parent="9" name="sqlite_autoindex_images_1">
 | 
			
		||||
    <column id="57" parent="9" 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">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>imageId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="52" parent="9">
 | 
			
		||||
    <key id="59" parent="9">
 | 
			
		||||
      <ColNames>imageId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="53" parent="10" name="labelId">
 | 
			
		||||
    <column id="60" parent="10" name="labelId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="54" parent="10" name="noteId">
 | 
			
		||||
    <column id="61" parent="10" name="noteId">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="55" parent="10" name="name">
 | 
			
		||||
    <column id="62" parent="10" name="name">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="56" parent="10" name="value">
 | 
			
		||||
    <column id="63" parent="10" name="value">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>''</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="57" parent="10" name="position">
 | 
			
		||||
    <column id="64" parent="10" name="position">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="58" parent="10" name="dateCreated">
 | 
			
		||||
    <column id="65" parent="10" name="dateCreated">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="59" parent="10" name="dateModified">
 | 
			
		||||
    <column id="66" parent="10" name="dateModified">
 | 
			
		||||
      <Position>7</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="60" parent="10" name="isDeleted">
 | 
			
		||||
    <column id="67" parent="10" name="isDeleted">
 | 
			
		||||
      <Position>8</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="61" parent="10" name="sqlite_autoindex_labels_1">
 | 
			
		||||
    <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="62" parent="10" name="IDX_labels_noteId">
 | 
			
		||||
    <index id="70" parent="10" name="IDX_labels_noteId">
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="63" parent="10" name="IDX_labels_name_value">
 | 
			
		||||
    <index id="71" parent="10" name="IDX_labels_name_value">
 | 
			
		||||
      <ColNames>name
 | 
			
		||||
value</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="64" parent="10">
 | 
			
		||||
    <key id="72" parent="10">
 | 
			
		||||
      <ColNames>labelId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="65" parent="11" name="noteImageId">
 | 
			
		||||
    <column id="73" parent="11" name="noteImageId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="66" parent="11" name="noteId">
 | 
			
		||||
    <column id="74" parent="11" name="noteId">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="67" parent="11" name="imageId">
 | 
			
		||||
    <column id="75" parent="11" name="imageId">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="68" parent="11" name="isDeleted">
 | 
			
		||||
    <column id="76" parent="11" name="isDeleted">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="69" parent="11" name="dateModified">
 | 
			
		||||
    <column id="77" parent="11" name="dateModified">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="70" parent="11" name="dateCreated">
 | 
			
		||||
    <column id="78" parent="11" name="dateCreated">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="71" parent="11" name="sqlite_autoindex_note_images_1">
 | 
			
		||||
    <column id="79" 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">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>noteImageId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="72" parent="11" name="IDX_note_images_noteId_imageId">
 | 
			
		||||
    <index id="81" parent="11" name="IDX_note_images_noteId_imageId">
 | 
			
		||||
      <ColNames>noteId
 | 
			
		||||
imageId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="73" parent="11" name="IDX_note_images_noteId">
 | 
			
		||||
    <index id="82" parent="11" name="IDX_note_images_noteId">
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="74" parent="11" name="IDX_note_images_imageId">
 | 
			
		||||
    <index id="83" parent="11" name="IDX_note_images_imageId">
 | 
			
		||||
      <ColNames>imageId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="75" parent="11">
 | 
			
		||||
    <key id="84" parent="11">
 | 
			
		||||
      <ColNames>noteImageId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="76" parent="12" name="noteRevisionId">
 | 
			
		||||
    <column id="85" parent="12" name="noteRevisionId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="77" parent="12" name="noteId">
 | 
			
		||||
    <column id="86" parent="12" name="noteId">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="78" parent="12" name="title">
 | 
			
		||||
    <column id="87" parent="12" name="title">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="79" parent="12" name="content">
 | 
			
		||||
    <column id="88" parent="12" name="content">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="80" parent="12" name="isProtected">
 | 
			
		||||
    <column id="89" parent="12" name="isProtected">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="81" parent="12" name="dateModifiedFrom">
 | 
			
		||||
    <column id="90" parent="12" name="dateModifiedFrom">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="82" parent="12" name="dateModifiedTo">
 | 
			
		||||
    <column id="91" parent="12" name="dateModifiedTo">
 | 
			
		||||
      <Position>7</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="83" parent="12" name="type">
 | 
			
		||||
    <column id="92" parent="12" name="type">
 | 
			
		||||
      <Position>8</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>''</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="84" parent="12" name="mime">
 | 
			
		||||
    <column id="93" parent="12" name="mime">
 | 
			
		||||
      <Position>9</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>''</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="85" parent="12" name="sqlite_autoindex_note_revisions_1">
 | 
			
		||||
    <column id="94" 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">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>noteRevisionId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="86" parent="12" name="IDX_note_revisions_noteId">
 | 
			
		||||
    <index id="96" parent="12" name="IDX_note_revisions_noteId">
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="87" parent="12" name="IDX_note_revisions_dateModifiedFrom">
 | 
			
		||||
    <index id="97" parent="12" name="IDX_note_revisions_dateModifiedFrom">
 | 
			
		||||
      <ColNames>dateModifiedFrom</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="88" parent="12" name="IDX_note_revisions_dateModifiedTo">
 | 
			
		||||
    <index id="98" parent="12" name="IDX_note_revisions_dateModifiedTo">
 | 
			
		||||
      <ColNames>dateModifiedTo</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="89" parent="12">
 | 
			
		||||
    <key id="99" parent="12">
 | 
			
		||||
      <ColNames>noteRevisionId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="90" parent="13" name="noteId">
 | 
			
		||||
    <column id="100" parent="13" name="noteId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="91" parent="13" name="title">
 | 
			
		||||
    <column id="101" parent="13" name="title">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>"unnamed"</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="92" parent="13" name="content">
 | 
			
		||||
    <column id="102" parent="13" name="content">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>""</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="93" parent="13" name="isProtected">
 | 
			
		||||
    <column id="103" parent="13" name="isProtected">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="94" parent="13" name="isDeleted">
 | 
			
		||||
    <column id="104" parent="13" name="isDeleted">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="95" parent="13" name="dateCreated">
 | 
			
		||||
    <column id="105" parent="13" name="dateCreated">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="96" parent="13" name="dateModified">
 | 
			
		||||
    <column id="106" parent="13" name="dateModified">
 | 
			
		||||
      <Position>7</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="97" parent="13" name="type">
 | 
			
		||||
    <column id="107" parent="13" name="type">
 | 
			
		||||
      <Position>8</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>'text'</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="98" parent="13" name="mime">
 | 
			
		||||
    <column id="108" parent="13" name="mime">
 | 
			
		||||
      <Position>9</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>'text/html'</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="99" parent="13" name="sqlite_autoindex_notes_1">
 | 
			
		||||
    <column id="109" 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">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="100" parent="13" name="IDX_notes_isDeleted">
 | 
			
		||||
      <ColNames>isDeleted</ColNames>
 | 
			
		||||
    <index id="111" parent="13" name="IDX_notes_type">
 | 
			
		||||
      <ColNames>type</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="101" parent="13">
 | 
			
		||||
    <key id="112" parent="13">
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="102" parent="14" name="name">
 | 
			
		||||
    <column id="113" parent="14" name="name">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="103" parent="14" name="value">
 | 
			
		||||
    <column id="114" parent="14" name="value">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="104" parent="14" name="dateModified">
 | 
			
		||||
    <column id="115" parent="14" name="dateModified">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="105" parent="14" name="isSynced">
 | 
			
		||||
    <column id="116" parent="14" name="isSynced">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>INTEGER|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="106" parent="14" name="sqlite_autoindex_options_1">
 | 
			
		||||
    <column id="117" parent="14" name="hash">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>""</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="118" 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">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>name</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="107" parent="14">
 | 
			
		||||
    <key id="120" parent="14">
 | 
			
		||||
      <ColNames>name</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="108" parent="15" name="branchId">
 | 
			
		||||
    <column id="121" parent="15" name="branchId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="109" parent="15" name="notePath">
 | 
			
		||||
    <column id="122" parent="15" name="notePath">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="110" parent="15" name="dateAccessed">
 | 
			
		||||
    <column id="123" parent="15" name="hash">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>""</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="111" parent="15" name="isDeleted">
 | 
			
		||||
    <column id="124" parent="15" name="dateCreated">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="125" parent="15" name="isDeleted">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="112" parent="15" name="sqlite_autoindex_recent_notes_1">
 | 
			
		||||
    <index id="126" parent="15" name="sqlite_autoindex_recent_notes_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>branchId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="113" parent="15">
 | 
			
		||||
    <key id="127" parent="15">
 | 
			
		||||
      <ColNames>branchId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="114" parent="16" name="sourceId">
 | 
			
		||||
    <column id="128" parent="16" name="relationId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="115" parent="16" name="dateCreated">
 | 
			
		||||
    <column id="129" parent="16" name="sourceNoteId">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="116" parent="16" name="sqlite_autoindex_source_ids_1">
 | 
			
		||||
    <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">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>sourceId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="117" parent="16">
 | 
			
		||||
    <key id="144" parent="17">
 | 
			
		||||
      <ColNames>sourceId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="118" parent="17" name="type">
 | 
			
		||||
    <column id="145" parent="18" name="type">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>text|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="119" parent="17" name="name">
 | 
			
		||||
    <column id="146" parent="18" name="name">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>text|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="120" parent="17" name="tbl_name">
 | 
			
		||||
    <column id="147" parent="18" name="tbl_name">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>text|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="121" parent="17" name="rootpage">
 | 
			
		||||
    <column id="148" parent="18" name="rootpage">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>integer|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="122" parent="17" name="sql">
 | 
			
		||||
    <column id="149" parent="18" name="sql">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>text|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="123" parent="18" name="name">
 | 
			
		||||
    <column id="150" parent="19" name="name">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="124" parent="18" name="seq">
 | 
			
		||||
    <column id="151" parent="19" name="seq">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="125" parent="19" name="id">
 | 
			
		||||
    <column id="152" parent="20" name="id">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>INTEGER|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <SequenceIdentity>1</SequenceIdentity>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="126" parent="19" name="entityName">
 | 
			
		||||
    <column id="153" parent="20" name="entityName">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="127" parent="19" name="entityId">
 | 
			
		||||
    <column id="154" parent="20" name="entityId">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="128" parent="19" name="sourceId">
 | 
			
		||||
    <column id="155" parent="20" name="sourceId">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="129" parent="19" name="syncDate">
 | 
			
		||||
    <column id="156" parent="20" name="syncDate">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="130" parent="19" name="IDX_sync_entityName_entityId">
 | 
			
		||||
    <index id="157" parent="20" name="IDX_sync_entityName_entityId">
 | 
			
		||||
      <ColNames>entityName
 | 
			
		||||
entityId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="131" parent="19" name="IDX_sync_syncDate">
 | 
			
		||||
    <index id="158" parent="20" name="IDX_sync_syncDate">
 | 
			
		||||
      <ColNames>syncDate</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="132" parent="19">
 | 
			
		||||
    <key id="159" parent="20">
 | 
			
		||||
      <ColNames>id</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
    </key>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
			
		||||
FROM node:8.11.2
 | 
			
		||||
 | 
			
		||||
RUN apt-get update && apt-get install -y nasm
 | 
			
		||||
 | 
			
		||||
# Create app directory
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
 | 
			
		||||
# Install app dependencies
 | 
			
		||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
 | 
			
		||||
# where available (npm@5+)
 | 
			
		||||
COPY package*.json ./
 | 
			
		||||
 | 
			
		||||
RUN npm install --production
 | 
			
		||||
# If you are building your code for production
 | 
			
		||||
# RUN npm install --only=production
 | 
			
		||||
 | 
			
		||||
# Bundle app source
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
EXPOSE 8080
 | 
			
		||||
CMD [ "node", "src/www" ]
 | 
			
		||||
							
								
								
									
										8
									
								
								bin/build-docker.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
if [[ $# -eq 0 ]] ; then
 | 
			
		||||
    echo "Missing argument of new version"
 | 
			
		||||
    exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
sudo docker build -t zadam/trilium:latest -t zadam/trilium:$1 .
 | 
			
		||||
							
								
								
									
										16
									
								
								bin/build-pkg.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
PKG_DIR=dist/trilium-linux-x64-server
 | 
			
		||||
 | 
			
		||||
mkdir $PKG_DIR
 | 
			
		||||
 | 
			
		||||
pkg . --targets node8-linux-x64 --output ${PKG_DIR}/trilium
 | 
			
		||||
 | 
			
		||||
chmod +x ${PKG_DIR}/trilium
 | 
			
		||||
 | 
			
		||||
cp node_modules/sqlite3/lib/binding/node-v57-linux-x64/node_sqlite3.node ${PKG_DIR}/
 | 
			
		||||
cp node_modules/scrypt/build/Release/scrypt.node ${PKG_DIR}/
 | 
			
		||||
 | 
			
		||||
cd dist
 | 
			
		||||
 | 
			
		||||
7z a trilium-linux-x64-server.7z trilium-linux-x64-server
 | 
			
		||||
							
								
								
									
										9
									
								
								bin/push-docker-image.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
if [[ $# -eq 0 ]] ; then
 | 
			
		||||
    echo "Missing argument of new version"
 | 
			
		||||
    exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
sudo docker push zadam/trilium:latest
 | 
			
		||||
sudo docker push zadam/trilium:$1
 | 
			
		||||
@@ -47,6 +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
 | 
			
		||||
 | 
			
		||||
echo "Creating release in GitHub"
 | 
			
		||||
 | 
			
		||||
@@ -75,4 +76,21 @@ github-release upload \
 | 
			
		||||
    --name "$WINDOWS_X64_BUILD" \
 | 
			
		||||
    --file "dist/$WINDOWS_X64_BUILD"
 | 
			
		||||
 | 
			
		||||
echo "Packaging server version"
 | 
			
		||||
 | 
			
		||||
npm run build-pkg
 | 
			
		||||
 | 
			
		||||
github-release upload \
 | 
			
		||||
    --tag $TAG \
 | 
			
		||||
    --name "$SERVER_BUILD" \
 | 
			
		||||
    --file "dist/$SERVER_BUILD"
 | 
			
		||||
 | 
			
		||||
echo "Building docker image"
 | 
			
		||||
 | 
			
		||||
bin/build-docker.sh $VERSION
 | 
			
		||||
 | 
			
		||||
echo "Pushing docker image to dockerhub"
 | 
			
		||||
 | 
			
		||||
bin/push-docker-image.sh $VERSION
 | 
			
		||||
 | 
			
		||||
echo "Release finished!"
 | 
			
		||||
@@ -9,9 +9,3 @@ https=false
 | 
			
		||||
# path to certificate (run "bash generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
 | 
			
		||||
certPath=
 | 
			
		||||
keyPath=
 | 
			
		||||
 | 
			
		||||
[Sync]
 | 
			
		||||
syncServerHost=
 | 
			
		||||
syncServerTimeout=10000
 | 
			
		||||
syncProxy=
 | 
			
		||||
syncServerCertificate=
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('root', 'root', 'none', 0, null, 1, 0, '2018-01-01T00:00:00.000Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('dLgtLUFn3GoN', '1Heh2acXfPNt', 'root', 21, null, 1, 0, '2017-12-23T00:46:39.304Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QLfS835GSfIh', '3RkyK9LI18dO', '1Heh2acXfPNt', 1, null, 1, 0, '2017-12-23T01:20:04.181Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QJAcYJ1gGUh9', 'L1Ox40M1aEyy', '3RkyK9LI18dO', 0, null, 0, 0, '2017-12-23T01:20:45.365Z');
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								db/migrations/0089__add_root_branch.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, dateModified)
 | 
			
		||||
    VALUES ('root', 'root', 'none', 0, null, 1, '2018-01-01T00:00:00.000Z');
 | 
			
		||||
 | 
			
		||||
INSERT INTO sync (entityName, entityId, sourceId, syncDate)
 | 
			
		||||
    VALUES ('branches' ,'root', 'SYNC_FILL', '2018-01-01T00:00:00.000Z');
 | 
			
		||||
							
								
								
									
										1
									
								
								db/migrations/0090__branch_index.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
 | 
			
		||||
							
								
								
									
										2
									
								
								db/migrations/0091__drop_isDeleted_index.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
-- index confuses planner and is mostly useless anyway since we're mostly used in non-deleted notes (which are presumably majority)
 | 
			
		||||
DROP INDEX IDX_notes_isDeleted;
 | 
			
		||||
							
								
								
									
										2
									
								
								db/migrations/0092__add_type_index.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
create index IDX_notes_type
 | 
			
		||||
  on notes (type);
 | 
			
		||||
							
								
								
									
										9
									
								
								db/migrations/0093__add_hash_field.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
ALTER TABLE notes ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE branches ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE note_revisions ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE recent_notes ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE options ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE note_images ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE images ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE labels ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE api_tokens ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
							
								
								
									
										30
									
								
								db/migrations/0094__unify_auditing_fields.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,30 @@
 | 
			
		||||
ALTER TABLE branches ADD dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
 | 
			
		||||
 | 
			
		||||
CREATE TABLE `event_log_mig` (
 | 
			
		||||
  `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
  `noteId`	TEXT,
 | 
			
		||||
  `comment`	TEXT,
 | 
			
		||||
  `dateCreated`	TEXT NOT NULL
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
INSERT INTO event_log_mig (id, noteId, comment, dateCreated)
 | 
			
		||||
SELECT id, noteId, comment, dateAdded FROM event_log;
 | 
			
		||||
 | 
			
		||||
DROP TABLE event_log;
 | 
			
		||||
ALTER TABLE event_log_mig RENAME TO event_log;
 | 
			
		||||
 | 
			
		||||
ALTER TABLE options ADD dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
 | 
			
		||||
 | 
			
		||||
CREATE TABLE `recent_notes_mig` (
 | 
			
		||||
  `branchId` TEXT NOT NULL PRIMARY KEY,
 | 
			
		||||
  `notePath` TEXT NOT NULL,
 | 
			
		||||
  hash TEXT DEFAULT "" NOT NULL,
 | 
			
		||||
  `dateCreated` TEXT NOT NULL,
 | 
			
		||||
  isDeleted INT
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
INSERT INTO recent_notes_mig (branchId, notePath, hash, dateCreated, isDeleted)
 | 
			
		||||
SELECT branchId, notePath, hash, dateAccessed, isDeleted FROM recent_notes;
 | 
			
		||||
 | 
			
		||||
DROP TABLE recent_notes;
 | 
			
		||||
ALTER TABLE recent_notes_mig RENAME TO recent_notes;
 | 
			
		||||
							
								
								
									
										1
									
								
								db/migrations/0095__mime_type_for_render.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
UPDATE notes SET mime = 'text/html' WHERE type = 'render';
 | 
			
		||||
							
								
								
									
										29
									
								
								db/migrations/0096__unify_surrogate_keys.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,29 @@
 | 
			
		||||
CREATE TABLE `event_log_mig` (
 | 
			
		||||
  `eventId`	TEXT NOT NULL PRIMARY KEY,
 | 
			
		||||
  `noteId`	TEXT,
 | 
			
		||||
  `comment`	TEXT,
 | 
			
		||||
  `dateCreated`	TEXT NOT NULL
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
INSERT INTO event_log_mig (eventId, noteId, comment, dateCreated)
 | 
			
		||||
SELECT id, noteId, comment, dateCreated FROM event_log;
 | 
			
		||||
 | 
			
		||||
DROP TABLE event_log;
 | 
			
		||||
ALTER TABLE event_log_mig RENAME TO event_log;
 | 
			
		||||
 | 
			
		||||
create table options_mig
 | 
			
		||||
(
 | 
			
		||||
  optionId TEXT NOT NULL PRIMARY KEY,
 | 
			
		||||
  name TEXT not null,
 | 
			
		||||
  value TEXT,
 | 
			
		||||
  dateModified INT,
 | 
			
		||||
  isSynced INTEGER default 0 not null,
 | 
			
		||||
  hash TEXT default "" not null,
 | 
			
		||||
  dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
INSERT INTO options_mig (optionId, name, value, dateModified, isSynced, hash, dateCreated)
 | 
			
		||||
  SELECT name || "_key", name, value, dateModified, isSynced, hash, dateCreated FROM options;
 | 
			
		||||
 | 
			
		||||
DROP TABLE options;
 | 
			
		||||
ALTER TABLE options_mig RENAME TO options;
 | 
			
		||||
							
								
								
									
										2
									
								
								db/migrations/0097__add_zoomFactor.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
INSERT INTO options (optionId, name, value, dateCreated, dateModified, isSynced)
 | 
			
		||||
VALUES ('zoomFactor_key', 'zoomFactor', '1.0', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
 | 
			
		||||
							
								
								
									
										1
									
								
								db/migrations/0098__rename_hideInAutocomplete.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
UPDATE labels SET name = 'archived' WHERE name = 'hideInAutocomplete'
 | 
			
		||||
							
								
								
									
										2
									
								
								db/migrations/0099__add_theme_option.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
INSERT INTO options (optionId, name, value, dateCreated, dateModified, isSynced)
 | 
			
		||||
VALUES ('theme_key', 'theme', 'white', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
 | 
			
		||||
							
								
								
									
										15
									
								
								db/migrations/0100__remove_optionId.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
create table options_mig
 | 
			
		||||
(
 | 
			
		||||
  name TEXT not null PRIMARY KEY,
 | 
			
		||||
  value TEXT,
 | 
			
		||||
  dateModified INT,
 | 
			
		||||
  isSynced INTEGER default 0 not null,
 | 
			
		||||
  hash TEXT default "" not null,
 | 
			
		||||
  dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
INSERT INTO options_mig (name, value, dateModified, isSynced, hash, dateCreated)
 | 
			
		||||
SELECT name, value, dateModified, isSynced, hash, dateCreated FROM options;
 | 
			
		||||
 | 
			
		||||
DROP TABLE options;
 | 
			
		||||
ALTER TABLE options_mig RENAME TO options;
 | 
			
		||||
							
								
								
									
										8
									
								
								db/migrations/0101__add_sync_options.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
			
		||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
 | 
			
		||||
VALUES ('syncServerHost', '', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
 | 
			
		||||
 | 
			
		||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
 | 
			
		||||
VALUES ('syncServerTimeout', '5000', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
 | 
			
		||||
 | 
			
		||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
 | 
			
		||||
VALUES ('syncProxy', '', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
 | 
			
		||||
							
								
								
									
										2
									
								
								db/migrations/0102__fix_sync_entityIds.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
DELETE FROM sync WHERE entityName = 'note_tree';
 | 
			
		||||
DELETE FROM sync WHERE entityName = 'attributes';
 | 
			
		||||
							
								
								
									
										2
									
								
								db/migrations/0103__add_initialized_option.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
 | 
			
		||||
VALUES ('initialized', 'true', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
 | 
			
		||||
							
								
								
									
										4
									
								
								db/migrations/0104__fill_sync_rows_for_options.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
			
		||||
const syncTableService = require('../../src/services/sync_table');
 | 
			
		||||
 | 
			
		||||
// options has not been filled so far which caused problems with clean-slate sync.
 | 
			
		||||
module.exports = async () => await syncTableService.fillAllSyncRows();
 | 
			
		||||
@@ -0,0 +1,2 @@
 | 
			
		||||
UPDATE notes SET content = '' WHERE isDeleted = 1;
 | 
			
		||||
UPDATE note_revisions SET content = '' WHERE (SELECT isDeleted FROM notes WHERE noteId = note_revisions.noteId) = 1;
 | 
			
		||||
							
								
								
									
										15
									
								
								db/migrations/0106__add_relations_table.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
CREATE TABLE relations
 | 
			
		||||
(
 | 
			
		||||
    relationId  TEXT not null primary key,
 | 
			
		||||
    sourceNoteId       TEXT not null,
 | 
			
		||||
    name         TEXT not null,
 | 
			
		||||
    targetNoteId        TEXT not null,
 | 
			
		||||
    position     INT  default 0 not null,
 | 
			
		||||
    dateCreated  TEXT not null,
 | 
			
		||||
    dateModified TEXT not null,
 | 
			
		||||
    isDeleted    INT  not null
 | 
			
		||||
  , hash TEXT DEFAULT "" NOT NULL);
 | 
			
		||||
CREATE INDEX IDX_relation_sourceNoteId
 | 
			
		||||
  on relations (sourceNoteId);
 | 
			
		||||
CREATE INDEX IDX_relation_targetNoteId
 | 
			
		||||
  on relations (targetNoteId);
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
ALTER TABLE relations ADD isInheritable int DEFAULT 0 NULL;
 | 
			
		||||
@@ -1,8 +1,3 @@
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "options" (
 | 
			
		||||
    `name`	TEXT NOT NULL PRIMARY KEY,
 | 
			
		||||
    `value`	TEXT,
 | 
			
		||||
    `dateModified` INT,
 | 
			
		||||
    isSynced INTEGER NOT NULL DEFAULT 0);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "sync" (
 | 
			
		||||
  `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
  `entityName`	TEXT NOT NULL,
 | 
			
		||||
@@ -29,7 +24,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (
 | 
			
		||||
  `isProtected`	INT NOT NULL DEFAULT 0,
 | 
			
		||||
  `dateModifiedFrom` TEXT NOT NULL,
 | 
			
		||||
  `dateModifiedTo` TEXT NOT NULL
 | 
			
		||||
, type TEXT DEFAULT '' NOT NULL, mime TEXT DEFAULT '' NOT NULL);
 | 
			
		||||
, type TEXT DEFAULT '' NOT NULL, mime TEXT DEFAULT '' NOT NULL, hash TEXT DEFAULT "" NOT NULL);
 | 
			
		||||
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
 | 
			
		||||
  `noteId`
 | 
			
		||||
);
 | 
			
		||||
@@ -49,7 +44,7 @@ CREATE TABLE IF NOT EXISTS "images"
 | 
			
		||||
  isDeleted INT NOT NULL DEFAULT 0,
 | 
			
		||||
  dateModified TEXT NOT NULL,
 | 
			
		||||
  dateCreated TEXT NOT NULL
 | 
			
		||||
);
 | 
			
		||||
, hash TEXT DEFAULT "" NOT NULL);
 | 
			
		||||
CREATE TABLE note_images
 | 
			
		||||
(
 | 
			
		||||
  noteImageId TEXT PRIMARY KEY NOT NULL,
 | 
			
		||||
@@ -58,7 +53,7 @@ CREATE TABLE note_images
 | 
			
		||||
  isDeleted INT NOT NULL DEFAULT 0,
 | 
			
		||||
  dateModified TEXT NOT NULL,
 | 
			
		||||
  dateCreated TEXT NOT NULL
 | 
			
		||||
);
 | 
			
		||||
, hash TEXT DEFAULT "" NOT NULL);
 | 
			
		||||
CREATE INDEX IDX_note_images_noteId ON note_images (noteId);
 | 
			
		||||
CREATE INDEX IDX_note_images_imageId ON note_images (imageId);
 | 
			
		||||
CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId);
 | 
			
		||||
@@ -68,7 +63,7 @@ CREATE TABLE IF NOT EXISTS "api_tokens"
 | 
			
		||||
  token TEXT NOT NULL,
 | 
			
		||||
  dateCreated TEXT NOT NULL,
 | 
			
		||||
  isDeleted INT NOT NULL DEFAULT 0
 | 
			
		||||
);
 | 
			
		||||
, hash TEXT DEFAULT "" NOT NULL);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "branches" (
 | 
			
		||||
  `branchId`	TEXT NOT NULL,
 | 
			
		||||
  `noteId`	TEXT NOT NULL,
 | 
			
		||||
@@ -77,7 +72,7 @@ CREATE TABLE IF NOT EXISTS "branches" (
 | 
			
		||||
  `prefix`	TEXT,
 | 
			
		||||
  `isExpanded`	BOOLEAN,
 | 
			
		||||
  `isDeleted`	INTEGER NOT NULL DEFAULT 0,
 | 
			
		||||
  `dateModified`	TEXT NOT NULL,
 | 
			
		||||
  `dateModified`	TEXT NOT NULL, hash TEXT DEFAULT "" NOT NULL, dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z',
 | 
			
		||||
  PRIMARY KEY(`branchId`)
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_branches_noteId` ON `branches` (
 | 
			
		||||
@@ -87,12 +82,6 @@ CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
 | 
			
		||||
  `noteId`,
 | 
			
		||||
  `parentNoteId`
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "recent_notes" (
 | 
			
		||||
  `branchId` TEXT NOT NULL PRIMARY KEY,
 | 
			
		||||
  `notePath` TEXT NOT NULL,
 | 
			
		||||
  `dateAccessed` TEXT NOT NULL,
 | 
			
		||||
  isDeleted INT
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE labels
 | 
			
		||||
(
 | 
			
		||||
  labelId  TEXT not null primary key,
 | 
			
		||||
@@ -103,18 +92,11 @@ CREATE TABLE labels
 | 
			
		||||
  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 "event_log"
 | 
			
		||||
(
 | 
			
		||||
  id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 | 
			
		||||
  noteId TEXT,
 | 
			
		||||
  comment TEXT,
 | 
			
		||||
  dateAdded TEXT NOT NULL
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "notes" (
 | 
			
		||||
  `noteId`	TEXT NOT NULL,
 | 
			
		||||
  `title`	TEXT NOT NULL DEFAULT "unnamed",
 | 
			
		||||
@@ -124,9 +106,47 @@ CREATE TABLE IF NOT EXISTS "notes" (
 | 
			
		||||
  `dateCreated`	TEXT NOT NULL,
 | 
			
		||||
  `dateModified`	TEXT NOT NULL,
 | 
			
		||||
  type TEXT NOT NULL DEFAULT 'text',
 | 
			
		||||
  mime TEXT NOT NULL DEFAULT 'text/html',
 | 
			
		||||
  mime TEXT NOT NULL DEFAULT 'text/html', hash TEXT DEFAULT "" NOT NULL,
 | 
			
		||||
  PRIMARY KEY(`noteId`)
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
 | 
			
		||||
  `isDeleted`
 | 
			
		||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
 | 
			
		||||
CREATE INDEX IDX_notes_type
 | 
			
		||||
  on notes (type);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "recent_notes" (
 | 
			
		||||
  `branchId` TEXT NOT NULL PRIMARY KEY,
 | 
			
		||||
  `notePath` TEXT NOT NULL,
 | 
			
		||||
  hash TEXT DEFAULT "" NOT NULL,
 | 
			
		||||
  `dateCreated` TEXT NOT NULL,
 | 
			
		||||
  isDeleted INT
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "event_log" (
 | 
			
		||||
  `eventId`	TEXT NOT NULL PRIMARY KEY,
 | 
			
		||||
  `noteId`	TEXT,
 | 
			
		||||
  `comment`	TEXT,
 | 
			
		||||
  `dateCreated`	TEXT NOT NULL
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "options"
 | 
			
		||||
(
 | 
			
		||||
  name TEXT not null PRIMARY KEY,
 | 
			
		||||
  value TEXT,
 | 
			
		||||
  dateModified INT,
 | 
			
		||||
  isSynced INTEGER default 0 not null,
 | 
			
		||||
  hash TEXT default "" not null,
 | 
			
		||||
  dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE relations
 | 
			
		||||
(
 | 
			
		||||
    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);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5447
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										68
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -1,19 +1,22 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "trilium",
 | 
			
		||||
  "description": "Trilium Notes",
 | 
			
		||||
  "version": "0.11.1",
 | 
			
		||||
  "version": "0.18.0",
 | 
			
		||||
  "license": "AGPL-3.0-only",
 | 
			
		||||
  "main": "electron.js",
 | 
			
		||||
  "bin": {
 | 
			
		||||
    "trilium": "./src/www"
 | 
			
		||||
  },
 | 
			
		||||
  "repository": {
 | 
			
		||||
    "type": "git",
 | 
			
		||||
    "url": "https://github.com/zadam/trilium.git"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "node ./bin/www",
 | 
			
		||||
    "start": "node ./src/www",
 | 
			
		||||
    "test-electron": "xo",
 | 
			
		||||
    "rebuild-electron": "electron-rebuild",
 | 
			
		||||
    "start-electron": "electron . --disable-gpu",
 | 
			
		||||
    "build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64",
 | 
			
		||||
    "build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64 --app-version=",
 | 
			
		||||
    "start-forge": "electron-forge start",
 | 
			
		||||
    "package-forge": "electron-forge package",
 | 
			
		||||
    "make-forge": "electron-forge make",
 | 
			
		||||
@@ -21,22 +24,20 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "async-mutex": "^0.1.3",
 | 
			
		||||
    "axios": "^0.17.1",
 | 
			
		||||
    "body-parser": "~1.18.2",
 | 
			
		||||
    "axios": "^0.18",
 | 
			
		||||
    "body-parser": "^1.18.3",
 | 
			
		||||
    "cls-hooked": "^4.2.2",
 | 
			
		||||
    "cookie-parser": "~1.4.3",
 | 
			
		||||
    "debug": "~3.1.0",
 | 
			
		||||
    "devtron": "^1.4.0",
 | 
			
		||||
    "ejs": "~2.5.7",
 | 
			
		||||
    "electron": "^2.0.0-beta.5",
 | 
			
		||||
    "electron-debug": "^1.5.0",
 | 
			
		||||
    "electron-dl": "^1.11.0",
 | 
			
		||||
    "electron-in-page-search": "^1.2.4",
 | 
			
		||||
    "electron-rebuild": "^1.7.3",
 | 
			
		||||
    "ejs": "~2.6.1",
 | 
			
		||||
    "electron-debug": "^2.0.0",
 | 
			
		||||
    "electron-dl": "^1.12.0",
 | 
			
		||||
    "electron-in-page-search": "^1.3.2",
 | 
			
		||||
    "express": "~4.16.3",
 | 
			
		||||
    "express-session": "^1.15.6",
 | 
			
		||||
    "fs-extra": "^4.0.3",
 | 
			
		||||
    "helmet": "^3.12.0",
 | 
			
		||||
    "fs-extra": "^6.0.1",
 | 
			
		||||
    "helmet": "^3.12.1",
 | 
			
		||||
    "html": "^1.0.0",
 | 
			
		||||
    "image-type": "^3.0.0",
 | 
			
		||||
    "imagemin": "^5.3.1",
 | 
			
		||||
@@ -45,30 +46,35 @@
 | 
			
		||||
    "imagemin-pngquant": "^5.1.0",
 | 
			
		||||
    "ini": "^1.3.5",
 | 
			
		||||
    "jimp": "^0.2.28",
 | 
			
		||||
    "moment": "^2.21.0",
 | 
			
		||||
    "multer": "^1.3.0",
 | 
			
		||||
    "moment": "^2.22.2",
 | 
			
		||||
    "multer": "^1.3.1",
 | 
			
		||||
    "open": "0.0.5",
 | 
			
		||||
    "rand-token": "^0.4.0",
 | 
			
		||||
    "request": "^2.85.0",
 | 
			
		||||
    "rcedit": "^1.1.0",
 | 
			
		||||
    "request": "^2.87.0",
 | 
			
		||||
    "request-promise": "^4.2.2",
 | 
			
		||||
    "rimraf": "^2.6.2",
 | 
			
		||||
    "sanitize-filename": "^1.6.1",
 | 
			
		||||
    "scrypt": "^6.0.3",
 | 
			
		||||
    "serve-favicon": "~2.4.5",
 | 
			
		||||
    "serve-favicon": "~2.5.0",
 | 
			
		||||
    "session-file-store": "^1.2.0",
 | 
			
		||||
    "simple-node-logger": "^0.93.37",
 | 
			
		||||
    "sqlite": "^2.9.1",
 | 
			
		||||
    "tar-stream": "^1.5.5",
 | 
			
		||||
    "sqlite": "^2.9.2",
 | 
			
		||||
    "tar-stream": "^1.6.1",
 | 
			
		||||
    "unescape": "^1.0.1",
 | 
			
		||||
    "ws": "^3.3.3"
 | 
			
		||||
    "ws": "^5.2.1",
 | 
			
		||||
    "xml2js": "^0.4.19"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "electron-compile": "^6.4.2",
 | 
			
		||||
    "electron-packager": "^11.1.0",
 | 
			
		||||
    "electron-prebuilt-compile": "2.0.0-beta.5",
 | 
			
		||||
    "lorem-ipsum": "^1.0.4",
 | 
			
		||||
    "tape": "^4.9.0",
 | 
			
		||||
    "xo": "^0.18.0"
 | 
			
		||||
    "electron": "^2.0.4",
 | 
			
		||||
    "electron-compile": "^6.4.3",
 | 
			
		||||
    "electron-packager": "^12.1.0",
 | 
			
		||||
    "electron-prebuilt-compile": "2.0.4",
 | 
			
		||||
    "electron-rebuild": "^1.8.1",
 | 
			
		||||
    "lorem-ipsum": "^1.0.5",
 | 
			
		||||
    "tape": "^4.9.1",
 | 
			
		||||
    "xo": "^0.21.1",
 | 
			
		||||
    "pkg": "^4.3.3"
 | 
			
		||||
  },
 | 
			
		||||
  "config": {
 | 
			
		||||
    "forge": {
 | 
			
		||||
@@ -107,5 +113,15 @@
 | 
			
		||||
      "node",
 | 
			
		||||
      "browser"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "pkg": {
 | 
			
		||||
    "assets": [
 | 
			
		||||
      "./db/**/*",
 | 
			
		||||
      "./src/public/**/*",
 | 
			
		||||
      "./src/views/**/*",
 | 
			
		||||
      "./node_modules/mozjpeg/vendor/*",
 | 
			
		||||
      "./node_modules/pngquant-bin/vendor/*",
 | 
			
		||||
      "./node_modules/giflossy/vendor/*"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ const sessionParser = session({
 | 
			
		||||
    cookie: {
 | 
			
		||||
        //    path: "/",
 | 
			
		||||
        httpOnly: true,
 | 
			
		||||
        maxAge:  1800000
 | 
			
		||||
        maxAge:  24 * 60 * 60 * 1000 // in milliseconds
 | 
			
		||||
    },
 | 
			
		||||
    store: new FileStore({
 | 
			
		||||
        ttl: 30 * 24 * 3600,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ const dateUtils = require('../services/date_utils');
 | 
			
		||||
class ApiToken extends Entity {
 | 
			
		||||
    static get tableName() { return "api_tokens"; }
 | 
			
		||||
    static get primaryKeyName() { return "apiTokenId"; }
 | 
			
		||||
    static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@ const sql = require('../services/sql');
 | 
			
		||||
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"]; }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
@@ -25,7 +27,11 @@ class Branch extends Entity {
 | 
			
		||||
            this.isDeleted = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.dateModified = dateUtils.nowDate()
 | 
			
		||||
        if (!this.dateCreated) {
 | 
			
		||||
            this.dateCreated = dateUtils.nowDate();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.dateModified = dateUtils.nowDate();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const utils = require('../services/utils');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
 | 
			
		||||
class Entity {
 | 
			
		||||
    constructor(row = {}) {
 | 
			
		||||
@@ -14,10 +13,18 @@ class Entity {
 | 
			
		||||
        if (!this[this.constructor.primaryKeyName]) {
 | 
			
		||||
            this[this.constructor.primaryKeyName] = utils.newEntityId();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let contentToHash = "";
 | 
			
		||||
 | 
			
		||||
        for (const propertyName of this.constructor.hashedProperties) {
 | 
			
		||||
            contentToHash += "|" + this[propertyName];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this["hash"] = utils.hash(contentToHash).substr(0, 10);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async save() {
 | 
			
		||||
        await repository.updateEntity(this);
 | 
			
		||||
        await require('../services/repository').updateEntity(this);
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,10 @@ 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 RecentNote = require('../entities/recent_note');
 | 
			
		||||
const ApiToken = require('../entities/api_token');
 | 
			
		||||
const Option = require('../entities/option');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
 | 
			
		||||
function createEntityFromRow(row) {
 | 
			
		||||
@@ -14,6 +16,9 @@ function createEntityFromRow(row) {
 | 
			
		||||
    if (row.labelId) {
 | 
			
		||||
        entity = new Label(row);
 | 
			
		||||
    }
 | 
			
		||||
    else if (row.relationId) {
 | 
			
		||||
        entity = new Relation(row);
 | 
			
		||||
    }
 | 
			
		||||
    else if (row.noteRevisionId) {
 | 
			
		||||
        entity = new NoteRevision(row);
 | 
			
		||||
    }
 | 
			
		||||
@@ -35,6 +40,9 @@ function createEntityFromRow(row) {
 | 
			
		||||
    else if (row.noteId) {
 | 
			
		||||
        entity = new Note(row);
 | 
			
		||||
    }
 | 
			
		||||
    else if (row.name) {
 | 
			
		||||
        entity = new Option(row);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        throw new Error('Unknown entity type for row: ' + JSON.stringify(row));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ 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"]; }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ 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]);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,21 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const protected_session = require('../services/protected_session');
 | 
			
		||||
const protectedSessionService = require('../services/protected_session');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
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"]; }
 | 
			
		||||
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        super(row);
 | 
			
		||||
 | 
			
		||||
        // check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
 | 
			
		||||
        if (this.isProtected && this.noteId) {
 | 
			
		||||
            protected_session.decryptNote(this);
 | 
			
		||||
            protectedSessionService.decryptNote(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.setContent(this.content);
 | 
			
		||||
@@ -39,7 +40,7 @@ class Note extends Entity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isHtml() {
 | 
			
		||||
        return (this.type === "code" || this.type === "file") && this.mime === "text/html";
 | 
			
		||||
        return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getScriptEnv() {
 | 
			
		||||
@@ -146,7 +147,7 @@ class Note extends Entity {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.isProtected) {
 | 
			
		||||
            protected_session.encryptNote(this);
 | 
			
		||||
            protectedSessionService.encryptNote(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.isDeleted) {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +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"]; }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const protected_session = require('../services/protected_session');
 | 
			
		||||
const utils = require('../services/utils');
 | 
			
		||||
const protectedSessionService = require('../services/protected_session');
 | 
			
		||||
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"]; }
 | 
			
		||||
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        super(row);
 | 
			
		||||
 | 
			
		||||
        if (this.isProtected) {
 | 
			
		||||
            protected_session.decryptNoteRevision(this);
 | 
			
		||||
            protectedSessionService.decryptNoteRevision(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -25,7 +25,7 @@ class NoteRevision extends Entity {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        if (this.isProtected) {
 | 
			
		||||
            protected_session.encryptNoteRevision(this);
 | 
			
		||||
            protectedSessionService.encryptNoteRevision(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								src/entities/option.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const dateUtils = require('../services/date_utils');
 | 
			
		||||
 | 
			
		||||
class Option extends Entity {
 | 
			
		||||
    static get tableName() { return "options"; }
 | 
			
		||||
    static get primaryKeyName() { return "name"; }
 | 
			
		||||
    static get hashedProperties() { return ["name", "value"]; }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        this.dateModified = dateUtils.nowDate();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = Option;
 | 
			
		||||
@@ -1,10 +1,24 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const dateUtils = require('../services/date_utils');
 | 
			
		||||
 | 
			
		||||
class RecentNote extends Entity {
 | 
			
		||||
    static get tableName() { return "recent_notes"; }
 | 
			
		||||
    static get primaryKeyName() { return "branchId"; }
 | 
			
		||||
    static get hashedProperties() { return ["branchId", "notePath", "dateCreated", "isDeleted"]; }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        if (!this.isDeleted) {
 | 
			
		||||
            this.isDeleted = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.dateCreated) {
 | 
			
		||||
            this.dateCreated = dateUtils.nowDate();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = RecentNote;
 | 
			
		||||
							
								
								
									
										44
									
								
								src/entities/relation.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,44 @@
 | 
			
		||||
"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;
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/back-24.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 511 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/clock-16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 381 B  | 
| 
		 Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 245 B  | 
| 
		 Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 339 B  | 
| 
		 Before Width: | Height: | Size: 463 B After Width: | Height: | Size: 463 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/edit-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 312 B  | 
| 
		 Before Width: | Height: | Size: 288 B After Width: | Height: | Size: 288 B  | 
| 
		 Before Width: | Height: | Size: 284 B After Width: | Height: | Size: 284 B  | 
| 
		 Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/forward-24.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 511 B  | 
| 
		 Before Width: | Height: | Size: 155 B After Width: | Height: | Size: 155 B  | 
| 
		 Before Width: | Height: | Size: 323 B  | 
| 
		 Before Width: | Height: | Size: 358 B After Width: | Height: | Size: 358 B  | 
| 
		 Before Width: | Height: | Size: 252 B After Width: | Height: | Size: 252 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/play-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 288 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/save-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 388 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/search-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 431 B  | 
| 
		 Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 419 B  | 
| 
		 Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 354 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/shield-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 388 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/shield-off-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 462 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/tree-root-16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 240 B  | 
| 
		 Before Width: | Height: | Size: 337 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/x-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 259 B  | 
							
								
								
									
										1
									
								
								src/public/images/trilium.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M63.966,45.043c0.008-0.009,0.021-0.021,0.027-0.029c0.938-1.156-0.823-13.453-5.063-20.125  c-1.389-2.186-2.239-3.423-3.219-4.719c-3.907-5.166-6-6.125-6-6.125S35.732,24.78,36.149,44.315  c-1.754,0.065-11.218,7.528-14.826,14.388c-1.206,2.291-1.856,3.645-2.493,5.141c-2.539,5.957-2.33,8.25-2.33,8.25  s16.271,6.79,33.014-3.294c0.007,0.021,0.013,0.046,0.02,0.063c0.537,1.389,12.08,5.979,19.976,5.621  c2.587-0.116,4.084-0.238,5.696-0.444c6.424-0.818,8.298-2.157,8.298-2.157S81.144,54.396,63.966,45.043z M50.787,65.343  c1.059-1.183,4.648-5.853,0.995-11.315c-0.253-0.377-0.496-0.236-0.496-0.236s0.063,10.822-5.162,12.359  c-5.225,1.537-13.886,4.4-20.427,0.455C25,66.186,26.924,53.606,38.544,47.229c0.546,1.599,2.836,6.854,9.292,6.409  c0.453-0.031,0.453-0.313,0.453-0.313s-9.422-5.328-8.156-10.625s3.089-14.236,9.766-17.948c0.714-0.397,10.746,7.593,10.417,20.94  c-1.606-0.319-7.377-1.004-10.226,4.864c-0.198,0.409,0.046,0.549,0.046,0.549s9.31-5.521,13.275-1.789  c3.965,3.733,10.813,9.763,10.71,17.4C74.111,67.533,62.197,72.258,50.787,65.343z M35.613,35.145c0,0-0.991,3.241-0.603,7.524  l-13.393-7.524C21.618,35.145,27.838,30.931,35.613,35.145z M21.193,36.03l13.344,7.612c-3.872,1.872-6.142,4.388-6.142,4.388  C20.78,43.531,21.193,36.03,21.193,36.03z M72.287,49.064c0,0-2.321-2.471-6.23-4.263l13.187-7.881  C79.243,36.92,79.808,44.413,72.287,49.064z M78.687,36.113l-13.237,7.794c0.3-4.291-0.754-7.511-0.754-7.511  C72.383,32.025,78.687,36.113,78.687,36.113z M42.076,73.778c0,0,3.309-0.737,6.845-3.185l0.056,15.361  C48.977,85.955,42.244,82.621,42.076,73.778z M49.956,85.888L50,70.526c3.539,2.445,6.846,3.181,6.846,3.181  C56.686,82.551,49.956,85.888,49.956,85.888z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.8 KiB  | 
@@ -2,7 +2,8 @@ import cloningService from '../services/cloning.js';
 | 
			
		||||
import linkService from '../services/link.js';
 | 
			
		||||
import noteDetailService from '../services/note_detail.js';
 | 
			
		||||
import treeUtils from '../services/tree_utils.js';
 | 
			
		||||
import autocompleteService from '../services/autocomplete.js';
 | 
			
		||||
import server from "../services/server.js";
 | 
			
		||||
import noteDetailText from "../services/note_detail_text.js";
 | 
			
		||||
 | 
			
		||||
const $dialog = $("#add-link-dialog");
 | 
			
		||||
const $form = $("#add-link-form");
 | 
			
		||||
@@ -11,8 +12,10 @@ const $linkTitle = $("#link-title");
 | 
			
		||||
const $clonePrefix = $("#clone-prefix");
 | 
			
		||||
const $linkTitleFormGroup = $("#add-link-title-form-group");
 | 
			
		||||
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");
 | 
			
		||||
 | 
			
		||||
function setLinkType(linkType) {
 | 
			
		||||
    $linkTypes.each(function () {
 | 
			
		||||
@@ -45,18 +48,39 @@ async function showDialog() {
 | 
			
		||||
    $clonePrefix.val('');
 | 
			
		||||
    $linkTitle.val('');
 | 
			
		||||
 | 
			
		||||
    function setDefaultLinkTitle(noteId) {
 | 
			
		||||
        const noteTitle = treeUtils.getNoteTitle(noteId);
 | 
			
		||||
    async function setDefaultLinkTitle(noteId) {
 | 
			
		||||
        const noteTitle = await treeUtils.getNoteTitle(noteId);
 | 
			
		||||
 | 
			
		||||
        $linkTitle.val(noteTitle);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $autoComplete.autocomplete({
 | 
			
		||||
        source: await autocompleteService.getAutocompleteItems(),
 | 
			
		||||
    await $autoComplete.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,
 | 
			
		||||
        change: () => {
 | 
			
		||||
            const val = $autoComplete.val();
 | 
			
		||||
            const notePath = linkService.getNodePathFromLabel(val);
 | 
			
		||||
        change: async (event, ui) => {
 | 
			
		||||
            if (!ui.item) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const notePath = linkService.getNotePathFromLabel(ui.item.value);
 | 
			
		||||
 | 
			
		||||
            if (!notePath) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
@@ -64,24 +88,33 @@ async function showDialog() {
 | 
			
		||||
            const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
			
		||||
 | 
			
		||||
            if (noteId) {
 | 
			
		||||
                setDefaultLinkTitle(noteId);
 | 
			
		||||
                await setDefaultLinkTitle(noteId);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        select: function (event, ui) {
 | 
			
		||||
            if (ui.item.value === 'No results') {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        // this is called when user goes through autocomplete list with keyboard
 | 
			
		||||
        // at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
 | 
			
		||||
        focus: (event, ui) => {
 | 
			
		||||
            const notePath = linkService.getNodePathFromLabel(ui.item.value);
 | 
			
		||||
        focus: async (event, ui) => {
 | 
			
		||||
            const notePath = linkService.getNotePathFromLabel(ui.item.value);
 | 
			
		||||
            const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
			
		||||
 | 
			
		||||
            setDefaultLinkTitle(noteId);
 | 
			
		||||
            await setDefaultLinkTitle(noteId);
 | 
			
		||||
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    showRecentNotes();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$form.submit(() => {
 | 
			
		||||
    const value = $autoComplete.val();
 | 
			
		||||
 | 
			
		||||
    const notePath = linkService.getNodePathFromLabel(value);
 | 
			
		||||
    const notePath = linkService.getNotePathFromLabel(value);
 | 
			
		||||
    const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
			
		||||
 | 
			
		||||
    if (notePath) {
 | 
			
		||||
@@ -92,7 +125,16 @@ $form.submit(() => {
 | 
			
		||||
 | 
			
		||||
            $dialog.dialog("close");
 | 
			
		||||
 | 
			
		||||
            linkService.addLinkToEditor(linkTitle, '#' + notePath);
 | 
			
		||||
            const linkHref = '#' + notePath;
 | 
			
		||||
 | 
			
		||||
            if (hasSelection()) {
 | 
			
		||||
                const editor = noteDetailText.getEditor();
 | 
			
		||||
 | 
			
		||||
                editor.execute('link', linkHref);
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                linkService.addLinkToEditor(linkTitle, linkHref);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (linkType === 'selected-to-current') {
 | 
			
		||||
            const prefix = $clonePrefix.val();
 | 
			
		||||
@@ -113,21 +155,31 @@ $form.submit(() => {
 | 
			
		||||
    return false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// returns true if user selected some text, false if there's no selection
 | 
			
		||||
function hasSelection() {
 | 
			
		||||
    const model = noteDetailText.getEditor().model;
 | 
			
		||||
    const selection = model.document.selection;
 | 
			
		||||
 | 
			
		||||
    return !selection.isCollapsed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function linkTypeChanged() {
 | 
			
		||||
    const value = $linkTypes.filter(":checked").val();
 | 
			
		||||
 | 
			
		||||
    if (value === 'html') {
 | 
			
		||||
        $linkTitleFormGroup.show();
 | 
			
		||||
        $prefixFormGroup.hide();
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        $linkTitleFormGroup.hide();
 | 
			
		||||
        $prefixFormGroup.show();
 | 
			
		||||
    }
 | 
			
		||||
    $linkTitleFormGroup.toggle(!hasSelection() && value === 'html');
 | 
			
		||||
    $prefixFormGroup.toggle(!hasSelection() && value !== 'html');
 | 
			
		||||
 | 
			
		||||
    $linkTypeDiv.toggle(!hasSelection());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showRecentNotes() {
 | 
			
		||||
    $autoComplete.autocomplete("search", "");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$linkTypes.change(linkTypeChanged);
 | 
			
		||||
 | 
			
		||||
$showRecentNotesButton.click(showRecentNotes);
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog
 | 
			
		||||
};
 | 
			
		||||
@@ -25,7 +25,7 @@ async function showDialog() {
 | 
			
		||||
 | 
			
		||||
    $treePrefixInput.val(branch.prefix).focus();
 | 
			
		||||
 | 
			
		||||
    const noteTitle = treeUtils.getNoteTitle(currentNode.data.noteId);
 | 
			
		||||
    const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId);
 | 
			
		||||
 | 
			
		||||
    $noteTitle.html(noteTitle);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,10 +19,10 @@ async function showDialog() {
 | 
			
		||||
    $list.html('');
 | 
			
		||||
 | 
			
		||||
    for (const event of result) {
 | 
			
		||||
        const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded));
 | 
			
		||||
        const dateTime = utils.formatDateTime(utils.parseDate(event.dateCreated));
 | 
			
		||||
 | 
			
		||||
        if (event.noteId) {
 | 
			
		||||
            const noteLink = linkService.createNoteLink(event.noteId).prop('outerHTML');
 | 
			
		||||
            const noteLink = await linkService.createNoteLink(event.noteId).prop('outerHTML');
 | 
			
		||||
 | 
			
		||||
            event.comment = event.comment.replace('<note>', noteLink);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import treeService from '../services/tree.js';
 | 
			
		||||
import linkService from '../services/link.js';
 | 
			
		||||
import utils from '../services/utils.js';
 | 
			
		||||
import autocompleteService from '../services/autocomplete.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
import searchNotesService from '../services/search_notes.js';
 | 
			
		||||
 | 
			
		||||
const $dialog = $("#jump-to-note-dialog");
 | 
			
		||||
const $autoComplete = $("#jump-to-note-autocomplete");
 | 
			
		||||
const $form = $("#jump-to-note-form");
 | 
			
		||||
const $showInFullTextButton = $("#show-in-full-text-button");
 | 
			
		||||
const $showRecentNotesButton = $("#jump-to-note-show-recent-notes");
 | 
			
		||||
 | 
			
		||||
async function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
@@ -14,35 +14,66 @@ async function showDialog() {
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog({
 | 
			
		||||
        modal: true,
 | 
			
		||||
        width: 800
 | 
			
		||||
        width: 800,
 | 
			
		||||
        position: { my: "center top+100", at: "top", of: window }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await $autoComplete.autocomplete({
 | 
			
		||||
        source: await utils.stopWatch("building autocomplete", autocompleteService.getAutocompleteItems),
 | 
			
		||||
        minLength: 1
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
        source: async function(request, response) {
 | 
			
		||||
            const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
 | 
			
		||||
 | 
			
		||||
function getSelectedNotePath() {
 | 
			
		||||
    const val = $autoComplete.val();
 | 
			
		||||
    return linkService.getNodePathFromLabel(val);
 | 
			
		||||
}
 | 
			
		||||
            if (result.length > 0) {
 | 
			
		||||
                response(result);
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                response([{
 | 
			
		||||
                    label: "No results",
 | 
			
		||||
                    value: "No results"
 | 
			
		||||
                }]);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        focus: function(event, ui) {
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
        },
 | 
			
		||||
        minLength: 0,
 | 
			
		||||
        autoFocus: true,
 | 
			
		||||
        select: function (event, ui) {
 | 
			
		||||
            if (ui.item.value === 'No results') {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
function goToNote() {
 | 
			
		||||
    const notePath = getSelectedNotePath();
 | 
			
		||||
 | 
			
		||||
    if (notePath) {
 | 
			
		||||
        treeService.activateNode(notePath);
 | 
			
		||||
            treeService.activateNode(ui.item.value);
 | 
			
		||||
 | 
			
		||||
            $dialog.dialog('close');
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    showRecentNotes();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$form.submit(() => {
 | 
			
		||||
    goToNote();
 | 
			
		||||
function showInFullText(e) {
 | 
			
		||||
    // stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
});
 | 
			
		||||
    const searchText = $autoComplete.val();
 | 
			
		||||
 | 
			
		||||
    searchNotesService.resetSearch();
 | 
			
		||||
    searchNotesService.showSearch();
 | 
			
		||||
    searchNotesService.doSearch(searchText);
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog('close');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showRecentNotes() {
 | 
			
		||||
    $autoComplete.autocomplete("search", "");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$showInFullTextButton.click(showInFullText);
 | 
			
		||||
 | 
			
		||||
$showRecentNotesButton.click(showRecentNotes);
 | 
			
		||||
 | 
			
		||||
$dialog.bind('keydown', 'ctrl+return', showInFullText);
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import noteDetailService from '../services/note_detail.js';
 | 
			
		||||
import utils from '../services/utils.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
import infoService from "../services/info.js";
 | 
			
		||||
 | 
			
		||||
@@ -168,9 +167,9 @@ async function showDialog() {
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ko.applyBindings(labelsModel, document.getElementById('labels-dialog'));
 | 
			
		||||
ko.applyBindings(labelsModel, $dialog[0]);
 | 
			
		||||
 | 
			
		||||
$(document).on('focus', '.label-name', function (e) {
 | 
			
		||||
$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
 | 
			
		||||
@@ -188,7 +187,7 @@ $(document).on('focus', '.label-name', function (e) {
 | 
			
		||||
    $(this).autocomplete("search", $(this).val());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).on('focus', '.label-value', async function (e) {
 | 
			
		||||
$dialog.on('focus', '.label-value', async function (e) {
 | 
			
		||||
    if (!$(this).hasClass("ui-autocomplete-input")) {
 | 
			
		||||
        const labelName = $(this).parent().parent().find('.label-name').val();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ function formatNode(node, level) {
 | 
			
		||||
    const indentAfter  = new Array(level - 1).join('  ');
 | 
			
		||||
    let textNode;
 | 
			
		||||
 | 
			
		||||
    for (const i = 0; i < node.children.length; i++) {
 | 
			
		||||
    for (let i = 0; i < node.children.length; i++) {
 | 
			
		||||
        textNode = document.createTextNode('\n' + indentBefore);
 | 
			
		||||
        node.insertBefore(textNode, node.children[i]);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
import protectedSessionHolder from '../services/protected_session_holder.js';
 | 
			
		||||
import utils from '../services/utils.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
import infoService from "../services/info.js";
 | 
			
		||||
import zoomService from "../services/zoom.js";
 | 
			
		||||
import utils from "../services/utils.js";
 | 
			
		||||
 | 
			
		||||
const $dialog = $("#options-dialog");
 | 
			
		||||
const $tabs = $("#options-tabs");
 | 
			
		||||
@@ -33,8 +34,8 @@ async function showDialog() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function saveOptions(optionName, optionValue) {
 | 
			
		||||
    await server.put('options/' + encodeURIComponent(optionName) + '/' + encodeURIComponent(optionValue));
 | 
			
		||||
async function saveOptions(options) {
 | 
			
		||||
    await server.put('options', options);
 | 
			
		||||
 | 
			
		||||
    infoService.showMessage("Options change have been saved.");
 | 
			
		||||
}
 | 
			
		||||
@@ -44,6 +45,41 @@ export default {
 | 
			
		||||
    saveOptions
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
addTabHandler((function() {
 | 
			
		||||
    const $themeSelect = $("#theme-select");
 | 
			
		||||
    const $zoomFactorSelect = $("#zoom-factor-select");
 | 
			
		||||
    const $html = $("html");
 | 
			
		||||
 | 
			
		||||
    function optionsLoaded(options) {
 | 
			
		||||
        $themeSelect.val(options.theme);
 | 
			
		||||
 | 
			
		||||
        if (utils.isElectron()) {
 | 
			
		||||
            $zoomFactorSelect.val(options.zoomFactor);
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            $zoomFactorSelect.prop('disabled', true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $themeSelect.change(function() {
 | 
			
		||||
        const newTheme = $(this).val();
 | 
			
		||||
 | 
			
		||||
        $html.attr("class", "theme-" + newTheme);
 | 
			
		||||
 | 
			
		||||
        server.put('options/theme/' + newTheme);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $zoomFactorSelect.change(function() {
 | 
			
		||||
        const newZoomFactor = $(this).val();
 | 
			
		||||
 | 
			
		||||
        zoomService.setZoomFactorAndSave(newZoomFactor);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        optionsLoaded
 | 
			
		||||
    };
 | 
			
		||||
})());
 | 
			
		||||
 | 
			
		||||
addTabHandler((function() {
 | 
			
		||||
    const $form = $("#change-password-form");
 | 
			
		||||
    const $oldPassword = $("#old-password");
 | 
			
		||||
@@ -93,16 +129,15 @@ addTabHandler((function() {
 | 
			
		||||
addTabHandler((function() {
 | 
			
		||||
    const $form = $("#protected-session-timeout-form");
 | 
			
		||||
    const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
 | 
			
		||||
    const optionName = 'protectedSessionTimeout';
 | 
			
		||||
 | 
			
		||||
    function optionsLoaded(options) {
 | 
			
		||||
        $protectedSessionTimeout.val(options[optionName]);
 | 
			
		||||
        $protectedSessionTimeout.val(options['protectedSessionTimeout']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $form.submit(() => {
 | 
			
		||||
        const protectedSessionTimeout = $protectedSessionTimeout.val();
 | 
			
		||||
 | 
			
		||||
        saveOptions(optionName, protectedSessionTimeout).then(() => {
 | 
			
		||||
        saveOptions({ 'protectedSessionTimeout': protectedSessionTimeout }).then(() => {
 | 
			
		||||
            protectedSessionHolder.setProtectedSessionTimeout(protectedSessionTimeout);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -117,14 +152,13 @@ addTabHandler((function() {
 | 
			
		||||
addTabHandler((function () {
 | 
			
		||||
    const $form = $("#note-revision-snapshot-time-interval-form");
 | 
			
		||||
    const $timeInterval = $("#note-revision-snapshot-time-interval-in-seconds");
 | 
			
		||||
    const optionName = 'noteRevisionSnapshotTimeInterval';
 | 
			
		||||
 | 
			
		||||
    function optionsLoaded(options) {
 | 
			
		||||
        $timeInterval.val(options[optionName]);
 | 
			
		||||
        $timeInterval.val(options['noteRevisionSnapshotTimeInterval']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $form.submit(() => {
 | 
			
		||||
        saveOptions(optionName, $timeInterval.val());
 | 
			
		||||
        saveOptions({ 'noteRevisionSnapshotTimeInterval': $timeInterval.val() });
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    });
 | 
			
		||||
@@ -137,6 +171,7 @@ addTabHandler((function () {
 | 
			
		||||
addTabHandler((async function () {
 | 
			
		||||
    const $appVersion = $("#app-version");
 | 
			
		||||
    const $dbVersion = $("#db-version");
 | 
			
		||||
    const $syncVersion = $("#sync-version");
 | 
			
		||||
    const $buildDate = $("#build-date");
 | 
			
		||||
    const $buildRevision = $("#build-revision");
 | 
			
		||||
 | 
			
		||||
@@ -144,6 +179,7 @@ addTabHandler((async function () {
 | 
			
		||||
 | 
			
		||||
    $appVersion.html(appInfo.appVersion);
 | 
			
		||||
    $dbVersion.html(appInfo.dbVersion);
 | 
			
		||||
    $syncVersion.html(appInfo.syncVersion);
 | 
			
		||||
    $buildDate.html(appInfo.buildDate);
 | 
			
		||||
    $buildRevision.html(appInfo.buildRevision);
 | 
			
		||||
    $buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
 | 
			
		||||
@@ -151,6 +187,57 @@ addTabHandler((async function () {
 | 
			
		||||
    return {};
 | 
			
		||||
})());
 | 
			
		||||
 | 
			
		||||
addTabHandler((function() {
 | 
			
		||||
    const $form = $("#sync-setup-form");
 | 
			
		||||
    const $syncServerHost = $("#sync-server-host");
 | 
			
		||||
    const $syncServerTimeout = $("#sync-server-timeout");
 | 
			
		||||
    const $syncProxy = $("#sync-proxy");
 | 
			
		||||
    const $testSyncButton = $("#test-sync-button");
 | 
			
		||||
    const $syncToServerButton = $("#sync-to-server-button");
 | 
			
		||||
 | 
			
		||||
    function optionsLoaded(options) {
 | 
			
		||||
        $syncServerHost.val(options['syncServerHost']);
 | 
			
		||||
        $syncServerTimeout.val(options['syncServerTimeout']);
 | 
			
		||||
        $syncProxy.val(options['syncProxy']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $form.submit(() => {
 | 
			
		||||
        saveOptions({
 | 
			
		||||
            'syncServerHost': $syncServerHost.val(),
 | 
			
		||||
            'syncServerTimeout': $syncServerTimeout.val(),
 | 
			
		||||
            'syncProxy': $syncProxy.val()
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $testSyncButton.click(async () => {
 | 
			
		||||
        const result = await server.post('sync/test');
 | 
			
		||||
 | 
			
		||||
        if (result.connection === "Success") {
 | 
			
		||||
            infoService.showMessage("Sync server handshake has been successful");
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            infoService.showError("Sync server handshake failed, error: " + result.error);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $syncToServerButton.click(async () => {
 | 
			
		||||
        const resp = await server.post("setup/sync-to-server");
 | 
			
		||||
 | 
			
		||||
        if (resp.success) {
 | 
			
		||||
            infoService.showMessage("Sync has been established to the server instance. It will take some time to finish.");
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            infoService.showError('Sync setup failed: ' + resp.error);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        optionsLoaded
 | 
			
		||||
    };
 | 
			
		||||
})());
 | 
			
		||||
 | 
			
		||||
addTabHandler((async function () {
 | 
			
		||||
    const $forceFullSyncButton = $("#force-full-sync-button");
 | 
			
		||||
    const $fillSyncRowsButton = $("#fill-sync-rows-button");
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,7 @@ async function showDialog() {
 | 
			
		||||
                noteLink = change.current_title;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                noteLink = linkService.createNoteLink(change.noteId, change.title);
 | 
			
		||||
                noteLink = await linkService.createNoteLink(change.noteId, change.title);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            changesListEl.append($('<li>')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,113 +0,0 @@
 | 
			
		||||
import treeService from '../services/tree.js';
 | 
			
		||||
import messagingService from '../services/messaging.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
import utils from "../services/utils.js";
 | 
			
		||||
import treeUtils from "../services/tree_utils.js";
 | 
			
		||||
 | 
			
		||||
const $dialog = $("#recent-notes-dialog");
 | 
			
		||||
const $searchInput = $('#recent-notes-search-input');
 | 
			
		||||
 | 
			
		||||
// list of recent note paths
 | 
			
		||||
let list = [];
 | 
			
		||||
 | 
			
		||||
async function reload() {
 | 
			
		||||
    const result = await server.get('recent-notes');
 | 
			
		||||
 | 
			
		||||
    list = result.map(r => r.notePath);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addRecentNote(branchId, notePath) {
 | 
			
		||||
    setTimeout(async () => {
 | 
			
		||||
        // we include the note into recent list only if the user stayed on the note at least 5 seconds
 | 
			
		||||
        if (notePath && notePath === treeService.getCurrentNotePath()) {
 | 
			
		||||
            const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
 | 
			
		||||
 | 
			
		||||
            list = result.map(r => r.notePath);
 | 
			
		||||
        }
 | 
			
		||||
    }, 1500);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getNoteTitle(notePath) {
 | 
			
		||||
    let noteTitle;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        noteTitle = await treeUtils.getNotePathTitle(notePath);
 | 
			
		||||
    }
 | 
			
		||||
    catch (e) {
 | 
			
		||||
        noteTitle = "[error - can't find note title]";
 | 
			
		||||
 | 
			
		||||
        messagingService.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return noteTitle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog({
 | 
			
		||||
        modal: true,
 | 
			
		||||
        width: 800,
 | 
			
		||||
        height: 100,
 | 
			
		||||
        position: { my: "center top+100", at: "top", of: window }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $searchInput.val('');
 | 
			
		||||
 | 
			
		||||
    // remove the current note
 | 
			
		||||
    const recNotes = list.filter(note => note !== treeService.getCurrentNotePath());
 | 
			
		||||
    const items = [];
 | 
			
		||||
 | 
			
		||||
    for (const notePath of recNotes) {
 | 
			
		||||
        items.push({
 | 
			
		||||
            label: await getNoteTitle(notePath),
 | 
			
		||||
            value: notePath
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $searchInput.autocomplete({
 | 
			
		||||
        source: items,
 | 
			
		||||
        minLength: 0,
 | 
			
		||||
        autoFocus: true,
 | 
			
		||||
        select: function (event, ui) {
 | 
			
		||||
            treeService.activateNode(ui.item.value);
 | 
			
		||||
 | 
			
		||||
            $searchInput.autocomplete('destroy');
 | 
			
		||||
            $dialog.dialog('close');
 | 
			
		||||
        },
 | 
			
		||||
        focus: function (event, ui) {
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
        },
 | 
			
		||||
        close: function (event, ui) {
 | 
			
		||||
            if (event.keyCode === 27) { // escape closes dialog
 | 
			
		||||
                $searchInput.autocomplete('destroy');
 | 
			
		||||
                $dialog.dialog('close');
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                // keep autocomplete open
 | 
			
		||||
                // we're kind of abusing autocomplete to work in a way which it's not designed for
 | 
			
		||||
                $searchInput.autocomplete("search", "");
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        create: () => $searchInput.autocomplete("search", ""),
 | 
			
		||||
        classes: {
 | 
			
		||||
            "ui-autocomplete": "recent-notes-autocomplete"
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
setTimeout(reload, 100);
 | 
			
		||||
 | 
			
		||||
messagingService.subscribeToMessages(syncData => {
 | 
			
		||||
    if (syncData.some(sync => sync.entityName === 'recent_notes')) {
 | 
			
		||||
        console.log(utils.now(), "Reloading recent notes because of background changes");
 | 
			
		||||
 | 
			
		||||
        reload();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog,
 | 
			
		||||
    addRecentNote,
 | 
			
		||||
    reload
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										250
									
								
								src/public/javascripts/dialogs/relations.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,250 @@
 | 
			
		||||
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
 | 
			
		||||
};
 | 
			
		||||
@@ -6,7 +6,7 @@ class NoteShort {
 | 
			
		||||
        this.isProtected = row.isProtected;
 | 
			
		||||
        this.type = row.type;
 | 
			
		||||
        this.mime = row.mime;
 | 
			
		||||
        this.hideInAutocomplete = row.hideInAutocomplete;
 | 
			
		||||
        this.archived = row.archived;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isJson() {
 | 
			
		||||
@@ -14,13 +14,15 @@ class NoteShort {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getBranches() {
 | 
			
		||||
        const branches = [];
 | 
			
		||||
        const branchIds = this.treeCache.parents[this.noteId].map(
 | 
			
		||||
            parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId));
 | 
			
		||||
 | 
			
		||||
        for (const parent of this.treeCache.parents[this.noteId]) {
 | 
			
		||||
            branches.push(await this.treeCache.getBranchByChildParent(this.noteId, parent.noteId));
 | 
			
		||||
        return this.treeCache.getBranches(branchIds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        return branches;
 | 
			
		||||
    hasChildren() {
 | 
			
		||||
        return this.treeCache.children[this.noteId]
 | 
			
		||||
            && this.treeCache.children[this.noteId].length > 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildBranches() {
 | 
			
		||||
@@ -28,23 +30,28 @@ class NoteShort {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const branches = [];
 | 
			
		||||
        const branchIds = this.treeCache.children[this.noteId].map(
 | 
			
		||||
            childNoteId => this.treeCache.getBranchIdByChildParent(childNoteId, this.noteId));
 | 
			
		||||
 | 
			
		||||
        for (const child of this.treeCache.children[this.noteId]) {
 | 
			
		||||
            branches.push(await this.treeCache.getBranchByChildParent(child.noteId, this.noteId));
 | 
			
		||||
        return await this.treeCache.getBranches(branchIds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        return branches;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getParentNotes() {
 | 
			
		||||
    getParentNoteIds() {
 | 
			
		||||
        return this.treeCache.parents[this.noteId] || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildNotes() {
 | 
			
		||||
    async getParentNotes() {
 | 
			
		||||
        return await this.treeCache.getNotes(this.getParentNoteIds());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChildNoteIds() {
 | 
			
		||||
        return this.treeCache.children[this.noteId] || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildNotes() {
 | 
			
		||||
        return await this.treeCache.getNotes(this.getChildNoteIds());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get toString() {
 | 
			
		||||
        return `Note(noteId=${this.noteId}, title=${this.title})`;
 | 
			
		||||
    }
 | 
			
		||||
@@ -52,7 +59,7 @@ class NoteShort {
 | 
			
		||||
    get dto() {
 | 
			
		||||
        const dto = Object.assign({}, this);
 | 
			
		||||
        delete dto.treeCache;
 | 
			
		||||
        delete dto.hideInAutocomplete;
 | 
			
		||||
        delete dto.archived;
 | 
			
		||||
 | 
			
		||||
        return dto;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
import server from './services/server.js';
 | 
			
		||||
 | 
			
		||||
$(document).ready(async () => {
 | 
			
		||||
    const {appDbVersion, dbVersion} = await server.get('migration');
 | 
			
		||||
 | 
			
		||||
    console.log("HI", {appDbVersion, dbVersion});
 | 
			
		||||
 | 
			
		||||
    if (appDbVersion === dbVersion) {
 | 
			
		||||
        $("#up-to-date").show();
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        $("#need-to-migrate").show();
 | 
			
		||||
 | 
			
		||||
        $("#app-db-version").html(appDbVersion);
 | 
			
		||||
        $("#db-version").html(dbVersion);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#run-migration").click(async () => {
 | 
			
		||||
    $("#run-migration").prop("disabled", true);
 | 
			
		||||
 | 
			
		||||
    $("#migration-result").show();
 | 
			
		||||
 | 
			
		||||
    const result = await server.post('migration');
 | 
			
		||||
 | 
			
		||||
    for (const migration of result.migrations) {
 | 
			
		||||
        const row = $('<tr>')
 | 
			
		||||
            .append($('<td>').html(migration.dbVersion))
 | 
			
		||||
            .append($('<td>').html(migration.name))
 | 
			
		||||
            .append($('<td>').html(migration.success ? 'Yes' : 'No'))
 | 
			
		||||
            .append($('<td>').html(migration.success ? 'N/A' : migration.error));
 | 
			
		||||
 | 
			
		||||
        if (!migration.success) {
 | 
			
		||||
            row.addClass("danger");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $("#migration-table").append(row);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// copy of this shortcut to be able to debug migration problems
 | 
			
		||||
$(document).bind('keydown', 'ctrl+shift+i', () => {
 | 
			
		||||
    require('electron').remote.getCurrentWindow().toggleDevTools();
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
});
 | 
			
		||||
@@ -1,104 +0,0 @@
 | 
			
		||||
import treeCache from "./tree_cache.js";
 | 
			
		||||
import treeUtils from "./tree_utils.js";
 | 
			
		||||
import protectedSessionHolder from './protected_session_holder.js';
 | 
			
		||||
 | 
			
		||||
async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
 | 
			
		||||
    if (!parentNoteId) {
 | 
			
		||||
        parentNoteId = 'root';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const parentNote = await treeCache.getNote(parentNoteId);
 | 
			
		||||
    const childNotes = await parentNote.getChildNotes();
 | 
			
		||||
 | 
			
		||||
    if (!childNotes.length) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!notePath) {
 | 
			
		||||
        notePath = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!titlePath) {
 | 
			
		||||
        titlePath = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const autocompleteItems = [];
 | 
			
		||||
 | 
			
		||||
    for (const childNote of childNotes) {
 | 
			
		||||
        if (childNote.hideInAutocomplete) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId;
 | 
			
		||||
        const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId);
 | 
			
		||||
 | 
			
		||||
        if (!childNote.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
			
		||||
            autocompleteItems.push({
 | 
			
		||||
                value: childTitlePath + ' (' + childNotePath + ')',
 | 
			
		||||
                label: childTitlePath
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath);
 | 
			
		||||
 | 
			
		||||
        for (const childItem of childItems) {
 | 
			
		||||
            autocompleteItems.push(childItem);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (parentNoteId === 'root') {
 | 
			
		||||
        console.log(`Generated ${autocompleteItems.length} autocomplete items`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return autocompleteItems;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
 | 
			
		||||
$.ui.autocomplete.filter = (array, terms) => {
 | 
			
		||||
    if (!terms) {
 | 
			
		||||
        return array;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const startDate = new Date();
 | 
			
		||||
 | 
			
		||||
    const results = [];
 | 
			
		||||
    const tokens = terms.toLowerCase().split(" ");
 | 
			
		||||
 | 
			
		||||
    for (const item of array) {
 | 
			
		||||
        const lcLabel = item.label.toLowerCase();
 | 
			
		||||
 | 
			
		||||
        const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
 | 
			
		||||
        if (!found) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // this is not completely correct and might cause minor problems with note with names containing this " / "
 | 
			
		||||
        const lastSegmentIndex = lcLabel.lastIndexOf(" / ");
 | 
			
		||||
 | 
			
		||||
        if (lastSegmentIndex !== -1) {
 | 
			
		||||
            const lastSegment = lcLabel.substr(lastSegmentIndex + 3);
 | 
			
		||||
 | 
			
		||||
            // at least some token needs to be in the last segment (leaf note), otherwise this
 | 
			
		||||
            // particular note is not that interesting (query is satisfied by parent note)
 | 
			
		||||
            const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1);
 | 
			
		||||
 | 
			
		||||
            if (!foundInLastSegment) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        results.push(item);
 | 
			
		||||
 | 
			
		||||
        if (results.length > 100) {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
 | 
			
		||||
 | 
			
		||||
    return results;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    getAutocompleteItems
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										11
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -4,7 +4,6 @@ import labelsDialog from '../dialogs/labels.js';
 | 
			
		||||
import noteRevisionsDialog from '../dialogs/note_revisions.js';
 | 
			
		||||
import noteSourceDialog from '../dialogs/note_source.js';
 | 
			
		||||
import recentChangesDialog from '../dialogs/recent_changes.js';
 | 
			
		||||
import recentNotesDialog from '../dialogs/recent_notes.js';
 | 
			
		||||
import optionsDialog from '../dialogs/options.js';
 | 
			
		||||
import sqlConsoleDialog from '../dialogs/sql_console.js';
 | 
			
		||||
 | 
			
		||||
@@ -17,7 +16,7 @@ import messagingService from './messaging.js';
 | 
			
		||||
import noteDetailService from './note_detail.js';
 | 
			
		||||
import noteType from './note_type.js';
 | 
			
		||||
import protected_session from './protected_session.js';
 | 
			
		||||
import searchTreeService from './search_tree.js';
 | 
			
		||||
import searchNotesService from './search_notes.js';
 | 
			
		||||
import ScriptApi from './script_api.js';
 | 
			
		||||
import ScriptContext from './script_context.js';
 | 
			
		||||
import sync from './sync.js';
 | 
			
		||||
@@ -35,6 +34,7 @@ import libraryLoader from "./library_loader.js";
 | 
			
		||||
// required for CKEditor image upload plugin
 | 
			
		||||
window.glob.getCurrentNode = treeService.getCurrentNode;
 | 
			
		||||
window.glob.getHeaders = server.getHeaders;
 | 
			
		||||
window.glob.showAddLinkDialog = addLinkDialog.showDialog;
 | 
			
		||||
 | 
			
		||||
// required for ESLint plugin
 | 
			
		||||
window.glob.getCurrentNote = noteDetailService.getCurrentNote;
 | 
			
		||||
@@ -46,7 +46,12 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
 | 
			
		||||
 | 
			
		||||
    let message = "Uncaught error: ";
 | 
			
		||||
 | 
			
		||||
    if (string.indexOf("script error") > -1){
 | 
			
		||||
    if (string.includes("Cannot read property 'defaultView' of undefined")) {
 | 
			
		||||
        // ignore this specific error which is very common but we don't know where it comes from
 | 
			
		||||
        // and it seems to be harmless
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    else if (string.includes("script error")) {
 | 
			
		||||
        message += 'No details available';
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,14 @@
 | 
			
		||||
import ScriptContext from "./script_context.js";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
 | 
			
		||||
async function executeBundle(bundle) {
 | 
			
		||||
    const apiContext = ScriptContext(bundle.note, bundle.allNotes);
 | 
			
		||||
async function getAndExecuteBundle(noteId, targetNote = null) {
 | 
			
		||||
    const bundle = await server.get('script/bundle/' + noteId);
 | 
			
		||||
 | 
			
		||||
    await executeBundle(bundle, targetNote);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function executeBundle(bundle, targetNote) {
 | 
			
		||||
    const apiContext = ScriptContext(bundle.note, bundle.allNotes, targetNote);
 | 
			
		||||
 | 
			
		||||
    return await (function () {
 | 
			
		||||
        return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
 | 
			
		||||
@@ -17,7 +23,17 @@ async function executeStartupBundles() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function executeRelationBundles(note, relationName) {
 | 
			
		||||
    const bundlesToRun = await server.get("script/relation/" + note.noteId + "/" + relationName);
 | 
			
		||||
 | 
			
		||||
    for (const bundle of bundlesToRun) {
 | 
			
		||||
        await executeBundle(bundle, note);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    executeBundle,
 | 
			
		||||
    executeStartupBundles
 | 
			
		||||
    getAndExecuteBundle,
 | 
			
		||||
    executeStartupBundles,
 | 
			
		||||
    executeRelationBundles
 | 
			
		||||
}
 | 
			
		||||
@@ -94,27 +94,34 @@ const contextMenuOptions = {
 | 
			
		||||
        {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
 | 
			
		||||
        {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
 | 
			
		||||
        {title: "----"},
 | 
			
		||||
        {title: "Export branch", cmd: "exportBranch", uiIcon: " ui-icon-arrowthick-1-ne"},
 | 
			
		||||
        {title: "Import into branch", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"},
 | 
			
		||||
        {title: "Export branch", cmd: "exportBranch", uiIcon: " ui-icon-arrowthick-1-ne", children: [
 | 
			
		||||
            {title: "Native Tar", cmd: "exportBranchToTar"},
 | 
			
		||||
            {title: "OPML", cmd: "exportBranchToOpml"}
 | 
			
		||||
        ]},
 | 
			
		||||
        {title: "Import into branch (tar, opml)", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"},
 | 
			
		||||
        {title: "----"},
 | 
			
		||||
        {title: "Collapse branch <kbd>Alt+-</kbd>", cmd: "collapseBranch", uiIcon: "ui-icon-minus"},
 | 
			
		||||
        {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"},
 | 
			
		||||
        {title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
 | 
			
		||||
 | 
			
		||||
    ],
 | 
			
		||||
    beforeOpen: async (event, ui) => {
 | 
			
		||||
        const node = $.ui.fancytree.getNode(ui.target);
 | 
			
		||||
        const branch = await treeCache.getBranch(node.data.branchId);
 | 
			
		||||
        const note = await treeCache.getNote(node.data.noteId);
 | 
			
		||||
        const parentNote = await treeCache.getNote(branch.parentNoteId);
 | 
			
		||||
        const isNotRoot = note.noteId !== 'root';
 | 
			
		||||
 | 
			
		||||
        // Modify menu entries depending on node status
 | 
			
		||||
        $tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && (!parentNote || parentNote.type !== 'search'));
 | 
			
		||||
        $tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "insertNoteHere", !parentNote || parentNote.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "insertNoteHere", isNotRoot && parentNote.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "delete", isNotRoot && parentNote.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "copy", isNotRoot);
 | 
			
		||||
        $tree.contextmenu("enableEntry", "cut", isNotRoot);
 | 
			
		||||
        $tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "importBranch", note.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "editBranchPrefix", isNotRoot && parentNote.type !== 'search');
 | 
			
		||||
 | 
			
		||||
        // Activate node on right-click
 | 
			
		||||
        node.setActive();
 | 
			
		||||
@@ -159,8 +166,11 @@ const contextMenuOptions = {
 | 
			
		||||
        else if (ui.cmd === "delete") {
 | 
			
		||||
            treeChangesService.deleteNodes(treeService.getSelectedNodes(true));
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "exportBranch") {
 | 
			
		||||
            exportService.exportBranch(node.data.noteId);
 | 
			
		||||
        else if (ui.cmd === "exportBranchToTar") {
 | 
			
		||||
            exportService.exportBranch(node.data.noteId, 'tar');
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "exportBranchToOpml") {
 | 
			
		||||
            exportService.exportBranch(node.data.noteId, 'opml');
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "importBranch") {
 | 
			
		||||
            exportService.importBranch(node.data.noteId);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,16 +2,18 @@ import utils from "./utils.js";
 | 
			
		||||
import treeService from "./tree.js";
 | 
			
		||||
import linkService from "./link.js";
 | 
			
		||||
import fileService from "./file.js";
 | 
			
		||||
import zoomService from "./zoom.js";
 | 
			
		||||
import noteRevisionsDialog from "../dialogs/note_revisions.js";
 | 
			
		||||
import optionsDialog from "../dialogs/options.js";
 | 
			
		||||
import addLinkDialog from "../dialogs/add_link.js";
 | 
			
		||||
import recentNotesDialog from "../dialogs/recent_notes.js";
 | 
			
		||||
import jumpToNoteDialog from "../dialogs/jump_to_note.js";
 | 
			
		||||
import noteSourceDialog from "../dialogs/note_source.js";
 | 
			
		||||
import recentChangesDialog from "../dialogs/recent_changes.js";
 | 
			
		||||
import sqlConsoleDialog from "../dialogs/sql_console.js";
 | 
			
		||||
import searchTreeService from "./search_tree.js";
 | 
			
		||||
import searchNotesService from "./search_notes.js";
 | 
			
		||||
import labelsDialog from "../dialogs/labels.js";
 | 
			
		||||
import relationsDialog from "../dialogs/relations.js";
 | 
			
		||||
import protectedSessionService from "./protected_session.js";
 | 
			
		||||
 | 
			
		||||
function registerEntrypoints() {
 | 
			
		||||
    // hot keys are active also inside inputs and content editables
 | 
			
		||||
@@ -21,35 +23,47 @@ function registerEntrypoints() {
 | 
			
		||||
 | 
			
		||||
    utils.bindShortcut('ctrl+l', addLinkDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    $("#jump-to-note-button").click(jumpToNoteDialog.showDialog);
 | 
			
		||||
    $("#jump-to-note-dialog-button").click(jumpToNoteDialog.showDialog);
 | 
			
		||||
    utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    $("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions);
 | 
			
		||||
 | 
			
		||||
    $("#show-source-button").click(noteSourceDialog.showDialog);
 | 
			
		||||
    utils.bindShortcut('ctrl+u', noteSourceDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    $("#recent-changes-button").click(recentChangesDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    $("#recent-notes-button").click(recentNotesDialog.showDialog);
 | 
			
		||||
    utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog);
 | 
			
		||||
    $("#protected-session-on").click(protectedSessionService.enterProtectedSession);
 | 
			
		||||
    $("#protected-session-off").click(protectedSessionService.leaveProtectedSession);
 | 
			
		||||
 | 
			
		||||
    $("#toggle-search-button").click(searchTreeService.toggleSearch);
 | 
			
		||||
    utils.bindShortcut('ctrl+s', searchTreeService.toggleSearch);
 | 
			
		||||
    $("#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);
 | 
			
		||||
 | 
			
		||||
    $("#options-button").click(optionsDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    if (utils.isElectron()) {
 | 
			
		||||
        $("#history-navigation").show();
 | 
			
		||||
        $("#history-back-button").click(window.history.back);
 | 
			
		||||
        $("#history-forward-button").click(window.history.forward);
 | 
			
		||||
 | 
			
		||||
        utils.bindShortcut('alt+left', window.history.back);
 | 
			
		||||
        utils.bindShortcut('alt+right', window.history.forward);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    utils.bindShortcut('alt+m', e => $(".hide-toggle").toggleClass("suppressed"));
 | 
			
		||||
    utils.bindShortcut('alt+m', e => {
 | 
			
		||||
        $(".hide-toggle").toggle();
 | 
			
		||||
 | 
			
		||||
        // when hiding switch display to block, otherwise grid still tries to display columns which shows
 | 
			
		||||
        // left empty column
 | 
			
		||||
        $("#container").css("display", $("#container").css("display") === "grid" ? "block" : "grid");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // hide (toggle) everything except for the note content for distraction free writing
 | 
			
		||||
    utils.bindShortcut('alt+t', e => {
 | 
			
		||||
@@ -101,28 +115,11 @@ function registerEntrypoints() {
 | 
			
		||||
        $("#note-detail-text").focus();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+-', () => {
 | 
			
		||||
    if (utils.isElectron()) {
 | 
			
		||||
            const webFrame = require('electron').webFrame;
 | 
			
		||||
 | 
			
		||||
            if (webFrame.getZoomFactor() > 0.2) {
 | 
			
		||||
                webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
 | 
			
		||||
        $(document).bind('keydown', 'ctrl+-', zoomService.decreaseZoomFactor);
 | 
			
		||||
        $(document).bind('keydown', 'ctrl+=', zoomService.increaseZoomFactor);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+=', () => {
 | 
			
		||||
        if (utils.isElectron()) {
 | 
			
		||||
            const webFrame = require('electron').webFrame;
 | 
			
		||||
 | 
			
		||||
            webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus());
 | 
			
		||||
 | 
			
		||||
    $("#upload-file-button").click(fileService.uploadFile);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@ import protectedSessionHolder from './protected_session_holder.js';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
 | 
			
		||||
function exportBranch(noteId) {
 | 
			
		||||
    const url = utils.getHost() + "/api/notes/" + noteId + "/export?protectedSessionId="
 | 
			
		||||
        + encodeURIComponent(protectedSessionHolder.getProtectedSessionId());
 | 
			
		||||
function exportBranch(noteId, format) {
 | 
			
		||||
    const url = utils.getHost() + "/api/notes/" + noteId + "/export/" + format +
 | 
			
		||||
        "?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId());
 | 
			
		||||
 | 
			
		||||
    utils.download(url);
 | 
			
		||||
}
 | 
			
		||||
@@ -29,7 +29,7 @@ $("#import-upload").change(async function() {
 | 
			
		||||
        type: 'POST',
 | 
			
		||||
        contentType: false, // NEEDED, DON'T OMIT THIS
 | 
			
		||||
        processData: false, // NEEDED, DON'T OMIT THIS
 | 
			
		||||
    });
 | 
			
		||||
    }).fail((xhr, status, error) => alert('Import error: ' + xhr.responseText));
 | 
			
		||||
 | 
			
		||||
    await treeService.reload();
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ function getNotePathFromLink(url) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getNodePathFromLabel(label) {
 | 
			
		||||
function getNotePathFromLabel(label) {
 | 
			
		||||
    const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
 | 
			
		||||
 | 
			
		||||
    if (notePathMatch !== null) {
 | 
			
		||||
@@ -23,11 +23,11 @@ function getNodePathFromLabel(label) {
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createNoteLink(notePath, noteTitle) {
 | 
			
		||||
async function createNoteLink(notePath, noteTitle) {
 | 
			
		||||
    if (!noteTitle) {
 | 
			
		||||
        const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
			
		||||
 | 
			
		||||
        noteTitle = treeUtils.getNoteTitle(noteId);
 | 
			
		||||
        noteTitle = await treeUtils.getNoteTitle(noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const noteLink = $("<a>", {
 | 
			
		||||
@@ -76,9 +76,11 @@ function goToLink(e) {
 | 
			
		||||
 | 
			
		||||
function addLinkToEditor(linkTitle, linkHref) {
 | 
			
		||||
    const editor = noteDetailText.getEditor();
 | 
			
		||||
    const doc = editor.document;
 | 
			
		||||
 | 
			
		||||
    doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection);
 | 
			
		||||
    editor.model.change( writer => {
 | 
			
		||||
        const insertPosition = editor.model.document.selection.getFirstPosition();
 | 
			
		||||
        writer.insertText(linkTitle, { linkHref: linkHref }, insertPosition);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addTextToEditor(text) {
 | 
			
		||||
@@ -95,7 +97,7 @@ $(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToL
 | 
			
		||||
$(document).on('dblclick', '#note-detail-text a', goToLink);
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    getNodePathFromLabel,
 | 
			
		||||
    getNotePathFromLabel,
 | 
			
		||||
    getNotePathFromLink,
 | 
			
		||||
    createNoteLink,
 | 
			
		||||
    addLinkToEditor,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import infoService from "./info.js";
 | 
			
		||||
 | 
			
		||||
const $changesToPushCount = $("#changes-to-push-count");
 | 
			
		||||
const $outstandingSyncsCount = $("#outstanding-syncs-count");
 | 
			
		||||
 | 
			
		||||
const messageHandlers = [];
 | 
			
		||||
 | 
			
		||||
@@ -43,7 +43,7 @@ function handleMessage(event) {
 | 
			
		||||
            messageHandler(syncData);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $changesToPushCount.html(message.changesToPushCount);
 | 
			
		||||
        $outstandingSyncsCount.html(message.outstandingSyncs);
 | 
			
		||||
    }
 | 
			
		||||
    else if (message.type === 'sync-hash-check-failed') {
 | 
			
		||||
        infoService.showError("Sync check failed!", 60000);
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import utils from './utils.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
import messagingService from "./messaging.js";
 | 
			
		||||
import infoService from "./info.js";
 | 
			
		||||
import linkService from "./link.js";
 | 
			
		||||
import treeCache from "./tree_cache.js";
 | 
			
		||||
import NoteFull from "../entities/note_full.js";
 | 
			
		||||
import noteDetailCode from './note_detail_code.js';
 | 
			
		||||
@@ -14,6 +15,7 @@ import noteDetailText from './note_detail_text.js';
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
const $noteTitle = $("#note-title");
 | 
			
		||||
 | 
			
		||||
@@ -22,10 +24,14 @@ const $noteDetailComponents = $(".note-detail-component");
 | 
			
		||||
const $protectButton = $("#protect-button");
 | 
			
		||||
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 $childrenOverview = $("#children-overview");
 | 
			
		||||
const $scriptArea = $("#note-detail-script-area");
 | 
			
		||||
 | 
			
		||||
let currentNote = null;
 | 
			
		||||
 | 
			
		||||
@@ -116,9 +122,9 @@ async function saveNoteIfChanged() {
 | 
			
		||||
function setNoteBackgroundIfProtected(note) {
 | 
			
		||||
    const isProtected = !!note.isProtected;
 | 
			
		||||
 | 
			
		||||
    $noteDetailWrapper.toggleClass("protected", isProtected);
 | 
			
		||||
    $protectButton.toggle(!isProtected);
 | 
			
		||||
    $unprotectButton.toggle(isProtected);
 | 
			
		||||
    $noteDetailComponentWrapper.toggleClass("protected", isProtected);
 | 
			
		||||
    $protectButton.toggleClass("active", isProtected);
 | 
			
		||||
    $unprotectButton.toggleClass("active", !isProtected);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let isNewNoteCreated = false;
 | 
			
		||||
@@ -150,6 +156,8 @@ async function loadNoteDetail(noteId) {
 | 
			
		||||
 | 
			
		||||
    $noteIdDisplay.html(noteId);
 | 
			
		||||
 | 
			
		||||
    setNoteBackgroundIfProtected(currentNote);
 | 
			
		||||
 | 
			
		||||
    await handleProtectedSession();
 | 
			
		||||
 | 
			
		||||
    $noteDetailWrapper.show();
 | 
			
		||||
@@ -170,7 +178,6 @@ async function loadNoteDetail(noteId) {
 | 
			
		||||
        noteChangeDisabled = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setNoteBackgroundIfProtected(currentNote);
 | 
			
		||||
    treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
 | 
			
		||||
 | 
			
		||||
    // after loading new note make sure editor is scrolled to the top
 | 
			
		||||
@@ -180,6 +187,12 @@ async function loadNoteDetail(noteId) {
 | 
			
		||||
 | 
			
		||||
    const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview');
 | 
			
		||||
    await showChildrenOverview(hideChildrenOverview);
 | 
			
		||||
 | 
			
		||||
    await loadRelationList();
 | 
			
		||||
 | 
			
		||||
    $scriptArea.html('');
 | 
			
		||||
 | 
			
		||||
    await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function showChildrenOverview(hideChildrenOverview) {
 | 
			
		||||
@@ -228,6 +241,29 @@ async function loadLabelList() {
 | 
			
		||||
    return labels;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loadRelationList() {
 | 
			
		||||
    const noteId = getCurrentNoteId();
 | 
			
		||||
 | 
			
		||||
    const relations = await server.get('notes/' + noteId + '/relations');
 | 
			
		||||
 | 
			
		||||
    $relationListInner.html('');
 | 
			
		||||
 | 
			
		||||
    if (relations.length > 0) {
 | 
			
		||||
        for (const relation of relations) {
 | 
			
		||||
            $relationListInner.append(relation.name + " = ");
 | 
			
		||||
            $relationListInner.append(await linkService.createNoteLink(relation.targetNoteId));
 | 
			
		||||
            $relationListInner.append(" ");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $relationList.show();
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        $relationList.hide();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return relations;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loadNote(noteId) {
 | 
			
		||||
    const row = await server.get('notes/' + noteId);
 | 
			
		||||
 | 
			
		||||
@@ -277,6 +313,7 @@ export default {
 | 
			
		||||
    newNoteCreated,
 | 
			
		||||
    focus,
 | 
			
		||||
    loadLabelList,
 | 
			
		||||
    loadRelationList,
 | 
			
		||||
    saveNote,
 | 
			
		||||
    saveNoteIfChanged,
 | 
			
		||||
    noteChanged
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,8 @@ async function show() {
 | 
			
		||||
            highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
 | 
			
		||||
            lint: true,
 | 
			
		||||
            gutters: ["CodeMirror-lint-markers"],
 | 
			
		||||
            lineNumbers: true
 | 
			
		||||
            lineNumbers: true,
 | 
			
		||||
            tabindex: 2 // so that tab from title will lead to code editor focus
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        codeEditor.on('change', noteDetailService.noteChanged);
 | 
			
		||||
@@ -63,16 +64,18 @@ function focus() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function executeCurrentNote() {
 | 
			
		||||
    if (noteDetailService.getCurrentNoteType() === 'code') {
 | 
			
		||||
    // ctrl+enter is also used elsewhere so make sure we're running only when appropriate
 | 
			
		||||
    if (noteDetailService.getCurrentNoteType() !== 'code') {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // make sure note is saved so we load latest changes
 | 
			
		||||
    await noteDetailService.saveNoteIfChanged();
 | 
			
		||||
 | 
			
		||||
    const currentNote = noteDetailService.getCurrentNote();
 | 
			
		||||
 | 
			
		||||
    if (currentNote.mime.endsWith("env=frontend")) {
 | 
			
		||||
            const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
 | 
			
		||||
 | 
			
		||||
            bundleService.executeBundle(bundle);
 | 
			
		||||
        await bundleService.getAndExecuteBundle(noteDetailService.getCurrentNoteId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (currentNote.mime.endsWith("env=backend")) {
 | 
			
		||||
@@ -80,7 +83,6 @@ async function executeCurrentNote() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    infoService.showMessage("Note executed");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', "ctrl+return", executeCurrentNote);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,73 @@
 | 
			
		||||
import bundleService from "./bundle.js";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
import noteDetailService from "./note_detail.js";
 | 
			
		||||
import noteDetailCodeService from "./note_detail_code.js";
 | 
			
		||||
 | 
			
		||||
const $noteDetailCode = $('#note-detail-code');
 | 
			
		||||
const $noteDetailRender = $('#note-detail-render');
 | 
			
		||||
const $toggleEditButton = $('#toggle-edit-button');
 | 
			
		||||
const $renderButton = $('#render-button');
 | 
			
		||||
 | 
			
		||||
let codeEditorInitialized;
 | 
			
		||||
 | 
			
		||||
async function show() {
 | 
			
		||||
    codeEditorInitialized = false;
 | 
			
		||||
 | 
			
		||||
    $noteDetailRender.show();
 | 
			
		||||
 | 
			
		||||
    await render();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function toggleEdit() {
 | 
			
		||||
    if ($noteDetailCode.is(":visible")) {
 | 
			
		||||
        $noteDetailCode.hide();
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        if (!codeEditorInitialized) {
 | 
			
		||||
            await noteDetailCodeService.show();
 | 
			
		||||
 | 
			
		||||
            // because we can't properly scroll only the editor without scrolling the rendering
 | 
			
		||||
            // we limit its height
 | 
			
		||||
            $noteDetailCode.find('.CodeMirror').css('height', '300');
 | 
			
		||||
 | 
			
		||||
            codeEditorInitialized = true;
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            $noteDetailCode.show();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$toggleEditButton.click(toggleEdit);
 | 
			
		||||
 | 
			
		||||
$renderButton.click(render);
 | 
			
		||||
 | 
			
		||||
async function render() {
 | 
			
		||||
    // ctrl+enter is also used elsewhere so make sure we're running only when appropriate
 | 
			
		||||
    if (noteDetailService.getCurrentNoteType() !== 'render') {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (codeEditorInitialized) {
 | 
			
		||||
        await noteDetailService.saveNoteIfChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
 | 
			
		||||
 | 
			
		||||
    $noteDetailRender.html(bundle.html);
 | 
			
		||||
 | 
			
		||||
    // if the note is empty, it doesn't make sense to do render-only since nothing will be rendered
 | 
			
		||||
    if (!bundle.html.trim()) {
 | 
			
		||||
        toggleEdit();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await bundleService.executeBundle(bundle);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', "ctrl+return", render);
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    show,
 | 
			
		||||
    getContent: () => null,
 | 
			
		||||
    getContent: noteDetailCodeService.getContent,
 | 
			
		||||
    focus: () => null
 | 
			
		||||
}
 | 
			
		||||
@@ -11,11 +11,16 @@ async function show() {
 | 
			
		||||
 | 
			
		||||
        textEditor = await BalloonEditor.create($noteDetailText[0], {});
 | 
			
		||||
 | 
			
		||||
        textEditor.document.on('change', noteDetailService.noteChanged);
 | 
			
		||||
        textEditor.model.document.on('change', () => {
 | 
			
		||||
                // change is triggered on just marker/selection changes which is not interesting for us
 | 
			
		||||
                if (textEditor.model.document.differ.getChanges().length > 0) {
 | 
			
		||||
                    noteDetailService.noteChanged();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
 | 
			
		||||
    textEditor.setData(noteDetailService.getCurrentNote().content || "<p></p>");
 | 
			
		||||
    textEditor.setData(noteDetailService.getCurrentNote().content);
 | 
			
		||||
 | 
			
		||||
    $noteDetailText.show();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,9 @@ import server from './server.js';
 | 
			
		||||
import infoService from "./info.js";
 | 
			
		||||
 | 
			
		||||
const $executeScriptButton = $("#execute-script-button");
 | 
			
		||||
const $toggleEditButton = $('#toggle-edit-button');
 | 
			
		||||
const $renderButton = $('#render-button');
 | 
			
		||||
 | 
			
		||||
const noteTypeModel = new NoteTypeModel();
 | 
			
		||||
 | 
			
		||||
function NoteTypeModel() {
 | 
			
		||||
@@ -107,7 +110,7 @@ function NoteTypeModel() {
 | 
			
		||||
 | 
			
		||||
    this.selectRender = function() {
 | 
			
		||||
        self.type('render');
 | 
			
		||||
        self.mime('');
 | 
			
		||||
        self.mime('text/html');
 | 
			
		||||
 | 
			
		||||
        save();
 | 
			
		||||
    };
 | 
			
		||||
@@ -128,6 +131,9 @@ function NoteTypeModel() {
 | 
			
		||||
 | 
			
		||||
    this.updateExecuteScriptButtonVisibility = function() {
 | 
			
		||||
        $executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
 | 
			
		||||
 | 
			
		||||
        $toggleEditButton.toggle(self.type() === 'render');
 | 
			
		||||
        $renderButton.toggle(self.type() === 'render');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								src/public/javascripts/services/options_init.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
 | 
			
		||||
const optionsReady = new Promise((resolve, reject) => {
 | 
			
		||||
    $(document).ready(() => server.get('options').then(resolve));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    optionsReady
 | 
			
		||||
}
 | 
			
		||||
@@ -11,9 +11,23 @@ const $password = $("#protected-session-password");
 | 
			
		||||
const $noteDetailWrapper = $("#note-detail-wrapper");
 | 
			
		||||
const $protectButton = $("#protect-button");
 | 
			
		||||
const $unprotectButton = $("#unprotect-button");
 | 
			
		||||
const $protectedSessionOnButton = $("#protected-session-on");
 | 
			
		||||
const $protectedSessionOffButton = $("#protected-session-off");
 | 
			
		||||
 | 
			
		||||
let protectedSessionDeferred = null;
 | 
			
		||||
 | 
			
		||||
async function enterProtectedSession() {
 | 
			
		||||
    if (!protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
			
		||||
        await ensureProtectedSession(true, true);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function leaveProtectedSession() {
 | 
			
		||||
    if (protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
			
		||||
        utils.reloadApp();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ensureProtectedSession(requireProtectedSession, modal) {
 | 
			
		||||
    const dfd = $.Deferred();
 | 
			
		||||
 | 
			
		||||
@@ -25,7 +39,10 @@ function ensureProtectedSession(requireProtectedSession, modal) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $dialog.dialog({
 | 
			
		||||
            modal: modal,
 | 
			
		||||
            // modal: modal,
 | 
			
		||||
            // everything is now non-modal, because modal dialog caused weird high CPU usage on opening
 | 
			
		||||
            // and tearing of text input
 | 
			
		||||
            modal: false,
 | 
			
		||||
            width: 400,
 | 
			
		||||
            open: () => {
 | 
			
		||||
                if (!modal) {
 | 
			
		||||
@@ -46,7 +63,7 @@ async function setupProtectedSession() {
 | 
			
		||||
    const password = $password.val();
 | 
			
		||||
    $password.val("");
 | 
			
		||||
 | 
			
		||||
    const response = await enterProtectedSession(password);
 | 
			
		||||
    const response = await enterProtectedSessionOnServer(password);
 | 
			
		||||
 | 
			
		||||
    if (!response.success) {
 | 
			
		||||
        infoService.showError("Wrong password.");
 | 
			
		||||
@@ -66,8 +83,10 @@ async function setupProtectedSession() {
 | 
			
		||||
        $noteDetailWrapper.show();
 | 
			
		||||
 | 
			
		||||
        protectedSessionDeferred.resolve();
 | 
			
		||||
 | 
			
		||||
        protectedSessionDeferred = null;
 | 
			
		||||
 | 
			
		||||
        $protectedSessionOnButton.addClass('active');
 | 
			
		||||
        $protectedSessionOffButton.removeClass('active');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -81,13 +100,17 @@ function ensureDialogIsClosed() {
 | 
			
		||||
    $password.val('');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function enterProtectedSession(password) {
 | 
			
		||||
async function enterProtectedSessionOnServer(password) {
 | 
			
		||||
    return await server.post('login/protected', {
 | 
			
		||||
        password: password
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function protectNoteAndSendToServer() {
 | 
			
		||||
    if (noteDetailService.getCurrentNote().isProtected) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await ensureProtectedSession(true, true);
 | 
			
		||||
 | 
			
		||||
    const note = noteDetailService.getCurrentNote();
 | 
			
		||||
@@ -101,6 +124,10 @@ async function protectNoteAndSendToServer() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function unprotectNoteAndSendToServer() {
 | 
			
		||||
    if (!noteDetailService.getCurrentNote().isProtected) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await ensureProtectedSession(true, true);
 | 
			
		||||
 | 
			
		||||
    const note = noteDetailService.getCurrentNote();
 | 
			
		||||
@@ -138,5 +165,7 @@ export default {
 | 
			
		||||
    protectNoteAndSendToServer,
 | 
			
		||||
    unprotectNoteAndSendToServer,
 | 
			
		||||
    protectBranch,
 | 
			
		||||
    ensureDialogIsClosed
 | 
			
		||||
    ensureDialogIsClosed,
 | 
			
		||||
    enterProtectedSession,
 | 
			
		||||
    leaveProtectedSession
 | 
			
		||||
};
 | 
			
		||||
@@ -1,13 +1,11 @@
 | 
			
		||||
import utils from "./utils.js";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
import optionsInitService from './options_init.js';
 | 
			
		||||
 | 
			
		||||
let lastProtectedSessionOperationDate = null;
 | 
			
		||||
let protectedSessionTimeout = null;
 | 
			
		||||
let protectedSessionId = null;
 | 
			
		||||
 | 
			
		||||
$(document).ready(() => {
 | 
			
		||||
    server.get('options').then(options => protectedSessionTimeout = options.protectedSessionTimeout);
 | 
			
		||||
});
 | 
			
		||||
optionsInitService.optionsReady.then(options => protectedSessionTimeout = options.protectedSessionTimeout);
 | 
			
		||||
 | 
			
		||||
setInterval(() => {
 | 
			
		||||
    if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,9 @@ import treeService from './tree.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import infoService from './info.js';
 | 
			
		||||
import linkService from './link.js';
 | 
			
		||||
 | 
			
		||||
function ScriptApi(startNote, currentNote) {
 | 
			
		||||
function ScriptApi(startNote, currentNote, targetNote = null) {
 | 
			
		||||
    const $pluginButtons = $("#plugin-buttons");
 | 
			
		||||
 | 
			
		||||
    async function activateNote(notePath) {
 | 
			
		||||
@@ -42,7 +43,8 @@ function ScriptApi(startNote, currentNote) {
 | 
			
		||||
            script: script,
 | 
			
		||||
            params: prepareParams(params),
 | 
			
		||||
            startNoteId: startNote.noteId,
 | 
			
		||||
            currentNoteId: currentNote.noteId
 | 
			
		||||
            currentNoteId: currentNote.noteId,
 | 
			
		||||
            targetNoteId: targetNote ? targetNote.noteId : null
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return ret.executionResult;
 | 
			
		||||
@@ -51,6 +53,7 @@ function ScriptApi(startNote, currentNote) {
 | 
			
		||||
    return {
 | 
			
		||||
        startNote: startNote,
 | 
			
		||||
        currentNote: currentNote,
 | 
			
		||||
        targetNote: targetNote,
 | 
			
		||||
        addButtonToToolbar,
 | 
			
		||||
        activateNote,
 | 
			
		||||
        getInstanceName: () => window.glob.instanceName,
 | 
			
		||||
@@ -59,7 +62,8 @@ function ScriptApi(startNote, currentNote) {
 | 
			
		||||
        parseDate: utils.parseDate,
 | 
			
		||||
        showMessage: infoService.showMessage,
 | 
			
		||||
        showError: infoService.showError,
 | 
			
		||||
        reloadTree: treeService.reload
 | 
			
		||||
        reloadTree: treeService.reload,
 | 
			
		||||
        createNoteLink: linkService.createNoteLink
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
import ScriptApi from './script_api.js';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
 | 
			
		||||
function ScriptContext(startNote, allNotes) {
 | 
			
		||||
function ScriptContext(startNote, allNotes, targetNote = null) {
 | 
			
		||||
    const modules = {};
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        modules: modules,
 | 
			
		||||
        notes: utils.toObject(allNotes, note => [note.noteId, note]),
 | 
			
		||||
        apis: utils.toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]),
 | 
			
		||||
        apis: utils.toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note, targetNote)]),
 | 
			
		||||
        require: moduleNoteIds => {
 | 
			
		||||
            return moduleName => {
 | 
			
		||||
                const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
import treeUtils from "./tree_utils.js";
 | 
			
		||||
 | 
			
		||||
const $tree = $("#tree");
 | 
			
		||||
const $searchInput = $("input[name='search-text']");
 | 
			
		||||
@@ -7,40 +8,62 @@ const $resetSearchButton = $("#reset-search-button");
 | 
			
		||||
const $doSearchButton = $("#do-search-button");
 | 
			
		||||
const $saveSearchButton = $("#save-search-button");
 | 
			
		||||
const $searchBox = $("#search-box");
 | 
			
		||||
const $searchResults = $("#search-results");
 | 
			
		||||
const $searchResultsInner = $("#search-results-inner");
 | 
			
		||||
const $closeSearchButton = $("#close-search-button");
 | 
			
		||||
 | 
			
		||||
function showSearch() {
 | 
			
		||||
    $searchBox.show();
 | 
			
		||||
    $searchInput.focus();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function hideSearch() {
 | 
			
		||||
    resetSearch();
 | 
			
		||||
 | 
			
		||||
    $searchResults.hide();
 | 
			
		||||
    $searchBox.hide();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleSearch() {
 | 
			
		||||
    if ($searchBox.is(":hidden")) {
 | 
			
		||||
        $searchBox.show();
 | 
			
		||||
        $searchInput.focus();
 | 
			
		||||
        showSearch();
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        resetSearch();
 | 
			
		||||
 | 
			
		||||
        $searchBox.hide();
 | 
			
		||||
        hideSearch();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resetSearch() {
 | 
			
		||||
    $searchInput.val("");
 | 
			
		||||
 | 
			
		||||
    getTree().clearFilter();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getTree() {
 | 
			
		||||
    return $tree.fancytree('getTree');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function doSearch() {
 | 
			
		||||
    const searchText = $searchInput.val();
 | 
			
		||||
 | 
			
		||||
    const noteIds = await server.get('search/' + encodeURIComponent(searchText));
 | 
			
		||||
 | 
			
		||||
    for (const noteId of noteIds) {
 | 
			
		||||
        await treeService.expandToNote(noteId, {noAnimation: true, noEvents: true});
 | 
			
		||||
async function doSearch(searchText) {
 | 
			
		||||
    if (searchText) {
 | 
			
		||||
        $searchInput.val(searchText);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        searchText = $searchInput.val();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Pass a string to perform case insensitive matching
 | 
			
		||||
    getTree().filterBranches(node => noteIds.includes(node.data.noteId));
 | 
			
		||||
    const results = await server.get('search/' + encodeURIComponent(searchText));
 | 
			
		||||
 | 
			
		||||
    $searchResultsInner.empty();
 | 
			
		||||
    $searchResults.show();
 | 
			
		||||
 | 
			
		||||
    for (const result of results) {
 | 
			
		||||
        const link = $('<a>', {
 | 
			
		||||
            href: 'javascript:',
 | 
			
		||||
            text: result.title
 | 
			
		||||
        }).attr('action', 'note').attr('note-path', result.path);
 | 
			
		||||
 | 
			
		||||
        const $result = $('<li>').append(link);
 | 
			
		||||
 | 
			
		||||
        $searchResultsInner.append($result);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function saveSearch() {
 | 
			
		||||
@@ -71,6 +94,11 @@ $resetSearchButton.click(resetSearch);
 | 
			
		||||
 | 
			
		||||
$saveSearchButton.click(saveSearch);
 | 
			
		||||
 | 
			
		||||
$closeSearchButton.click(hideSearch);
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    toggleSearch
 | 
			
		||||
    toggleSearch,
 | 
			
		||||
    resetSearch,
 | 
			
		||||
    showSearch,
 | 
			
		||||
    doSearch
 | 
			
		||||
};
 | 
			
		||||