mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Compare commits
	
		
			127 Commits
		
	
	
		
			v0.9.0-bet
			...
			v0.10.2-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | b5e6f46b9c | ||
|  | 08af4a0465 | ||
|  | 8c5df6321f | ||
|  | d19f044961 | ||
|  | e378d9f645 | ||
|  | 39dc0f71b4 | ||
|  | 0cef5c6b8c | ||
|  | 9b5a44cef4 | ||
|  | 29769ed91d | ||
|  | 867d794e17 | ||
|  | fdd8458336 | ||
|  | a0bec22e96 | ||
|  | 5aeb5cd214 | ||
|  | e827ddffb9 | ||
|  | 98f80998b9 | ||
|  | 69727d0b12 | ||
|  | 84faf32b98 | ||
|  | 6ed6e27602 | ||
|  | fb54678fef | ||
|  | 2cdcb3af12 | ||
|  | cf7a336ac2 | ||
|  | abfc64af95 | ||
|  | 42dd8d4754 | ||
|  | a4e64350e9 | ||
|  | 6f567e3e10 | ||
|  | c6c76ba360 | ||
|  | 429d3f518e | ||
|  | 26e4ad9bf9 | ||
|  | 6ab0cea4e3 | ||
|  | 277368ab43 | ||
|  | e2921a648d | ||
|  | c765dbc5cf | ||
|  | a066c6fe2b | ||
|  | 311952d4dd | ||
|  | 96dab5d51e | ||
|  | 15d951b04e | ||
|  | 8ba830c04b | ||
|  | acc82f39c4 | ||
|  | fad0ec757b | ||
|  | c9d73c6115 | ||
|  | ab2f28ceef | ||
|  | 87e415992c | ||
|  | 12439d8761 | ||
|  | 4f200c73dc | ||
|  | 5f7e74e15c | ||
|  | e8a5d0ae16 | ||
|  | 088fb00ca9 | ||
|  | 05676f3459 | ||
|  | 5d203b2278 | ||
|  | 795d50f02e | ||
|  | cfe0ae1eda | ||
|  | aa57a64c61 | ||
|  | e36a81e189 | ||
|  | 88c07a9e48 | ||
|  | bfd9f292a6 | ||
|  | 9edee9340b | ||
|  | 8550ed72f2 | ||
|  | efffc29649 | ||
|  | 0ec909fd7a | ||
|  | b10b0048f3 | ||
|  | 9bb188b519 | ||
|  | 7464835058 | ||
|  | 913b6bb6f6 | ||
|  | 000cf99546 | ||
|  | c918267750 | ||
|  | 68921ee59b | ||
|  | 7e856283ee | ||
|  | 9c1b8da573 | ||
|  | cb39b9cca8 | ||
|  | 788ac43ad1 | ||
|  | 57d19f3302 | ||
|  | 68bba623b6 | ||
|  | 35998058ce | ||
|  | cdf94181d2 | ||
|  | 91ee90d827 | ||
|  | d3316cd09c | ||
|  | ac1b06967f | ||
|  | 47eb1e3e02 | ||
|  | a69d8737ce | ||
|  | 341f47f0f2 | ||
|  | 19c605a9a8 | ||
|  | 54e4f54678 | ||
|  | 297a2cd9da | ||
|  | d746d707b5 | ||
|  | 299252b650 | ||
|  | fddd1c278f | ||
|  | f52d7e3c28 | ||
|  | a699210a29 | ||
|  | b3c32a39e9 | ||
|  | df27533b66 | ||
|  | b96a1274c5 | ||
|  | 001a5107dd | ||
|  | c8e456cdb1 | ||
|  | 0f6b00e1c8 | ||
|  | 5ea060a054 | ||
|  | 95bb2cf0bb | ||
|  | 4c472ce78b | ||
|  | 511fb89af0 | ||
|  | 7e524c0cd1 | ||
|  | e3e2dc9fff | ||
|  | 1612e9093d | ||
|  | f8649feea4 | ||
|  | ac978c3fa7 | ||
|  | efcc804149 | ||
|  | db514e8f41 | ||
|  | 9c32f66329 | ||
|  | 0fd5102a26 | ||
|  | 840af15dae | ||
|  | f1b0b3bcdb | ||
|  | e5c0acbb43 | ||
|  | 834661c461 | ||
|  | 5204ab5a7e | ||
|  | 74862536a8 | ||
|  | a24f1f5b95 | ||
|  | 0be76f746a | ||
|  | fad89ff63f | ||
|  | b8ae791191 | ||
|  | ce754cbd91 | ||
|  | 0fdb6af98a | ||
|  | f6c7f6a0f2 | ||
|  | 354999f37a | ||
|  | 348c622845 | ||
|  | 44bcdedaba | ||
|  | 755c0f3ce2 | ||
|  | 895bda41b5 | ||
|  | b2df622cb6 | ||
|  | 9ba6e6d0f5 | 
							
								
								
									
										14
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="DataSourceManagerImpl" format="xml" multifile-model="true"> | ||||||
|  |     <data-source source="LOCAL" name="document.db" uuid="a2c75661-f9e2-478f-a69f-6a9409e69997"> | ||||||
|  |       <driver-ref>sqlite.xerial</driver-ref> | ||||||
|  |       <synchronize>true</synchronize> | ||||||
|  |       <jdbc-driver>org.sqlite.JDBC</jdbc-driver> | ||||||
|  |       <jdbc-url>jdbc:sqlite:$USER_HOME$/trilium-data/document.db</jdbc-url> | ||||||
|  |       <driver-properties> | ||||||
|  |         <property name="enable_load_extension" value="true" /> | ||||||
|  |       </driver-properties> | ||||||
|  |     </data-source> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										588
									
								
								.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										588
									
								
								.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,588 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <dataSource name="document.db"> | ||||||
|  |   <database-model serializer="dbm" rdbms="SQLITE" format-version="4.7"> | ||||||
|  |     <root id="1"/> | ||||||
|  |     <schema id="2" parent="1" name="main"> | ||||||
|  |       <Current>1</Current> | ||||||
|  |       <Visible>1</Visible> | ||||||
|  |     </schema> | ||||||
|  |     <collation id="3" parent="1" name="BINARY"/> | ||||||
|  |     <collation id="4" parent="1" name="NOCASE"/> | ||||||
|  |     <collation id="5" parent="1" name="RTRIM"/> | ||||||
|  |     <table id="6" parent="2" name="api_tokens"/> | ||||||
|  |     <table id="7" parent="2" name="branches"/> | ||||||
|  |     <table id="8" parent="2" name="event_log"/> | ||||||
|  |     <table id="9" parent="2" name="images"/> | ||||||
|  |     <table id="10" parent="2" name="labels"/> | ||||||
|  |     <table id="11" parent="2" name="note_images"/> | ||||||
|  |     <table id="12" parent="2" name="note_revisions"/> | ||||||
|  |     <table id="13" parent="2" name="notes"/> | ||||||
|  |     <table id="14" parent="2" name="options"/> | ||||||
|  |     <table id="15" parent="2" name="recent_notes"/> | ||||||
|  |     <table id="16" parent="2" name="source_ids"/> | ||||||
|  |     <table id="17" parent="2" name="sqlite_master"> | ||||||
|  |       <System>1</System> | ||||||
|  |     </table> | ||||||
|  |     <table id="18" parent="2" name="sqlite_sequence"> | ||||||
|  |       <System>1</System> | ||||||
|  |     </table> | ||||||
|  |     <table id="19" parent="2" name="sync"/> | ||||||
|  |     <column id="20" parent="6" name="apiTokenId"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="21" parent="6" name="token"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="22" parent="6" name="dateCreated"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="23" 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"> | ||||||
|  |       <NameSurrogate>1</NameSurrogate> | ||||||
|  |       <ColNames>apiTokenId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |       <Unique>1</Unique> | ||||||
|  |     </index> | ||||||
|  |     <key id="25" parent="6"> | ||||||
|  |       <ColNames>apiTokenId</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |       <UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName> | ||||||
|  |     </key> | ||||||
|  |     <column id="26" parent="7" name="branchId"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="27" parent="7" name="noteId"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="28" parent="7" name="parentNoteId"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="29" parent="7" name="notePosition"> | ||||||
|  |       <Position>4</Position> | ||||||
|  |       <DataType>INTEGER|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="30" parent="7" name="prefix"> | ||||||
|  |       <Position>5</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="31" parent="7" name="isExpanded"> | ||||||
|  |       <Position>6</Position> | ||||||
|  |       <DataType>BOOLEAN|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="32" 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"> | ||||||
|  |       <Position>8</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <index id="34" 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"> | ||||||
|  |       <ColNames>noteId | ||||||
|  | parentNoteId</ColNames> | ||||||
|  |       <ColumnCollations> | ||||||
|  | </ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <index id="36" parent="7" name="IDX_branches_noteId"> | ||||||
|  |       <ColNames>noteId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <key id="37" parent="7"> | ||||||
|  |       <ColNames>branchId</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |       <UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName> | ||||||
|  |     </key> | ||||||
|  |     <column id="38" parent="8" name="id"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>INTEGER|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <SequenceIdentity>1</SequenceIdentity> | ||||||
|  |     </column> | ||||||
|  |     <column id="39" parent="8" name="noteId"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="40" parent="8" name="comment"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="41" parent="8" name="dateAdded"> | ||||||
|  |       <Position>4</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <key id="42" parent="8"> | ||||||
|  |       <ColNames>id</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |     </key> | ||||||
|  |     <foreign-key id="43" parent="8"> | ||||||
|  |       <ColNames>noteId</ColNames> | ||||||
|  |       <RefTableName>notes</RefTableName> | ||||||
|  |       <RefColNames>noteId</RefColNames> | ||||||
|  |     </foreign-key> | ||||||
|  |     <column id="44" parent="9" name="imageId"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="45" parent="9" name="format"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="46" parent="9" name="checksum"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="47" parent="9" name="name"> | ||||||
|  |       <Position>4</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="48" parent="9" name="data"> | ||||||
|  |       <Position>5</Position> | ||||||
|  |       <DataType>BLOB|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="49" parent="9" name="isDeleted"> | ||||||
|  |       <Position>6</Position> | ||||||
|  |       <DataType>INT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <DefaultExpression>0</DefaultExpression> | ||||||
|  |     </column> | ||||||
|  |     <column id="50" parent="9" name="dateModified"> | ||||||
|  |       <Position>7</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="51" parent="9" name="dateCreated"> | ||||||
|  |       <Position>8</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <index id="52" parent="9" name="sqlite_autoindex_images_1"> | ||||||
|  |       <NameSurrogate>1</NameSurrogate> | ||||||
|  |       <ColNames>imageId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |       <Unique>1</Unique> | ||||||
|  |     </index> | ||||||
|  |     <key id="53" parent="9"> | ||||||
|  |       <ColNames>imageId</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |       <UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName> | ||||||
|  |     </key> | ||||||
|  |     <column id="54" parent="10" name="labelId"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="55" parent="10" name="noteId"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="56" parent="10" name="name"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="57" parent="10" name="value"> | ||||||
|  |       <Position>4</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <DefaultExpression>''</DefaultExpression> | ||||||
|  |     </column> | ||||||
|  |     <column id="58" parent="10" name="position"> | ||||||
|  |       <Position>5</Position> | ||||||
|  |       <DataType>INT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <DefaultExpression>0</DefaultExpression> | ||||||
|  |     </column> | ||||||
|  |     <column id="59" parent="10" name="dateCreated"> | ||||||
|  |       <Position>6</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="60" parent="10" name="dateModified"> | ||||||
|  |       <Position>7</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="61" parent="10" name="isDeleted"> | ||||||
|  |       <Position>8</Position> | ||||||
|  |       <DataType>INT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <index id="62" parent="10" name="sqlite_autoindex_labels_1"> | ||||||
|  |       <NameSurrogate>1</NameSurrogate> | ||||||
|  |       <ColNames>labelId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |       <Unique>1</Unique> | ||||||
|  |     </index> | ||||||
|  |     <index id="63" parent="10" name="IDX_labels_noteId"> | ||||||
|  |       <ColNames>noteId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <index id="64" parent="10" name="IDX_labels_name_value"> | ||||||
|  |       <ColNames>name | ||||||
|  | value</ColNames> | ||||||
|  |       <ColumnCollations> | ||||||
|  | </ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <key id="65" parent="10"> | ||||||
|  |       <ColNames>labelId</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |       <UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName> | ||||||
|  |     </key> | ||||||
|  |     <column id="66" parent="11" name="noteImageId"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="67" parent="11" name="noteId"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="68" parent="11" name="imageId"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="69" parent="11" name="isDeleted"> | ||||||
|  |       <Position>4</Position> | ||||||
|  |       <DataType>INT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <DefaultExpression>0</DefaultExpression> | ||||||
|  |     </column> | ||||||
|  |     <column id="70" parent="11" name="dateModified"> | ||||||
|  |       <Position>5</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="71" parent="11" name="dateCreated"> | ||||||
|  |       <Position>6</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <index id="72" parent="11" name="sqlite_autoindex_note_images_1"> | ||||||
|  |       <NameSurrogate>1</NameSurrogate> | ||||||
|  |       <ColNames>noteImageId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |       <Unique>1</Unique> | ||||||
|  |     </index> | ||||||
|  |     <index id="73" parent="11" name="IDX_note_images_noteId_imageId"> | ||||||
|  |       <ColNames>noteId | ||||||
|  | imageId</ColNames> | ||||||
|  |       <ColumnCollations> | ||||||
|  | </ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <index id="74" parent="11" name="IDX_note_images_noteId"> | ||||||
|  |       <ColNames>noteId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <index id="75" parent="11" name="IDX_note_images_imageId"> | ||||||
|  |       <ColNames>imageId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <key id="76" parent="11"> | ||||||
|  |       <ColNames>noteImageId</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |       <UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName> | ||||||
|  |     </key> | ||||||
|  |     <column id="77" parent="12" name="noteRevisionId"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="78" parent="12" name="noteId"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="79" parent="12" name="title"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="80" parent="12" name="content"> | ||||||
|  |       <Position>4</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="81" parent="12" name="isProtected"> | ||||||
|  |       <Position>5</Position> | ||||||
|  |       <DataType>INT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <DefaultExpression>0</DefaultExpression> | ||||||
|  |     </column> | ||||||
|  |     <column id="82" parent="12" name="dateModifiedFrom"> | ||||||
|  |       <Position>6</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="83" parent="12" name="dateModifiedTo"> | ||||||
|  |       <Position>7</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <index id="84" parent="12" name="sqlite_autoindex_note_revisions_1"> | ||||||
|  |       <NameSurrogate>1</NameSurrogate> | ||||||
|  |       <ColNames>noteRevisionId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |       <Unique>1</Unique> | ||||||
|  |     </index> | ||||||
|  |     <index id="85" parent="12" name="IDX_note_revisions_noteId"> | ||||||
|  |       <ColNames>noteId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <index id="86" parent="12" name="IDX_note_revisions_dateModifiedFrom"> | ||||||
|  |       <ColNames>dateModifiedFrom</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <index id="87" parent="12" name="IDX_note_revisions_dateModifiedTo"> | ||||||
|  |       <ColNames>dateModifiedTo</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <key id="88" parent="12"> | ||||||
|  |       <ColNames>noteRevisionId</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |       <UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName> | ||||||
|  |     </key> | ||||||
|  |     <column id="89" parent="13" name="noteId"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="90" parent="13" name="title"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="91" parent="13" name="content"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="92" parent="13" name="isProtected"> | ||||||
|  |       <Position>4</Position> | ||||||
|  |       <DataType>INT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <DefaultExpression>0</DefaultExpression> | ||||||
|  |     </column> | ||||||
|  |     <column id="93" parent="13" name="isDeleted"> | ||||||
|  |       <Position>5</Position> | ||||||
|  |       <DataType>INT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <DefaultExpression>0</DefaultExpression> | ||||||
|  |     </column> | ||||||
|  |     <column id="94" parent="13" name="dateCreated"> | ||||||
|  |       <Position>6</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="95" parent="13" name="dateModified"> | ||||||
|  |       <Position>7</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="96" parent="13" name="type"> | ||||||
|  |       <Position>8</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <DefaultExpression>'text'</DefaultExpression> | ||||||
|  |     </column> | ||||||
|  |     <column id="97" parent="13" name="mime"> | ||||||
|  |       <Position>9</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <DefaultExpression>'text/html'</DefaultExpression> | ||||||
|  |     </column> | ||||||
|  |     <index id="98" parent="13" name="sqlite_autoindex_notes_1"> | ||||||
|  |       <NameSurrogate>1</NameSurrogate> | ||||||
|  |       <ColNames>noteId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |       <Unique>1</Unique> | ||||||
|  |     </index> | ||||||
|  |     <index id="99" parent="13" name="IDX_notes_isDeleted"> | ||||||
|  |       <ColNames>isDeleted</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <key id="100" parent="13"> | ||||||
|  |       <ColNames>noteId</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |       <UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName> | ||||||
|  |     </key> | ||||||
|  |     <column id="101" parent="14" name="name"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="102" parent="14" name="value"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="103" parent="14" name="dateModified"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>INT|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="104" parent="14" name="isSynced"> | ||||||
|  |       <Position>4</Position> | ||||||
|  |       <DataType>INTEGER|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <DefaultExpression>0</DefaultExpression> | ||||||
|  |     </column> | ||||||
|  |     <index id="105" parent="14" name="sqlite_autoindex_options_1"> | ||||||
|  |       <NameSurrogate>1</NameSurrogate> | ||||||
|  |       <ColNames>name</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |       <Unique>1</Unique> | ||||||
|  |     </index> | ||||||
|  |     <key id="106" parent="14"> | ||||||
|  |       <ColNames>name</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |       <UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName> | ||||||
|  |     </key> | ||||||
|  |     <column id="107" parent="15" name="branchId"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="108" parent="15" name="notePath"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="109" parent="15" name="dateAccessed"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="110" parent="15" name="isDeleted"> | ||||||
|  |       <Position>4</Position> | ||||||
|  |       <DataType>INT|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <index id="111" parent="15" name="sqlite_autoindex_recent_notes_1"> | ||||||
|  |       <NameSurrogate>1</NameSurrogate> | ||||||
|  |       <ColNames>branchId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |       <Unique>1</Unique> | ||||||
|  |     </index> | ||||||
|  |     <key id="112" parent="15"> | ||||||
|  |       <ColNames>branchId</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |       <UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName> | ||||||
|  |     </key> | ||||||
|  |     <column id="113" parent="16" name="sourceId"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="114" parent="16" name="dateCreated"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <index id="115" parent="16" name="sqlite_autoindex_source_ids_1"> | ||||||
|  |       <NameSurrogate>1</NameSurrogate> | ||||||
|  |       <ColNames>sourceId</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |       <Unique>1</Unique> | ||||||
|  |     </index> | ||||||
|  |     <key id="116" parent="16"> | ||||||
|  |       <ColNames>sourceId</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |       <UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName> | ||||||
|  |     </key> | ||||||
|  |     <column id="117" parent="17" name="type"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>text|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="118" parent="17" name="name"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>text|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="119" parent="17" name="tbl_name"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>text|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="120" parent="17" name="rootpage"> | ||||||
|  |       <Position>4</Position> | ||||||
|  |       <DataType>integer|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="121" parent="17" name="sql"> | ||||||
|  |       <Position>5</Position> | ||||||
|  |       <DataType>text|0s</DataType> | ||||||
|  |     </column> | ||||||
|  |     <column id="122" parent="18" name="name"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |     </column> | ||||||
|  |     <column id="123" parent="18" name="seq"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |     </column> | ||||||
|  |     <column id="124" parent="19" name="id"> | ||||||
|  |       <Position>1</Position> | ||||||
|  |       <DataType>INTEGER|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |       <SequenceIdentity>1</SequenceIdentity> | ||||||
|  |     </column> | ||||||
|  |     <column id="125" parent="19" name="entityName"> | ||||||
|  |       <Position>2</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="126" parent="19" name="entityId"> | ||||||
|  |       <Position>3</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="127" parent="19" name="sourceId"> | ||||||
|  |       <Position>4</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <column id="128" parent="19" name="syncDate"> | ||||||
|  |       <Position>5</Position> | ||||||
|  |       <DataType>TEXT|0s</DataType> | ||||||
|  |       <NotNull>1</NotNull> | ||||||
|  |     </column> | ||||||
|  |     <index id="129" parent="19" name="IDX_sync_entityName_entityId"> | ||||||
|  |       <ColNames>entityName | ||||||
|  | entityId</ColNames> | ||||||
|  |       <ColumnCollations> | ||||||
|  | </ColumnCollations> | ||||||
|  |       <Unique>1</Unique> | ||||||
|  |     </index> | ||||||
|  |     <index id="130" parent="19" name="IDX_sync_syncDate"> | ||||||
|  |       <ColNames>syncDate</ColNames> | ||||||
|  |       <ColumnCollations></ColumnCollations> | ||||||
|  |     </index> | ||||||
|  |     <key id="131" parent="19"> | ||||||
|  |       <ColNames>id</ColNames> | ||||||
|  |       <Primary>1</Primary> | ||||||
|  |     </key> | ||||||
|  |   </database-model> | ||||||
|  | </dataSource> | ||||||
							
								
								
									
										2
									
								
								.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997/storage_v2/_src_/schema/main.uQUzAA.meta
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997/storage_v2/_src_/schema/main.uQUzAA.meta
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | #n:main | ||||||
|  | !<md> [0, 0, null, null, -2147483648, -2147483648] | ||||||
							
								
								
									
										10
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | <component name="InspectionProjectProfileManager"> | ||||||
|  |   <profile version="1.0"> | ||||||
|  |     <option name="myName" value="Project Default" /> | ||||||
|  |     <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false"> | ||||||
|  |       <option name="processCode" value="true" /> | ||||||
|  |       <option name="processLiterals" value="true" /> | ||||||
|  |       <option name="processComments" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |   </profile> | ||||||
|  | </component> | ||||||
							
								
								
									
										9
									
								
								.idea/jsLinters/jslint.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.idea/jsLinters/jslint.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="JSLintConfiguration"> | ||||||
|  |     <option devel="true" /> | ||||||
|  |     <option es6="true" /> | ||||||
|  |     <option maxerr="50" /> | ||||||
|  |     <option node="true" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										9
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="JavaScriptSettings"> | ||||||
|  |     <option name="languageLevel" value="ES6" /> | ||||||
|  |   </component> | ||||||
|  |   <component name="ProjectRootManager"> | ||||||
|  |     <output url="file://$PROJECT_DIR$/out" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="ProjectModuleManager"> | ||||||
|  |     <modules> | ||||||
|  |       <module fileurl="file://$PROJECT_DIR$/trilium.iml" filepath="$PROJECT_DIR$/trilium.iml" /> | ||||||
|  |     </modules> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="VcsDirectoryMappings"> | ||||||
|  |     <mapping directory="" vcs="Git" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
| @@ -1,4 +1,6 @@ | |||||||
| # Trilium Notes | # Trilium Notes | ||||||
|  |  | ||||||
|  | [](https://gitter.im/trilium-notes/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||||||
| Trilium Notes is a hierarchical note taking application. Picture tells a thousand words: | Trilium Notes is a hierarchical note taking application. Picture tells a thousand words: | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -10,7 +12,7 @@ Trilium Notes is a hierarchical note taking application. Picture tells a thousan | |||||||
| * WYSIWYG (What You See Is What You Get) editing | * WYSIWYG (What You See Is What You Get) editing | ||||||
| * Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation) | * Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation) | ||||||
| * Seamless note versioning | * Seamless note versioning | ||||||
| * Note attributes can be used to tag/label notes as an alternative note organization and querying | * Note labels can be used to tag/label notes as an alternative note organization and querying | ||||||
| * Can be deployed as web application and / or desktop application with offline access (electron based) | * Can be deployed as web application and / or desktop application with offline access (electron based) | ||||||
| * [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) self-hosted sync server | * [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) self-hosted sync server | ||||||
| * Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes) | * Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes) | ||||||
| @@ -35,11 +37,12 @@ List of documentation pages: | |||||||
| * [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp) | * [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp) | ||||||
| * [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation) | * [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation) | ||||||
| * [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation) | * [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation) | ||||||
| * [Attributes](https://github.com/zadam/trilium/wiki/Attributes) | * [Labels](https://github.com/zadam/trilium/wiki/Labels) | ||||||
| * [Links](https://github.com/zadam/trilium/wiki/Links) | * [Links](https://github.com/zadam/trilium/wiki/Links) | ||||||
| * [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes) | * [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes) | ||||||
| * [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes) | * [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes) | ||||||
| * [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization) | * [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization) | ||||||
| * [Document](https://github.com/zadam/trilium/wiki/Document) | * [Document](https://github.com/zadam/trilium/wiki/Document) | ||||||
|  | * [Theming](https://github.com/zadam/trilium/wiki/Theming) | ||||||
| * [Keyboard shortcuts](https://github.com/zadam/trilium/wiki/Keyboard-shortcuts) | * [Keyboard shortcuts](https://github.com/zadam/trilium/wiki/Keyboard-shortcuts) | ||||||
| * [Troubleshooting](https://github.com/zadam/trilium/wiki/Troubleshooting) | * [Troubleshooting](https://github.com/zadam/trilium/wiki/Troubleshooting) | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ jq '.version = "'$VERSION'"' package.json|sponge package.json | |||||||
|  |  | ||||||
| git add package.json | git add package.json | ||||||
|  |  | ||||||
| echo 'module.exports = { build_date:"'`date --iso-8601=seconds`'", build_revision: "'`git log -1 --format="%H"`'" };' > services/build.js | echo 'module.exports = { buildDate:"'`date --iso-8601=seconds`'", buildRevision: "'`git log -1 --format="%H"`'" };' > services/build.js | ||||||
|  |  | ||||||
| git add services/build.js | git add services/build.js | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								db/main_branches.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								db/main_branches.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | 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'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('wLTa2l3lYi83', 'HJusZTbBU494', '3RkyK9LI18dO', 2, null, 1, 0, '2017-12-23T01:20:50.709Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('yMhwsE7uvEij', '3oldoiMUPOlr', 'HJusZTbBU494', 1, null, 1, 0, '2017-12-23T01:20:55.775Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('EjQTcVVHFmmZ', 'MG0wntwILQW6', '3oldoiMUPOlr', 1, null, 1, 0, '2017-12-23T01:21:10.517Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('jvhKcwz4pYTr', 'ZC78NlmdXeC6', 'WdWZFuWNVDZk', 0, null, 1, 0, '2017-12-23T04:06:21.579Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('CarTrwkGVcPz', 'NncfGH8dyNjJ', 'WdWZFuWNVDZk', 1, null, 0, 0, '2017-12-23T04:06:24.012Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6M7qPlr7at6N', 'eouCLkjbruai', 'NncfGH8dyNjJ', 0, null, 0, 0, '2017-12-23T01:23:28.291Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('tQgognnAH9WI', 'C44aq4mkaX67', 'NncfGH8dyNjJ', 1, null, 0, 0, '2017-12-23T01:23:31.879Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xyAi7MmgvAgR', 'C44aq4mkaX67', 'ZC78NlmdXeC6', 1, null, 0, 0, '2017-12-23T01:23:47.756Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xQ3fjRp9yaPq', 'I6Cw88AirBBl', 'C44aq4mkaX67', 0, null, 0, 0, '2017-12-23T01:24:04.681Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('2GOsNT5LsvTP', 'mcEwFMSjhlvL', 'C44aq4mkaX67', 1, null, 0, 0, '2017-12-23T01:29:35.974Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RxUiraiR655R', 'CF2lUIJAr6Ey', 'NncfGH8dyNjJ', 2, null, 0, 0, '2017-12-23T01:34:37.658Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('mZuSrZ18Zmv0', 'xkXwueRoDNeN', 'MG0wntwILQW6', 0, null, 0, 0, '2017-12-23T01:35:40.306Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('hbcWTnEnXPwF', 'eXHZAKsMYgur', '1Heh2acXfPNt', 3, null, 1, 0, '2017-12-23T03:32:42.868Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('8a3aNxjG0nu7', '2WU27ekfy07E', 'eXHZAKsMYgur', 0, null, 0, 0, '2017-12-23T03:32:49.379Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('4Tu6vaPdCxCM', 'TjWEndYCCg7g', 'eXHZAKsMYgur', 1, null, 0, 0, '2017-12-23T03:33:23.584Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('lBPOmhP12egP', '8nRNDJGyGs2Z', 'TjWEndYCCg7g', 0, null, 0, 0, '2017-12-23T03:33:37.327Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('C5ipVqeDWySp', '9zSwD89vgzNO', '8nRNDJGyGs2Z', 0, null, 0, 0, '2017-12-23T03:37:04.912Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uSitzbGcSATJ', 'u5t1EvWa3CMO', 'TjWEndYCCg7g', 1, null, 0, 0, '2017-12-23T03:39:21.918Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GZ6aRI8rdSJt', '8nRNDJGyGs2Z', 'MG0wntwILQW6', 1, '', 0, 0, '2017-12-23T03:42:28.310Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('HsN4600rQoL9', 'Iha4YwchR413', '3oldoiMUPOlr', 0, null, 1, 0, '2017-12-23T03:44:30.945Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uipfvAfwWRgx', '6ZuXjCSWgjB4', 'HJusZTbBU494', 0, null, 0, 0, '2017-12-23T03:44:54.096Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nMRpPWWH8WRk', 'GpGnjmcAPeWG', '6ZuXjCSWgjB4', 0, null, 1, 0, '2017-12-23T03:44:57.036Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4wt27WNjepw', '21K84UqGhqlt', 'GpGnjmcAPeWG', 0, null, 0, 0, '2017-12-23T03:45:10.933Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('0fpnraUGs9Kl', 'rz5t0r9Qr2WC', 'HJusZTbBU494', 2, null, 1, 0, '2017-12-23T03:45:20.914Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('d8L8zYlLTbym', 'R6pheWjdwmNU', 'rz5t0r9Qr2WC', 0, null, 1, 0, '2017-12-23T03:45:28.002Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T4USGzfllu5t', '5v5Dx6LMHXIO', 'Iha4YwchR413', 0, null, 0, 0, '2017-12-23T03:45:44.184Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4JgFNIobvQW', 'MLQjmREtcnJ3', 'R6pheWjdwmNU', 0, null, 0, 0, '2017-12-23T03:47:48.208Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nfWjptAU2ZDg', 'pTTjrxgnvURB', 'R6pheWjdwmNU', 1, null, 0, 0, '2017-12-23T03:47:55.932Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T2ToYBfyPy0g', 'cFK9sGYZaMWs', 'rz5t0r9Qr2WC', 1, null, 0, 0, '2017-12-23T03:49:32.210Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('NG4gbKOnsM3v', '21K84UqGhqlt', 'MLQjmREtcnJ3', 0, '28. 11. 2017', 0, 0, '2017-12-23T03:53:38.110Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Fstg4tkccO4N', '5v5Dx6LMHXIO', 'MLQjmREtcnJ3', 1, '21. 12. 2017', 0, 0, '2017-12-23T03:53:49.737Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('MN8B7qXDUViO', 'xkXwueRoDNeN', 'MLQjmREtcnJ3', 2, '22. 12. 2017', 0, 0, '2017-12-23T03:53:57.486Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('gSRkHpB7Bu3D', 'pOFVzbXLmzhX', 'R6pheWjdwmNU', 2, null, 0, 0, '2017-12-23T03:54:46.138Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6brdjeWDOB6w', '0xtvjqrcGiRB', 'ZC78NlmdXeC6', 0, null, 0, 0, '2017-12-23T04:02:06.650Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('AqKUM2zUVFUF', 'Zl69uXBSen0w', 'ZC78NlmdXeC6', 2, null, 1, 0, '2017-12-23T04:02:16.685Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Ez7NN2WVzRc4', '62BKAQMVP2KW', 'Zl69uXBSen0w', 1, null, 0, 0, '2017-12-23T04:02:39.164Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('t3vVElqMIQVa', 'h4OfLEAYspud', 'WdWZFuWNVDZk', 2, null, 1, 0, '2017-12-23T04:06:25.769Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('O983DHtLpgmr', '1hASbLRDL7oo', 'h4OfLEAYspud', 0, null, 0, 0, '2017-12-23T16:42:26.347Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RsvL795Mk1bp', '1hASbLRDL7oo', 'GpGnjmcAPeWG', 1, '', 0, 0, '2017-12-23T04:04:56.830Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('79e4hrHLFmx6', 'jyqG9GucsMdn', 'Iha4YwchR413', 1, null, 0, 0, '2017-12-23T04:05:16.439Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('oWO8rctUjf7d', 'WdWZFuWNVDZk', '1Heh2acXfPNt', 5, null, 1, 0, '2017-12-23T04:06:16.179Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GOxcrZrxalFN', 'yK4SBJfwD3tY', '1Heh2acXfPNt', 8, null, 1, 0, '2017-12-23T04:06:32.833Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bSPmEvjLzQKU', 'r4BnsmSQeVr1', 'yK4SBJfwD3tY', 0, null, 0, 0, '2017-12-23T04:06:37.427Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bMtxCD6cwNR9', 'QbL3pTvhgzM8', 'yK4SBJfwD3tY', 2, null, 0, 0, '2017-12-23T04:06:43.841Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('o4ycR7xIi4oI', 'moMbTKwN15Ps', 'yK4SBJfwD3tY', 3, null, 1, 0, '2017-12-23T04:06:49.331Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('abTEhnOsAsSg', 'PEGQGg0In3Ar', 'GpGnjmcAPeWG', 2, null, 0, 0, '2017-12-23T16:44:35.900Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bryQseMhyzaI', 'IlULcDiOTI4K', '1Heh2acXfPNt', 0, null, 0, 0, '2017-12-23T18:04:26.439Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('ccslPJf3wQV3', 'vBv6ovBupfTj', 'IlULcDiOTI4K', 0, null, 0, 0, '2017-12-23T18:04:50.904Z'); | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('5Dt9YCMn59sY', 'mw4f2xB4J5fV', 'IlULcDiOTI4K', 1, null, 0, 0, '2017-12-23T18:05:24.868Z'); | ||||||
| @@ -1,52 +0,0 @@ | |||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('dLgtLUFn3GoN', '1Heh2acXfPNt', 'root', 21, null, 1, 0, '2017-12-23T00:46:39.304Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QLfS835GSfIh', '3RkyK9LI18dO', '1Heh2acXfPNt', 1, null, 1, 0, '2017-12-23T01:20:04.181Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QJAcYJ1gGUh9', 'L1Ox40M1aEyy', '3RkyK9LI18dO', 0, null, 0, 0, '2017-12-23T01:20:45.365Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('wLTa2l3lYi83', 'HJusZTbBU494', '3RkyK9LI18dO', 2, null, 1, 0, '2017-12-23T01:20:50.709Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('yMhwsE7uvEij', '3oldoiMUPOlr', 'HJusZTbBU494', 1, null, 1, 0, '2017-12-23T01:20:55.775Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('EjQTcVVHFmmZ', 'MG0wntwILQW6', '3oldoiMUPOlr', 1, null, 1, 0, '2017-12-23T01:21:10.517Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('jvhKcwz4pYTr', 'ZC78NlmdXeC6', 'WdWZFuWNVDZk', 0, null, 1, 0, '2017-12-23T04:06:21.579Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('CarTrwkGVcPz', 'NncfGH8dyNjJ', 'WdWZFuWNVDZk', 1, null, 0, 0, '2017-12-23T04:06:24.012Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6M7qPlr7at6N', 'eouCLkjbruai', 'NncfGH8dyNjJ', 0, null, 0, 0, '2017-12-23T01:23:28.291Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('tQgognnAH9WI', 'C44aq4mkaX67', 'NncfGH8dyNjJ', 1, null, 0, 0, '2017-12-23T01:23:31.879Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xyAi7MmgvAgR', 'C44aq4mkaX67', 'ZC78NlmdXeC6', 1, null, 0, 0, '2017-12-23T01:23:47.756Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xQ3fjRp9yaPq', 'I6Cw88AirBBl', 'C44aq4mkaX67', 0, null, 0, 0, '2017-12-23T01:24:04.681Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('2GOsNT5LsvTP', 'mcEwFMSjhlvL', 'C44aq4mkaX67', 1, null, 0, 0, '2017-12-23T01:29:35.974Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RxUiraiR655R', 'CF2lUIJAr6Ey', 'NncfGH8dyNjJ', 2, null, 0, 0, '2017-12-23T01:34:37.658Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('mZuSrZ18Zmv0', 'xkXwueRoDNeN', 'MG0wntwILQW6', 0, null, 0, 0, '2017-12-23T01:35:40.306Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('hbcWTnEnXPwF', 'eXHZAKsMYgur', '1Heh2acXfPNt', 3, null, 1, 0, '2017-12-23T03:32:42.868Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('8a3aNxjG0nu7', '2WU27ekfy07E', 'eXHZAKsMYgur', 0, null, 0, 0, '2017-12-23T03:32:49.379Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('4Tu6vaPdCxCM', 'TjWEndYCCg7g', 'eXHZAKsMYgur', 1, null, 0, 0, '2017-12-23T03:33:23.584Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('lBPOmhP12egP', '8nRNDJGyGs2Z', 'TjWEndYCCg7g', 0, null, 0, 0, '2017-12-23T03:33:37.327Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('C5ipVqeDWySp', '9zSwD89vgzNO', '8nRNDJGyGs2Z', 0, null, 0, 0, '2017-12-23T03:37:04.912Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uSitzbGcSATJ', 'u5t1EvWa3CMO', 'TjWEndYCCg7g', 1, null, 0, 0, '2017-12-23T03:39:21.918Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GZ6aRI8rdSJt', '8nRNDJGyGs2Z', 'MG0wntwILQW6', 1, '', 0, 0, '2017-12-23T03:42:28.310Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('HsN4600rQoL9', 'Iha4YwchR413', '3oldoiMUPOlr', 0, null, 1, 0, '2017-12-23T03:44:30.945Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uipfvAfwWRgx', '6ZuXjCSWgjB4', 'HJusZTbBU494', 0, null, 0, 0, '2017-12-23T03:44:54.096Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nMRpPWWH8WRk', 'GpGnjmcAPeWG', '6ZuXjCSWgjB4', 0, null, 1, 0, '2017-12-23T03:44:57.036Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4wt27WNjepw', '21K84UqGhqlt', 'GpGnjmcAPeWG', 0, null, 0, 0, '2017-12-23T03:45:10.933Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('0fpnraUGs9Kl', 'rz5t0r9Qr2WC', 'HJusZTbBU494', 2, null, 1, 0, '2017-12-23T03:45:20.914Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('d8L8zYlLTbym', 'R6pheWjdwmNU', 'rz5t0r9Qr2WC', 0, null, 1, 0, '2017-12-23T03:45:28.002Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T4USGzfllu5t', '5v5Dx6LMHXIO', 'Iha4YwchR413', 0, null, 0, 0, '2017-12-23T03:45:44.184Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4JgFNIobvQW', 'MLQjmREtcnJ3', 'R6pheWjdwmNU', 0, null, 0, 0, '2017-12-23T03:47:48.208Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nfWjptAU2ZDg', 'pTTjrxgnvURB', 'R6pheWjdwmNU', 1, null, 0, 0, '2017-12-23T03:47:55.932Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T2ToYBfyPy0g', 'cFK9sGYZaMWs', 'rz5t0r9Qr2WC', 1, null, 0, 0, '2017-12-23T03:49:32.210Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('NG4gbKOnsM3v', '21K84UqGhqlt', 'MLQjmREtcnJ3', 0, '28. 11. 2017', 0, 0, '2017-12-23T03:53:38.110Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Fstg4tkccO4N', '5v5Dx6LMHXIO', 'MLQjmREtcnJ3', 1, '21. 12. 2017', 0, 0, '2017-12-23T03:53:49.737Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('MN8B7qXDUViO', 'xkXwueRoDNeN', 'MLQjmREtcnJ3', 2, '22. 12. 2017', 0, 0, '2017-12-23T03:53:57.486Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('gSRkHpB7Bu3D', 'pOFVzbXLmzhX', 'R6pheWjdwmNU', 2, null, 0, 0, '2017-12-23T03:54:46.138Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6brdjeWDOB6w', '0xtvjqrcGiRB', 'ZC78NlmdXeC6', 0, null, 0, 0, '2017-12-23T04:02:06.650Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('AqKUM2zUVFUF', 'Zl69uXBSen0w', 'ZC78NlmdXeC6', 2, null, 1, 0, '2017-12-23T04:02:16.685Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Ez7NN2WVzRc4', '62BKAQMVP2KW', 'Zl69uXBSen0w', 1, null, 0, 0, '2017-12-23T04:02:39.164Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('t3vVElqMIQVa', 'h4OfLEAYspud', 'WdWZFuWNVDZk', 2, null, 1, 0, '2017-12-23T04:06:25.769Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('O983DHtLpgmr', '1hASbLRDL7oo', 'h4OfLEAYspud', 0, null, 0, 0, '2017-12-23T16:42:26.347Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RsvL795Mk1bp', '1hASbLRDL7oo', 'GpGnjmcAPeWG', 1, '', 0, 0, '2017-12-23T04:04:56.830Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('79e4hrHLFmx6', 'jyqG9GucsMdn', 'Iha4YwchR413', 1, null, 0, 0, '2017-12-23T04:05:16.439Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('oWO8rctUjf7d', 'WdWZFuWNVDZk', '1Heh2acXfPNt', 5, null, 1, 0, '2017-12-23T04:06:16.179Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GOxcrZrxalFN', 'yK4SBJfwD3tY', '1Heh2acXfPNt', 8, null, 1, 0, '2017-12-23T04:06:32.833Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bSPmEvjLzQKU', 'r4BnsmSQeVr1', 'yK4SBJfwD3tY', 0, null, 0, 0, '2017-12-23T04:06:37.427Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bMtxCD6cwNR9', 'QbL3pTvhgzM8', 'yK4SBJfwD3tY', 2, null, 0, 0, '2017-12-23T04:06:43.841Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('o4ycR7xIi4oI', 'moMbTKwN15Ps', 'yK4SBJfwD3tY', 3, null, 1, 0, '2017-12-23T04:06:49.331Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('abTEhnOsAsSg', 'PEGQGg0In3Ar', 'GpGnjmcAPeWG', 2, null, 0, 0, '2017-12-23T16:44:35.900Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bryQseMhyzaI', 'IlULcDiOTI4K', '1Heh2acXfPNt', 0, null, 0, 0, '2017-12-23T18:04:26.439Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('ccslPJf3wQV3', 'vBv6ovBupfTj', 'IlULcDiOTI4K', 0, null, 0, 0, '2017-12-23T18:04:50.904Z'); |  | ||||||
| INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('5Dt9YCMn59sY', 'mw4f2xB4J5fV', 'IlULcDiOTI4K', 1, null, 0, 0, '2017-12-23T18:05:24.868Z'); |  | ||||||
							
								
								
									
										38
									
								
								db/migrations/0079__rename_note_tree.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								db/migrations/0079__rename_note_tree.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | CREATE TABLE "branches" ( | ||||||
|  |   `branchId`	TEXT NOT NULL, | ||||||
|  |   `noteId`	TEXT NOT NULL, | ||||||
|  |   `parentNoteId`	TEXT NOT NULL, | ||||||
|  |   `notePosition`	INTEGER NOT NULL, | ||||||
|  |   `prefix`	TEXT, | ||||||
|  |   `isExpanded`	BOOLEAN, | ||||||
|  |   `isDeleted`	INTEGER NOT NULL DEFAULT 0, | ||||||
|  |   `dateModified`	TEXT NOT NULL, | ||||||
|  |   PRIMARY KEY(`branchId`) | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) | ||||||
|  |     SELECT noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified FROM note_tree; | ||||||
|  |  | ||||||
|  | DROP TABLE note_tree; | ||||||
|  |  | ||||||
|  | CREATE INDEX `IDX_branches_noteId` ON `branches` ( | ||||||
|  |   `noteId` | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` ( | ||||||
|  |   `noteId`, | ||||||
|  |   `parentNoteId` | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | CREATE TABLE `recent_notes_mig` ( | ||||||
|  |   `branchId` TEXT NOT NULL PRIMARY KEY, | ||||||
|  |   `notePath` TEXT NOT NULL, | ||||||
|  |   `dateAccessed` TEXT NOT NULL, | ||||||
|  |   isDeleted INT | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | INSERT INTO recent_notes_mig (branchId, notePath, dateAccessed, isDeleted) | ||||||
|  |     SELECT noteTreeId, notePath, dateAccessed, isDeleted FROM recent_notes; | ||||||
|  |  | ||||||
|  | DROP TABLE recent_notes; | ||||||
|  | ALTER TABLE recent_notes_mig RENAME TO recent_notes; | ||||||
							
								
								
									
										22
									
								
								db/migrations/0080__rename_attributes_to_labels.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								db/migrations/0080__rename_attributes_to_labels.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | create table labels | ||||||
|  | ( | ||||||
|  |   labelId  TEXT not null primary key, | ||||||
|  |   noteId       TEXT not null, | ||||||
|  |   name         TEXT not null, | ||||||
|  |   value        TEXT default '' not null, | ||||||
|  |   position     INT  default 0 not null, | ||||||
|  |   dateCreated  TEXT not null, | ||||||
|  |   dateModified TEXT not null, | ||||||
|  |   isDeleted    INT  not null | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | create index IDX_labels_name_value | ||||||
|  |   on labels (name, value); | ||||||
|  |  | ||||||
|  | create index IDX_labels_noteId | ||||||
|  |   on labels (noteId); | ||||||
|  |  | ||||||
|  | INSERT INTO labels (labelId, noteId, name, "value", "position", dateCreated, dateModified, isDeleted) | ||||||
|  |   SELECT attributeId, noteId, name, "value", "position", dateCreated, dateModified, isDeleted FROM attributes; | ||||||
|  |  | ||||||
|  | DROP TABLE attributes; | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | UPDATE options SET name = 'note_revision_snapshot_time_interval' WHERE name = 'history_snapshot_time_interval'; | ||||||
							
								
								
									
										14
									
								
								db/migrations/0082__camelCase_options.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								db/migrations/0082__camelCase_options.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | UPDATE "options" SET "name" = 'passwordVerificationHash' WHERE "name" = 'password_verification_hash'; | ||||||
|  | UPDATE "options" SET "name" = 'dbVersion' WHERE "name" = 'db_version'; | ||||||
|  | UPDATE "options" SET "name" = 'passwordDerivedKeySalt' WHERE "name" = 'password_derived_key_salt'; | ||||||
|  | UPDATE "options" SET "name" = 'documentId' WHERE "name" = 'document_id'; | ||||||
|  | UPDATE "options" SET "name" = 'lastSyncedPull' WHERE "name" = 'last_synced_pull'; | ||||||
|  | UPDATE "options" SET "name" = 'startNotePath' WHERE "name" = 'start_note_path'; | ||||||
|  | UPDATE "options" SET "name" = 'lastSyncedPush' WHERE "name" = 'last_synced_push'; | ||||||
|  | UPDATE "options" SET "name" = 'documentSecret' WHERE "name" = 'document_secret'; | ||||||
|  | UPDATE "options" SET "name" = 'lastBackupDate' WHERE "name" = 'last_backup_date'; | ||||||
|  | UPDATE "options" SET "name" = 'noteRevisionSnapshotTimeInterval' WHERE "name" = 'note_revision_snapshot_time_interval'; | ||||||
|  | UPDATE "options" SET "name" = 'protectedSessionTimeout' WHERE "name" = 'protected_session_timeout'; | ||||||
|  | UPDATE "options" SET "name" = 'encryptedDataKey' WHERE "name" = 'encrypted_data_key'; | ||||||
|  | UPDATE "options" SET "name" = 'encryptedDataKeyIv' WHERE "name" = 'encrypted_data_key_iv'; | ||||||
|  | UPDATE "options" SET "name" = 'passwordVerificationSalt' WHERE "name" = 'password_verification_salt'; | ||||||
							
								
								
									
										7
									
								
								db/migrations/0083__camelCase_labels.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								db/migrations/0083__camelCase_labels.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | UPDATE labels SET name = 'disableVersioning' WHERE name = 'disable_versioning'; | ||||||
|  | UPDATE labels SET name = 'calendarRoot' WHERE name = 'calendar_root'; | ||||||
|  | UPDATE labels SET name = 'hideInAutocomplete' WHERE name = 'hide_in_autocomplete'; | ||||||
|  | UPDATE labels SET name = 'excludeFromExport' WHERE name = 'exclude_from_export'; | ||||||
|  | UPDATE labels SET name = 'manualTransactionHandling' WHERE name = 'manual_transaction_handling'; | ||||||
|  | UPDATE labels SET name = 'disableInclusion' WHERE name = 'disable_inclusion'; | ||||||
|  | UPDATE labels SET name = 'appCss' WHERE name = 'app_css'; | ||||||
							
								
								
									
										4
									
								
								db/migrations/0084__camelCase_reddit_ids.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								db/migrations/0084__camelCase_reddit_ids.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | UPDATE labels SET name = 'redditId' WHERE name = 'reddit_id'; | ||||||
|  | UPDATE labels SET name = 'redditKind' WHERE name = 'reddit_kind'; | ||||||
|  | UPDATE labels SET name = 'redditCreatedUtc' WHERE name = 'reddit_created_utc'; | ||||||
|  | UPDATE labels SET name = 'redditDateNote' WHERE name = 'reddit_date_note'; | ||||||
							
								
								
									
										2
									
								
								db/migrations/0085__camelCase_run_values.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								db/migrations/0085__camelCase_run_values.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | UPDATE labels SET value = 'frontendStartup' WHERE value = 'frontend_startup'; | ||||||
|  | UPDATE labels SET value = 'backendStartup' WHERE value = 'backend_startup'; | ||||||
							
								
								
									
										7
									
								
								db/migrations/0086__camelCase_custom_labels.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								db/migrations/0086__camelCase_custom_labels.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | UPDATE labels SET name = 'dateData' WHERE name = 'date_data'; | ||||||
|  | UPDATE labels SET name = 'dateNote' WHERE name = 'date_note'; | ||||||
|  | UPDATE labels SET name = 'fileSize' WHERE name = 'file_size'; | ||||||
|  | UPDATE labels SET name = 'hideInAutocomplete' WHERE name = 'hide_in_autocomplete'; | ||||||
|  | UPDATE labels SET name = 'monthNote' WHERE name = 'month_note'; | ||||||
|  | UPDATE labels SET name = 'originalFileName' WHERE name = 'original_file_name'; | ||||||
|  | UPDATE labels SET name = 'yearNote' WHERE name = 'year_note'; | ||||||
							
								
								
									
										113
									
								
								db/schema.sql
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								db/schema.sql
									
									
									
									
									
								
							| @@ -9,6 +9,13 @@ CREATE TABLE IF NOT EXISTS "sync" ( | |||||||
|   `entityId`	TEXT NOT NULL, |   `entityId`	TEXT NOT NULL, | ||||||
|   `sourceId` TEXT NOT NULL, |   `sourceId` TEXT NOT NULL, | ||||||
|   `syncDate`	TEXT NOT NULL); |   `syncDate`	TEXT NOT NULL); | ||||||
|  | CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` ( | ||||||
|  |   `entityName`, | ||||||
|  |   `entityId` | ||||||
|  | ); | ||||||
|  | CREATE INDEX `IDX_sync_syncDate` ON `sync` ( | ||||||
|  |   `syncDate` | ||||||
|  | ); | ||||||
| CREATE TABLE IF NOT EXISTS "source_ids" ( | CREATE TABLE IF NOT EXISTS "source_ids" ( | ||||||
|   `sourceId`	TEXT NOT NULL, |   `sourceId`	TEXT NOT NULL, | ||||||
|   `dateCreated`	TEXT NOT NULL, |   `dateCreated`	TEXT NOT NULL, | ||||||
| @@ -26,6 +33,9 @@ CREATE TABLE IF NOT EXISTS "notes" ( | |||||||
|   mime TEXT NOT NULL DEFAULT 'text/html', |   mime TEXT NOT NULL DEFAULT 'text/html', | ||||||
|   PRIMARY KEY(`noteId`) |   PRIMARY KEY(`noteId`) | ||||||
| ); | ); | ||||||
|  | CREATE INDEX `IDX_notes_isDeleted` ON `notes` ( | ||||||
|  |   `isDeleted` | ||||||
|  | ); | ||||||
| CREATE TABLE IF NOT EXISTS "event_log" ( | CREATE TABLE IF NOT EXISTS "event_log" ( | ||||||
|   `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |   `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||||
|   `noteId`	TEXT, |   `noteId`	TEXT, | ||||||
| @@ -33,17 +43,6 @@ CREATE TABLE IF NOT EXISTS "event_log" ( | |||||||
|   `dateAdded`	TEXT NOT NULL, |   `dateAdded`	TEXT NOT NULL, | ||||||
|   FOREIGN KEY(noteId) REFERENCES notes(noteId) |   FOREIGN KEY(noteId) REFERENCES notes(noteId) | ||||||
| ); | ); | ||||||
| CREATE TABLE IF NOT EXISTS "note_tree" ( |  | ||||||
|   `noteTreeId`	TEXT NOT NULL, |  | ||||||
|   `noteId`	TEXT NOT NULL, |  | ||||||
|   `parentNoteId`	TEXT NOT NULL, |  | ||||||
|   `notePosition`	INTEGER NOT NULL, |  | ||||||
|   `prefix`	TEXT, |  | ||||||
|   `isExpanded`	BOOLEAN, |  | ||||||
|   `isDeleted`	INTEGER NOT NULL DEFAULT 0, |  | ||||||
|   `dateModified`	TEXT NOT NULL, |  | ||||||
|   PRIMARY KEY(`noteTreeId`) |  | ||||||
| ); |  | ||||||
| CREATE TABLE IF NOT EXISTS "note_revisions" ( | CREATE TABLE IF NOT EXISTS "note_revisions" ( | ||||||
|   `noteRevisionId`	TEXT NOT NULL PRIMARY KEY, |   `noteRevisionId`	TEXT NOT NULL PRIMARY KEY, | ||||||
|   `noteId`	TEXT NOT NULL, |   `noteId`	TEXT NOT NULL, | ||||||
| @@ -53,11 +52,14 @@ CREATE TABLE IF NOT EXISTS "note_revisions" ( | |||||||
|   `dateModifiedFrom` TEXT NOT NULL, |   `dateModifiedFrom` TEXT NOT NULL, | ||||||
|   `dateModifiedTo` TEXT NOT NULL |   `dateModifiedTo` TEXT NOT NULL | ||||||
| ); | ); | ||||||
| CREATE TABLE IF NOT EXISTS "recent_notes" ( | CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` ( | ||||||
|   `noteTreeId` TEXT NOT NULL PRIMARY KEY, |   `noteId` | ||||||
|   `notePath` TEXT NOT NULL, | ); | ||||||
|   `dateAccessed` TEXT NOT NULL, | CREATE INDEX `IDX_note_revisions_dateModifiedFrom` ON `note_revisions` ( | ||||||
|   isDeleted INT |   `dateModifiedFrom` | ||||||
|  | ); | ||||||
|  | CREATE INDEX `IDX_note_revisions_dateModifiedTo` ON `note_revisions` ( | ||||||
|  |   `dateModifiedTo` | ||||||
| ); | ); | ||||||
| CREATE TABLE IF NOT EXISTS "images" | CREATE TABLE IF NOT EXISTS "images" | ||||||
| ( | ( | ||||||
| @@ -79,49 +81,9 @@ CREATE TABLE note_images | |||||||
|   dateModified TEXT NOT NULL, |   dateModified TEXT NOT NULL, | ||||||
|   dateCreated TEXT NOT NULL |   dateCreated TEXT NOT NULL | ||||||
| ); | ); | ||||||
| CREATE TABLE IF NOT EXISTS "attributes" |  | ||||||
| ( |  | ||||||
|   attributeId TEXT PRIMARY KEY NOT NULL, |  | ||||||
|   noteId TEXT NOT NULL, |  | ||||||
|   name TEXT NOT NULL, |  | ||||||
|   value TEXT, |  | ||||||
|   position INT NOT NULL DEFAULT 0, |  | ||||||
|   dateCreated TEXT NOT NULL, |  | ||||||
|   dateModified TEXT NOT NULL, |  | ||||||
|   isDeleted INT NOT NULL |  | ||||||
| ); |  | ||||||
| CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` ( |  | ||||||
|   `entityName`, |  | ||||||
|   `entityId` |  | ||||||
| ); |  | ||||||
| CREATE INDEX `IDX_sync_syncDate` ON `sync` ( |  | ||||||
|   `syncDate` |  | ||||||
| ); |  | ||||||
| CREATE INDEX `IDX_notes_isDeleted` ON `notes` ( |  | ||||||
|   `isDeleted` |  | ||||||
| ); |  | ||||||
| CREATE INDEX `IDX_note_tree_noteId` ON `note_tree` ( |  | ||||||
|   `noteId` |  | ||||||
| ); |  | ||||||
| CREATE INDEX `IDX_note_tree_noteId_parentNoteId` ON `note_tree` ( |  | ||||||
|   `noteId`, |  | ||||||
|   `parentNoteId` |  | ||||||
| ); |  | ||||||
| CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` ( |  | ||||||
|   `noteId` |  | ||||||
| ); |  | ||||||
| CREATE INDEX `IDX_note_revisions_dateModifiedFrom` ON `note_revisions` ( |  | ||||||
|   `dateModifiedFrom` |  | ||||||
| ); |  | ||||||
| CREATE INDEX `IDX_note_revisions_dateModifiedTo` ON `note_revisions` ( |  | ||||||
|   `dateModifiedTo` |  | ||||||
| ); |  | ||||||
| CREATE INDEX IDX_note_images_noteId ON note_images (noteId); | 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_imageId ON note_images (imageId); | ||||||
| CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId); | CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId); | ||||||
| CREATE INDEX IDX_attributes_noteId ON attributes (noteId); |  | ||||||
| CREATE INDEX IDX_attributes_name_value ON attributes (name, value); |  | ||||||
|  |  | ||||||
| CREATE TABLE IF NOT EXISTS "api_tokens" | CREATE TABLE IF NOT EXISTS "api_tokens" | ||||||
| ( | ( | ||||||
|   apiTokenId TEXT PRIMARY KEY NOT NULL, |   apiTokenId TEXT PRIMARY KEY NOT NULL, | ||||||
| @@ -129,3 +91,42 @@ CREATE TABLE IF NOT EXISTS "api_tokens" | |||||||
|   dateCreated TEXT NOT NULL, |   dateCreated TEXT NOT NULL, | ||||||
|   isDeleted INT NOT NULL DEFAULT 0 |   isDeleted INT NOT NULL DEFAULT 0 | ||||||
| ); | ); | ||||||
|  | CREATE TABLE IF NOT EXISTS "branches" ( | ||||||
|  |   `branchId`	TEXT NOT NULL, | ||||||
|  |   `noteId`	TEXT NOT NULL, | ||||||
|  |   `parentNoteId`	TEXT NOT NULL, | ||||||
|  |   `notePosition`	INTEGER NOT NULL, | ||||||
|  |   `prefix`	TEXT, | ||||||
|  |   `isExpanded`	BOOLEAN, | ||||||
|  |   `isDeleted`	INTEGER NOT NULL DEFAULT 0, | ||||||
|  |   `dateModified`	TEXT NOT NULL, | ||||||
|  |   PRIMARY KEY(`branchId`) | ||||||
|  | ); | ||||||
|  | CREATE INDEX `IDX_branches_noteId` ON `branches` ( | ||||||
|  |   `noteId` | ||||||
|  | ); | ||||||
|  | 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, | ||||||
|  |   noteId       TEXT not null, | ||||||
|  |   name         TEXT not null, | ||||||
|  |   value        TEXT default '' not null, | ||||||
|  |   position     INT  default 0 not null, | ||||||
|  |   dateCreated  TEXT not null, | ||||||
|  |   dateModified TEXT not null, | ||||||
|  |   isDeleted    INT  not null | ||||||
|  | ); | ||||||
|  | CREATE INDEX IDX_labels_name_value | ||||||
|  |   on labels (name, value); | ||||||
|  | CREATE INDEX IDX_labels_noteId | ||||||
|  |   on labels (noteId); | ||||||
|   | |||||||
| @@ -73,15 +73,15 @@ app.on('ready', () => { | |||||||
|     mainWindow = createMainWindow(); |     mainWindow = createMainWindow(); | ||||||
|  |  | ||||||
|     const result = globalShortcut.register('CommandOrControl+Alt+P', async () => { |     const result = globalShortcut.register('CommandOrControl+Alt+P', async () => { | ||||||
|         const date_notes = require('./src/services/date_notes'); |         const dateNoteService = require('./src/services/date_notes'); | ||||||
|         const utils = require('./src/services/utils'); |         const dateUtils = require('./src/services/date_utils'); | ||||||
|  |  | ||||||
|         const parentNoteId = await date_notes.getDateNoteId(utils.nowDate()); |         const parentNote = await dateNoteService.getDateNote(dateUtils.nowDate()); | ||||||
|  |  | ||||||
|         // window may be hidden / not in focus |         // window may be hidden / not in focus | ||||||
|         mainWindow.focus(); |         mainWindow.focus(); | ||||||
|  |  | ||||||
|         mainWindow.webContents.send('create-day-sub-note', parentNoteId); |         mainWindow.webContents.send('create-day-sub-note', parentNote.noteId); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (!result) { |     if (!result) { | ||||||
|   | |||||||
							
								
								
									
										1475
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1475
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										41
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "description": "Trilium Notes", |   "description": "Trilium Notes", | ||||||
|   "version": "0.9.0-beta", |   "version": "0.10.2-beta", | ||||||
|   "license": "AGPL-3.0-only", |   "license": "AGPL-3.0-only", | ||||||
|   "main": "electron.js", |   "main": "electron.js", | ||||||
|   "repository": { |   "repository": { | ||||||
| @@ -12,8 +12,8 @@ | |||||||
|     "start": "node ./bin/www", |     "start": "node ./bin/www", | ||||||
|     "test-electron": "xo", |     "test-electron": "xo", | ||||||
|     "rebuild-electron": "electron-rebuild", |     "rebuild-electron": "electron-rebuild", | ||||||
|     "start-electron": "electron .", |     "start-electron": "electron . --disable-gpu", | ||||||
|     "build-electron": "electron-packager . --out=dist --asar --overwrite --all", |     "build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64", | ||||||
|     "start-forge": "electron-forge start", |     "start-forge": "electron-forge start", | ||||||
|     "package-forge": "electron-forge package", |     "package-forge": "electron-forge package", | ||||||
|     "make-forge": "electron-forge make", |     "make-forge": "electron-forge make", | ||||||
| @@ -23,50 +23,51 @@ | |||||||
|     "async-mutex": "^0.1.3", |     "async-mutex": "^0.1.3", | ||||||
|     "axios": "^0.17.1", |     "axios": "^0.17.1", | ||||||
|     "body-parser": "~1.18.2", |     "body-parser": "~1.18.2", | ||||||
|  |     "cls-hooked": "^4.2.2", | ||||||
|     "cookie-parser": "~1.4.3", |     "cookie-parser": "~1.4.3", | ||||||
|     "debug": "~3.1.0", |     "debug": "~3.1.0", | ||||||
|     "devtron": "^1.4.0", |     "devtron": "^1.4.0", | ||||||
|     "ejs": "~2.5.7", |     "ejs": "~2.5.7", | ||||||
|     "electron": "^1.8.2", |     "electron": "^2.0.0-beta.5", | ||||||
|     "electron-debug": "^1.5.0", |     "electron-debug": "^1.5.0", | ||||||
|     "electron-dl": "^1.11.0", |     "electron-dl": "^1.11.0", | ||||||
|     "electron-in-page-search": "^1.2.4", |     "electron-in-page-search": "^1.2.4", | ||||||
|     "express": "~4.16.2", |     "electron-rebuild": "^1.7.3", | ||||||
|     "express-promise-wrap": "^0.2.2", |     "express": "~4.16.3", | ||||||
|     "express-session": "^1.15.6", |     "express-session": "^1.15.6", | ||||||
|     "fs-extra": "^4.0.2", |     "fs-extra": "^4.0.3", | ||||||
|     "helmet": "^3.9.0", |     "helmet": "^3.12.0", | ||||||
|     "html": "^1.0.0", |     "html": "^1.0.0", | ||||||
|     "image-type": "^3.0.0", |     "image-type": "^3.0.0", | ||||||
|     "imagemin": "^5.3.1", |     "imagemin": "^5.3.1", | ||||||
|     "imagemin-giflossy": "^5.1.10", |     "imagemin-giflossy": "^5.1.10", | ||||||
|     "imagemin-mozjpeg": "^7.0.0", |     "imagemin-mozjpeg": "^7.0.0", | ||||||
|     "imagemin-pngquant": "^5.0.1", |     "imagemin-pngquant": "^5.1.0", | ||||||
|     "ini": "^1.3.4", |     "ini": "^1.3.5", | ||||||
|     "jimp": "^0.2.28", |     "jimp": "^0.2.28", | ||||||
|     "moment": "^2.20.1", |     "moment": "^2.21.0", | ||||||
|     "multer": "^1.3.0", |     "multer": "^1.3.0", | ||||||
|     "open": "0.0.5", |     "open": "0.0.5", | ||||||
|     "rand-token": "^0.4.0", |     "rand-token": "^0.4.0", | ||||||
|     "request": "^2.83.0", |     "request": "^2.85.0", | ||||||
|     "request-promise": "^4.2.2", |     "request-promise": "^4.2.2", | ||||||
|     "rimraf": "^2.6.2", |     "rimraf": "^2.6.2", | ||||||
|     "sanitize-filename": "^1.6.1", |     "sanitize-filename": "^1.6.1", | ||||||
|     "scrypt": "^6.0.3", |     "scrypt": "^6.0.3", | ||||||
|     "serve-favicon": "~2.4.5", |     "serve-favicon": "~2.4.5", | ||||||
|     "session-file-store": "^1.1.2", |     "session-file-store": "^1.2.0", | ||||||
|     "simple-node-logger": "^0.93.30", |     "simple-node-logger": "^0.93.37", | ||||||
|     "sqlite": "^2.9.0", |     "sqlite": "^2.9.1", | ||||||
|     "tar-stream": "^1.5.5", |     "tar-stream": "^1.5.5", | ||||||
|     "unescape": "^1.0.1", |     "unescape": "^1.0.1", | ||||||
|     "ws": "^3.3.2" |     "ws": "^3.3.3" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "electron-compile": "^6.4.2", |     "electron-compile": "^6.4.2", | ||||||
|     "electron-packager": "^11.0.1", |     "electron-packager": "^11.1.0", | ||||||
|     "electron-prebuilt-compile": "1.8.2", |     "electron-prebuilt-compile": "2.0.0-beta.5", | ||||||
|     "electron-rebuild": "^1.7.3", |     "lorem-ipsum": "^1.0.4", | ||||||
|     "tape": "^4.8.0", |     "tape": "^4.9.0", | ||||||
|     "xo": "^0.18.0" |     "xo": "^0.18.0" | ||||||
|   }, |   }, | ||||||
|   "config": { |   "config": { | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								src/app.js
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/app.js
									
									
									
									
									
								
							| @@ -9,6 +9,8 @@ const session = require('express-session'); | |||||||
| const FileStore = require('session-file-store')(session); | const FileStore = require('session-file-store')(session); | ||||||
| const os = require('os'); | const os = require('os'); | ||||||
| const sessionSecret = require('./services/session_secret'); | const sessionSecret = require('./services/session_secret'); | ||||||
|  | const cls = require('./services/cls'); | ||||||
|  | require('./entities/entity_constructor'); | ||||||
|  |  | ||||||
| const app = express(); | const app = express(); | ||||||
|  |  | ||||||
| @@ -23,6 +25,17 @@ app.use((req, res, next) => { | |||||||
|     next(); |     next(); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | app.use((req, res, next) => { | ||||||
|  |     cls.namespace.bindEmitter(req); | ||||||
|  |     cls.namespace.bindEmitter(res); | ||||||
|  |  | ||||||
|  |     cls.init(() => { | ||||||
|  |         cls.namespace.set("Hi"); | ||||||
|  |  | ||||||
|  |         next(); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  |  | ||||||
| app.use(bodyParser.json({limit: '50mb'})); | app.use(bodyParser.json({limit: '50mb'})); | ||||||
| app.use(bodyParser.urlencoded({extended: false})); | app.use(bodyParser.urlencoded({extended: false})); | ||||||
| app.use(cookieParser()); | app.use(cookieParser()); | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								src/entities/api_token.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/entities/api_token.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const Entity = require('./entity'); | ||||||
|  | const dateUtils = require('../services/date_utils'); | ||||||
|  |  | ||||||
|  | class ApiToken extends Entity { | ||||||
|  |     static get tableName() { return "api_tokens"; } | ||||||
|  |     static get primaryKeyName() { return "apiTokenId"; } | ||||||
|  |  | ||||||
|  |     beforeSaving() { | ||||||
|  |         super.beforeSaving(); | ||||||
|  |  | ||||||
|  |         if (!this.isDeleted) { | ||||||
|  |             this.isDeleted = false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.dateCreated) { | ||||||
|  |             this.dateCreated = dateUtils.nowDate(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = ApiToken; | ||||||
| @@ -1,14 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const Entity = require('./entity'); |  | ||||||
|  |  | ||||||
| class Attribute extends Entity { |  | ||||||
|     static get tableName() { return "attributes"; } |  | ||||||
|     static get primaryKeyName() { return "attributeId"; } |  | ||||||
|  |  | ||||||
|     async getNote() { |  | ||||||
|         return this.repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = Attribute; |  | ||||||
							
								
								
									
										32
									
								
								src/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const Entity = require('./entity'); | ||||||
|  | const dateUtils = require('../services/date_utils'); | ||||||
|  | const repository = require('../services/repository'); | ||||||
|  | const sql = require('../services/sql'); | ||||||
|  |  | ||||||
|  | class Branch extends Entity { | ||||||
|  |     static get tableName() { return "branches"; } | ||||||
|  |     static get primaryKeyName() { return "branchId"; } | ||||||
|  |  | ||||||
|  |     async getNote() { | ||||||
|  |         return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async beforeSaving() { | ||||||
|  |         super.beforeSaving(); | ||||||
|  |  | ||||||
|  |         if (this.notePosition === undefined) { | ||||||
|  |             const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]); | ||||||
|  |             this.notePosition = maxNotePos === null ? 0 : maxNotePos + 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.isDeleted) { | ||||||
|  |             this.isDeleted = false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.dateModified = dateUtils.nowDate() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = Branch; | ||||||
| @@ -1,17 +1,26 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const utils = require('../services/utils'); | const utils = require('../services/utils'); | ||||||
|  | const repository = require('../services/repository'); | ||||||
|  |  | ||||||
| class Entity { | class Entity { | ||||||
|     constructor(repository, row) { |     constructor(row = {}) { | ||||||
|         utils.assertArguments(repository, row); |  | ||||||
|  |  | ||||||
|         this.repository = repository; |  | ||||||
|  |  | ||||||
|         for (const key in row) { |         for (const key in row) { | ||||||
|             this[key] = row[key]; |             this[key] = row[key]; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     beforeSaving() { | ||||||
|  |         if (!this[this.constructor.primaryKeyName]) { | ||||||
|  |             this[this.constructor.primaryKeyName] = utils.newEntityId(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async save() { | ||||||
|  |         await repository.updateEntity(this); | ||||||
|  |  | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = Entity; | module.exports = Entity; | ||||||
							
								
								
									
										49
									
								
								src/entities/entity_constructor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/entities/entity_constructor.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | const Note = require('../entities/note'); | ||||||
|  | const NoteRevision = require('../entities/note_revision'); | ||||||
|  | const Image = require('../entities/image'); | ||||||
|  | const NoteImage = require('../entities/note_image'); | ||||||
|  | const Branch = require('../entities/branch'); | ||||||
|  | const Label = require('../entities/label'); | ||||||
|  | const RecentNote = require('../entities/recent_note'); | ||||||
|  | const ApiToken = require('../entities/api_token'); | ||||||
|  | const repository = require('../services/repository'); | ||||||
|  |  | ||||||
|  | function createEntityFromRow(row) { | ||||||
|  |     let entity; | ||||||
|  |  | ||||||
|  |     if (row.labelId) { | ||||||
|  |         entity = new Label(row); | ||||||
|  |     } | ||||||
|  |     else if (row.noteRevisionId) { | ||||||
|  |         entity = new NoteRevision(row); | ||||||
|  |     } | ||||||
|  |     else if (row.noteImageId) { | ||||||
|  |         entity = new NoteImage(row); | ||||||
|  |     } | ||||||
|  |     else if (row.imageId) { | ||||||
|  |         entity = new Image(row); | ||||||
|  |     } | ||||||
|  |     else if (row.branchId && row.notePath) { | ||||||
|  |         entity = new RecentNote(row); | ||||||
|  |     } | ||||||
|  |     else if (row.apiTokenId) { | ||||||
|  |         entity = new ApiToken(row); | ||||||
|  |     } | ||||||
|  |     else if (row.branchId) { | ||||||
|  |         entity = new Branch(row); | ||||||
|  |     } | ||||||
|  |     else if (row.noteId) { | ||||||
|  |         entity = new Note(row); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         throw new Error('Unknown entity type for row: ' + JSON.stringify(row)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return entity; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | repository.setEntityConstructor(createEntityFromRow); | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     createEntityFromRow | ||||||
|  | }; | ||||||
							
								
								
									
										25
									
								
								src/entities/image.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/entities/image.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const Entity = require('./entity'); | ||||||
|  | const dateUtils = require('../services/date_utils'); | ||||||
|  |  | ||||||
|  | class Image extends Entity { | ||||||
|  |     static get tableName() { return "images"; } | ||||||
|  |     static get primaryKeyName() { return "imageId"; } | ||||||
|  |  | ||||||
|  |     beforeSaving() { | ||||||
|  |         super.beforeSaving(); | ||||||
|  |  | ||||||
|  |         if (!this.isDeleted) { | ||||||
|  |             this.isDeleted = false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.dateCreated) { | ||||||
|  |             this.dateCreated = dateUtils.nowDate(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.dateModified = dateUtils.nowDate(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = Image; | ||||||
							
								
								
									
										40
									
								
								src/entities/label.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/entities/label.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const Entity = require('./entity'); | ||||||
|  | const repository = require('../services/repository'); | ||||||
|  | const dateUtils = require('../services/date_utils'); | ||||||
|  | const sql = require('../services/sql'); | ||||||
|  |  | ||||||
|  | class Label extends Entity { | ||||||
|  |     static get tableName() { return "labels"; } | ||||||
|  |     static get primaryKeyName() { return "labelId"; } | ||||||
|  |  | ||||||
|  |     async getNote() { | ||||||
|  |         return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async beforeSaving() { | ||||||
|  |         super.beforeSaving(); | ||||||
|  |  | ||||||
|  |         if (!this.value) { | ||||||
|  |             // null value isn't allowed | ||||||
|  |             this.value = ""; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this.position === undefined) { | ||||||
|  |             this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM labels WHERE noteId = ?`, [this.noteId]); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.isDeleted) { | ||||||
|  |             this.isDeleted = false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.dateCreated) { | ||||||
|  |             this.dateCreated = dateUtils.nowDate(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.dateModified = dateUtils.nowDate(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = Label; | ||||||
| @@ -2,16 +2,18 @@ | |||||||
|  |  | ||||||
| const Entity = require('./entity'); | const Entity = require('./entity'); | ||||||
| const protected_session = require('../services/protected_session'); | const protected_session = require('../services/protected_session'); | ||||||
|  | const repository = require('../services/repository'); | ||||||
|  | const dateUtils = require('../services/date_utils'); | ||||||
|  |  | ||||||
| class Note extends Entity { | class Note extends Entity { | ||||||
|     static get tableName() { return "notes"; } |     static get tableName() { return "notes"; } | ||||||
|     static get primaryKeyName() { return "noteId"; } |     static get primaryKeyName() { return "noteId"; } | ||||||
|  |  | ||||||
|     constructor(repository, row) { |     constructor(row) { | ||||||
|         super(repository, row); |         super(row); | ||||||
|  |  | ||||||
|         if (this.isProtected) { |         if (this.isProtected) { | ||||||
|             protected_session.decryptNote(this.dataKey, this); |             protected_session.decryptNote(this); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (this.isJson()) { |         if (this.isJson()) { | ||||||
| @@ -20,7 +22,7 @@ class Note extends Entity { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     isJson() { |     isJson() { | ||||||
|         return this.type === "code" && this.mime === "application/json"; |         return this.mime === "application/json"; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     isJavaScript() { |     isJavaScript() { | ||||||
| @@ -48,89 +50,106 @@ class Note extends Entity { | |||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getAttributes() { |     async getLabels() { | ||||||
|         return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]); |         return await repository.getEntities("SELECT * FROM labels WHERE noteId = ? AND isDeleted = 0", [this.noteId]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // WARNING: this doesn't take into account the possibility to have multi-valued attributes! |     // WARNING: this doesn't take into account the possibility to have multi-valued labels! | ||||||
|     async getAttributeMap() { |     async getLabelMap() { | ||||||
|         const map = {}; |         const map = {}; | ||||||
|  |  | ||||||
|         for (const attr of await this.getAttributes()) { |         for (const label of await this.getLabels()) { | ||||||
|             map[attr.name] = attr.value; |             map[label.name] = label.value; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return map; |         return map; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async hasAttribute(name) { |     async hasLabel(name) { | ||||||
|         const map = await this.getAttributeMap(); |         const map = await this.getLabelMap(); | ||||||
|  |  | ||||||
|         return map.hasOwnProperty(name); |         return map.hasOwnProperty(name); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // WARNING: this doesn't take into account the possibility to have multi-valued attributes! |     // WARNING: this doesn't take into account the possibility to have multi-valued labels! | ||||||
|     async getAttribute(name) { |     async getLabel(name) { | ||||||
|         return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]); |         return await repository.getEntity("SELECT * FROM labels WHERE noteId = ? AND name = ?", [this.noteId, name]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getRevisions() { |     async getRevisions() { | ||||||
|         return this.repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]); |         return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getTrees() { |     async getNoteImages() { | ||||||
|         return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]); |         return await repository.getEntities("SELECT * FROM note_images WHERE noteId = ? AND isDeleted = 0", [this.noteId]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getChild(name) { |     async getBranches() { | ||||||
|         return this.repository.getEntity(` |         return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getChildNote(name) { | ||||||
|  |         return await repository.getEntity(` | ||||||
|           SELECT notes.*  |           SELECT notes.*  | ||||||
|           FROM note_tree  |           FROM branches  | ||||||
|             JOIN notes USING(noteId)  |             JOIN notes USING(noteId)  | ||||||
|           WHERE notes.isDeleted = 0 |           WHERE notes.isDeleted = 0 | ||||||
|                 AND note_tree.isDeleted = 0 |                 AND branches.isDeleted = 0 | ||||||
|                 AND note_tree.parentNoteId = ? |                 AND branches.parentNoteId = ? | ||||||
|                 AND notes.title = ?`, [this.noteId, name]); |                 AND notes.title = ?`, [this.noteId, name]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getChildren() { |     async getChildNotes() { | ||||||
|         return this.repository.getEntities(` |         return await repository.getEntities(` | ||||||
|           SELECT notes.*  |           SELECT notes.*  | ||||||
|           FROM note_tree  |           FROM branches  | ||||||
|             JOIN notes USING(noteId)  |             JOIN notes USING(noteId)  | ||||||
|           WHERE notes.isDeleted = 0 |           WHERE notes.isDeleted = 0 | ||||||
|                 AND note_tree.isDeleted = 0 |                 AND branches.isDeleted = 0 | ||||||
|                 AND note_tree.parentNoteId = ? |                 AND branches.parentNoteId = ? | ||||||
|           ORDER BY note_tree.notePosition`, [this.noteId]); |           ORDER BY branches.notePosition`, [this.noteId]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getParents() { |     async getChildBranches() { | ||||||
|         return this.repository.getEntities(` |         return await repository.getEntities(` | ||||||
|  |           SELECT branches.*  | ||||||
|  |           FROM branches  | ||||||
|  |           WHERE branches.isDeleted = 0 | ||||||
|  |                 AND branches.parentNoteId = ? | ||||||
|  |           ORDER BY branches.notePosition`, [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getParentNotes() { | ||||||
|  |         return await repository.getEntities(` | ||||||
|           SELECT parent_notes.*  |           SELECT parent_notes.*  | ||||||
|           FROM  |           FROM  | ||||||
|             note_tree AS child_tree  |             branches AS child_tree  | ||||||
|             JOIN notes AS parent_notes ON parent_notes.noteId = child_tree.parentNoteId  |             JOIN notes AS parent_notes ON parent_notes.noteId = child_tree.parentNoteId  | ||||||
|           WHERE child_tree.noteId = ? |           WHERE child_tree.noteId = ? | ||||||
|                 AND child_tree.isDeleted = 0 |                 AND child_tree.isDeleted = 0 | ||||||
|                 AND parent_notes.isDeleted = 0`, [this.noteId]); |                 AND parent_notes.isDeleted = 0`, [this.noteId]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getNoteTree() { |  | ||||||
|         return this.repository.getEntities(` |  | ||||||
|           SELECT note_tree.*  |  | ||||||
|           FROM note_tree  |  | ||||||
|             JOIN notes USING(noteId)  |  | ||||||
|           WHERE notes.isDeleted = 0 |  | ||||||
|                 AND note_tree.isDeleted = 0 |  | ||||||
|                 AND note_tree.noteId = ?`, [this.noteId]); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     beforeSaving() { |     beforeSaving() { | ||||||
|  |         super.beforeSaving(); | ||||||
|  |  | ||||||
|  |         if (this.isJson()) { | ||||||
|             this.content = JSON.stringify(this.jsonContent, null, '\t'); |             this.content = JSON.stringify(this.jsonContent, null, '\t'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (this.isProtected) { |         if (this.isProtected) { | ||||||
|             protected_session.encryptNote(this.dataKey, this); |             protected_session.encryptNote(this); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (!this.isDeleted) { | ||||||
|  |             this.isDeleted = false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.dateCreated) { | ||||||
|  |             this.dateCreated = dateUtils.nowDate(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.dateModified = dateUtils.nowDate(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								src/entities/note_image.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/entities/note_image.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const Entity = require('./entity'); | ||||||
|  | const repository = require('../services/repository'); | ||||||
|  | const dateUtils = require('../services/date_utils'); | ||||||
|  |  | ||||||
|  | class NoteImage extends Entity { | ||||||
|  |     static get tableName() { return "note_images"; } | ||||||
|  |     static get primaryKeyName() { return "noteImageId"; } | ||||||
|  |  | ||||||
|  |     async getNote() { | ||||||
|  |         return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getImage() { | ||||||
|  |         return await repository.getEntity("SELECT * FROM images WHERE imageId = ?", [this.imageId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     beforeSaving() { | ||||||
|  |         super.beforeSaving(); | ||||||
|  |  | ||||||
|  |         if (!this.isDeleted) { | ||||||
|  |             this.isDeleted = false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.dateCreated) { | ||||||
|  |             this.dateCreated = dateUtils.nowDate(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.dateModified = dateUtils.nowDate(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = NoteImage; | ||||||
| @@ -1,13 +1,32 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const Entity = require('./entity'); | const Entity = require('./entity'); | ||||||
|  | const protected_session = require('../services/protected_session'); | ||||||
|  | const utils = require('../services/utils'); | ||||||
|  | const repository = require('../services/repository'); | ||||||
|  |  | ||||||
| class NoteRevision extends Entity { | class NoteRevision extends Entity { | ||||||
|     static get tableName() { return "note_revisions"; } |     static get tableName() { return "note_revisions"; } | ||||||
|     static get primaryKeyName() { return "noteRevisionId"; } |     static get primaryKeyName() { return "noteRevisionId"; } | ||||||
|  |  | ||||||
|  |     constructor(row) { | ||||||
|  |         super(row); | ||||||
|  |  | ||||||
|  |         if (this.isProtected) { | ||||||
|  |             protected_session.decryptNoteRevision(this); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async getNote() { |     async getNote() { | ||||||
|         return this.repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); |         return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     beforeSaving() { | ||||||
|  |         super.beforeSaving(); | ||||||
|  |  | ||||||
|  |         if (this.isProtected) { | ||||||
|  |             protected_session.encryptNoteRevision(this); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,18 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const Entity = require('./entity'); |  | ||||||
|  |  | ||||||
| class NoteTree extends Entity { |  | ||||||
|     static get tableName() { return "note_tree"; } |  | ||||||
|     static get primaryKeyName() { return "noteTreeId"; } |  | ||||||
|  |  | ||||||
|     async getNote() { |  | ||||||
|         return this.repository.getEntity("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async getParentNote() { |  | ||||||
|         return this.repository.getEntity("SELECT * FROM note_tree WHERE isDeleted = 0 AND parentNoteId = ?", [this.parentNoteId]); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = NoteTree; |  | ||||||
							
								
								
									
										10
									
								
								src/entities/recent_note.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/entities/recent_note.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const Entity = require('./entity'); | ||||||
|  |  | ||||||
|  | class RecentNote extends Entity { | ||||||
|  |     static get tableName() { return "recent_notes"; } | ||||||
|  |     static get primaryKeyName() { return "branchId"; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = RecentNote; | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/search-small.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/icons/search-small.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 354 B | 
| @@ -1,33 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const cloning = (function() { |  | ||||||
|     async function cloneNoteTo(childNoteId, parentNoteId, prefix) { |  | ||||||
|         const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, { |  | ||||||
|             prefix: prefix |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         if (!resp.success) { |  | ||||||
|             alert(resp.message); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await noteTree.reload(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // beware that first arg is noteId and second is noteTreeId! |  | ||||||
|     async function cloneNoteAfter(noteId, afterNoteTreeId) { |  | ||||||
|         const resp = await server.put('notes/' + noteId + '/clone-after/' + afterNoteTreeId); |  | ||||||
|  |  | ||||||
|         if (!resp.success) { |  | ||||||
|             alert(resp.message); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await noteTree.reload(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         cloneNoteAfter, |  | ||||||
|         cloneNoteTo |  | ||||||
|     }; |  | ||||||
| })(); |  | ||||||
| @@ -1,173 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const contextMenu = (function() { |  | ||||||
|     const $tree = $("#tree"); |  | ||||||
|  |  | ||||||
|     let clipboardIds = []; |  | ||||||
|     let clipboardMode = null; |  | ||||||
|  |  | ||||||
|     async function pasteAfter(node) { |  | ||||||
|         if (clipboardMode === 'cut') { |  | ||||||
|             const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey)); |  | ||||||
|  |  | ||||||
|             await treeChanges.moveAfterNode(nodes, node); |  | ||||||
|  |  | ||||||
|             clipboardIds = []; |  | ||||||
|             clipboardMode = null; |  | ||||||
|         } |  | ||||||
|         else if (clipboardMode === 'copy') { |  | ||||||
|             for (const noteId of clipboardIds) { |  | ||||||
|                 await cloning.cloneNoteAfter(noteId, node.data.noteTreeId); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places |  | ||||||
|         } |  | ||||||
|         else if (clipboardIds.length === 0) { |  | ||||||
|             // just do nothing |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             throwError("Unrecognized clipboard mode=" + clipboardMode); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function pasteInto(node) { |  | ||||||
|         if (clipboardMode === 'cut') { |  | ||||||
|             const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey)); |  | ||||||
|  |  | ||||||
|             await treeChanges.moveToNode(nodes, node); |  | ||||||
|  |  | ||||||
|             clipboardIds = []; |  | ||||||
|             clipboardMode = null; |  | ||||||
|         } |  | ||||||
|         else if (clipboardMode === 'copy') { |  | ||||||
|             for (const noteId of clipboardIds) { |  | ||||||
|                 await cloning.cloneNoteTo(noteId, node.data.noteId); |  | ||||||
|             } |  | ||||||
|             // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places |  | ||||||
|         } |  | ||||||
|         else if (clipboardIds.length === 0) { |  | ||||||
|             // just do nothing |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             throwError("Unrecognized clipboard mode=" + mode); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function copy(nodes) { |  | ||||||
|         clipboardIds = nodes.map(node => node.data.noteId); |  | ||||||
|         clipboardMode = 'copy'; |  | ||||||
|  |  | ||||||
|         showMessage("Note(s) have been copied into clipboard."); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function cut(nodes) { |  | ||||||
|         clipboardIds = nodes.map(node => node.key); |  | ||||||
|         clipboardMode = 'cut'; |  | ||||||
|  |  | ||||||
|         showMessage("Note(s) have been cut into clipboard."); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const contextMenuSettings = { |  | ||||||
|         delegate: "span.fancytree-title", |  | ||||||
|         autoFocus: true, |  | ||||||
|         menu: [ |  | ||||||
|             {title: "Insert note here <kbd>Ctrl+O</kbd>", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"}, |  | ||||||
|             {title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "ui-icon-plus"}, |  | ||||||
|             {title: "Delete <kbd>Ctrl+Del</kbd>", cmd: "delete", uiIcon: "ui-icon-trash"}, |  | ||||||
|             {title: "----"}, |  | ||||||
|             {title: "Edit tree prefix <kbd>F2</kbd>", cmd: "editTreePrefix", uiIcon: "ui-icon-pencil"}, |  | ||||||
|             {title: "----"}, |  | ||||||
|             {title: "Protect sub-tree", cmd: "protectSubTree", uiIcon: "ui-icon-locked"}, |  | ||||||
|             {title: "Unprotect sub-tree", cmd: "unprotectSubTree", uiIcon: "ui-icon-unlocked"}, |  | ||||||
|             {title: "----"}, |  | ||||||
|             {title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "ui-icon-copy"}, |  | ||||||
|             {title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "ui-icon-scissors"}, |  | ||||||
|             {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 sub-tree", cmd: "exportSubTree", uiIcon: " ui-icon-arrowthick-1-ne"}, |  | ||||||
|             {title: "Import sub-tree into", cmd: "importSubTree", uiIcon: "ui-icon-arrowthick-1-sw"}, |  | ||||||
|             {title: "----"}, |  | ||||||
|             {title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapseSubTree", 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: (event, ui) => { |  | ||||||
|             const node = $.ui.fancytree.getNode(ui.target); |  | ||||||
|             // Modify menu entries depending on node status |  | ||||||
|             $tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0); |  | ||||||
|             $tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0); |  | ||||||
|  |  | ||||||
|             // Activate node on right-click |  | ||||||
|             node.setActive(); |  | ||||||
|             // Disable tree keyboard handling |  | ||||||
|             ui.menu.prevKeyboard = node.tree.options.keyboard; |  | ||||||
|             node.tree.options.keyboard = false; |  | ||||||
|         }, |  | ||||||
|         close: (event, ui) => {}, |  | ||||||
|         select: (event, ui) => { |  | ||||||
|             const node = $.ui.fancytree.getNode(ui.target); |  | ||||||
|  |  | ||||||
|             if (ui.cmd === "insertNoteHere") { |  | ||||||
|                 const parentNoteId = node.data.parentNoteId; |  | ||||||
|                 const isProtected = treeUtils.getParentProtectedStatus(node); |  | ||||||
|  |  | ||||||
|                 noteTree.createNote(node, parentNoteId, 'after', isProtected); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "insertChildNote") { |  | ||||||
|                 noteTree.createNote(node, node.data.noteId, 'into'); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "editTreePrefix") { |  | ||||||
|                 editTreePrefix.showDialog(node); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "protectSubTree") { |  | ||||||
|                 protected_session.protectSubTree(node.data.noteId, true); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "unprotectSubTree") { |  | ||||||
|                 protected_session.protectSubTree(node.data.noteId, false); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "copy") { |  | ||||||
|                 copy(noteTree.getSelectedNodes()); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "cut") { |  | ||||||
|                 cut(noteTree.getSelectedNodes()); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "pasteAfter") { |  | ||||||
|                 pasteAfter(node); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "pasteInto") { |  | ||||||
|                 pasteInto(node); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "delete") { |  | ||||||
|                 treeChanges.deleteNodes(noteTree.getSelectedNodes(true)); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "exportSubTree") { |  | ||||||
|                 exportSubTree(node.data.noteId); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "importSubTree") { |  | ||||||
|                 importSubTree(node.data.noteId); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "collapseSubTree") { |  | ||||||
|                 noteTree.collapseTree(node); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "forceNoteSync") { |  | ||||||
|                 forceNoteSync(node.data.noteId); |  | ||||||
|             } |  | ||||||
|             else if (ui.cmd === "sortAlphabetically") { |  | ||||||
|                 noteTree.sortAlphabetically(node.data.noteId); |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 messaging.logError("Unknown command: " + ui.cmd); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         pasteAfter, |  | ||||||
|         pasteInto, |  | ||||||
|         cut, |  | ||||||
|         copy, |  | ||||||
|         contextMenuSettings |  | ||||||
|     } |  | ||||||
| })(); |  | ||||||
| @@ -1,6 +1,9 @@ | |||||||
| "use strict"; | 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'; | ||||||
|  |  | ||||||
| const addLink = (function() { |  | ||||||
| const $dialog = $("#add-link-dialog"); | const $dialog = $("#add-link-dialog"); | ||||||
| const $form = $("#add-link-form"); | const $form = $("#add-link-form"); | ||||||
| const $autoComplete = $("#note-autocomplete"); | const $autoComplete = $("#note-autocomplete"); | ||||||
| @@ -19,10 +22,10 @@ const addLink = (function() { | |||||||
|     linkTypeChanged(); |     linkTypeChanged(); | ||||||
| } | } | ||||||
|  |  | ||||||
|     function showDialog() { | async function showDialog() { | ||||||
|     glob.activeDialog = $dialog; |     glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         if (noteEditor.getCurrentNoteType() === 'text') { |     if (noteDetailService.getCurrentNoteType() === 'text') { | ||||||
|         $linkTypeHtml.prop('disabled', false); |         $linkTypeHtml.prop('disabled', false); | ||||||
|  |  | ||||||
|         setLinkType('html'); |         setLinkType('html'); | ||||||
| @@ -43,17 +46,17 @@ const addLink = (function() { | |||||||
|     $linkTitle.val(''); |     $linkTitle.val(''); | ||||||
|  |  | ||||||
|     function setDefaultLinkTitle(noteId) { |     function setDefaultLinkTitle(noteId) { | ||||||
|             const noteTitle = noteTree.getNoteTitle(noteId); |         const noteTitle = treeUtils.getNoteTitle(noteId); | ||||||
|  |  | ||||||
|         $linkTitle.val(noteTitle); |         $linkTitle.val(noteTitle); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $autoComplete.autocomplete({ |     $autoComplete.autocomplete({ | ||||||
|             source: noteTree.getAutocompleteItems(), |         source: await autocompleteService.getAutocompleteItems(), | ||||||
|         minLength: 0, |         minLength: 0, | ||||||
|         change: () => { |         change: () => { | ||||||
|             const val = $autoComplete.val(); |             const val = $autoComplete.val(); | ||||||
|                 const notePath = link.getNodePathFromLabel(val); |             const notePath = linkService.getNodePathFromLabel(val); | ||||||
|             if (!notePath) { |             if (!notePath) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| @@ -67,7 +70,7 @@ const addLink = (function() { | |||||||
|         // this is called when user goes through autocomplete list with keyboard |         // 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 |         // at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is | ||||||
|         focus: (event, ui) => { |         focus: (event, ui) => { | ||||||
|                 const notePath = link.getNodePathFromLabel(ui.item.value); |             const notePath = linkService.getNodePathFromLabel(ui.item.value); | ||||||
|             const noteId = treeUtils.getNoteIdFromNotePath(notePath); |             const noteId = treeUtils.getNoteIdFromNotePath(notePath); | ||||||
|  |  | ||||||
|             setDefaultLinkTitle(noteId); |             setDefaultLinkTitle(noteId); | ||||||
| @@ -78,7 +81,7 @@ const addLink = (function() { | |||||||
| $form.submit(() => { | $form.submit(() => { | ||||||
|     const value = $autoComplete.val(); |     const value = $autoComplete.val(); | ||||||
|  |  | ||||||
|         const notePath = link.getNodePathFromLabel(value); |     const notePath = linkService.getNodePathFromLabel(value); | ||||||
|     const noteId = treeUtils.getNoteIdFromNotePath(notePath); |     const noteId = treeUtils.getNoteIdFromNotePath(notePath); | ||||||
|  |  | ||||||
|     if (notePath) { |     if (notePath) { | ||||||
| @@ -89,19 +92,19 @@ const addLink = (function() { | |||||||
|  |  | ||||||
|             $dialog.dialog("close"); |             $dialog.dialog("close"); | ||||||
|  |  | ||||||
|                 link.addLinkToEditor(linkTitle, '#' + notePath); |             linkService.addLinkToEditor(linkTitle, '#' + notePath); | ||||||
|         } |         } | ||||||
|         else if (linkType === 'selected-to-current') { |         else if (linkType === 'selected-to-current') { | ||||||
|             const prefix = $clonePrefix.val(); |             const prefix = $clonePrefix.val(); | ||||||
|  |  | ||||||
|                 cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix); |             cloningService.cloneNoteTo(noteId, noteDetailService.getCurrentNoteId(), prefix); | ||||||
|  |  | ||||||
|             $dialog.dialog("close"); |             $dialog.dialog("close"); | ||||||
|         } |         } | ||||||
|         else if (linkType === 'current-to-selected') { |         else if (linkType === 'current-to-selected') { | ||||||
|             const prefix = $clonePrefix.val(); |             const prefix = $clonePrefix.val(); | ||||||
|  |  | ||||||
|                 cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix); |             cloningService.cloneNoteTo(noteDetailService.getCurrentNoteId(), noteId, prefix); | ||||||
|  |  | ||||||
|             $dialog.dialog("close"); |             $dialog.dialog("close"); | ||||||
|         } |         } | ||||||
| @@ -125,13 +128,6 @@ const addLink = (function() { | |||||||
|  |  | ||||||
| $linkTypes.change(linkTypeChanged); | $linkTypes.change(linkTypeChanged); | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+l', e => { | export default { | ||||||
|         showDialog(); |  | ||||||
|  |  | ||||||
|         e.preventDefault(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|     showDialog |     showDialog | ||||||
| }; | }; | ||||||
| })(); |  | ||||||
| @@ -1,224 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const attributesDialog = (function() { |  | ||||||
|     const $dialog = $("#attributes-dialog"); |  | ||||||
|     const $saveAttributesButton = $("#save-attributes-button"); |  | ||||||
|     const $attributesBody = $('#attributes-table tbody'); |  | ||||||
|  |  | ||||||
|     const attributesModel = new AttributesModel(); |  | ||||||
|     let attributeNames = []; |  | ||||||
|  |  | ||||||
|     function AttributesModel() { |  | ||||||
|         const self = this; |  | ||||||
|  |  | ||||||
|         this.attributes = ko.observableArray(); |  | ||||||
|  |  | ||||||
|         this.loadAttributes = async function() { |  | ||||||
|             const noteId = noteEditor.getCurrentNoteId(); |  | ||||||
|  |  | ||||||
|             const attributes = await server.get('notes/' + noteId + '/attributes'); |  | ||||||
|  |  | ||||||
|             self.attributes(attributes.map(ko.observable)); |  | ||||||
|  |  | ||||||
|             addLastEmptyRow(); |  | ||||||
|  |  | ||||||
|             attributeNames = await server.get('attributes/names'); |  | ||||||
|  |  | ||||||
|             // attribute might not be rendered immediatelly so could not focus |  | ||||||
|             setTimeout(() => $(".attribute-name:last").focus(), 100); |  | ||||||
|  |  | ||||||
|             $attributesBody.sortable({ |  | ||||||
|                 handle: '.handle', |  | ||||||
|                 containment: $attributesBody, |  | ||||||
|                 update: function() { |  | ||||||
|                     let position = 0; |  | ||||||
|  |  | ||||||
|                     // we need to update positions by searching in the DOM, because order of the |  | ||||||
|                     // attributes in the viewmodel (self.attributes()) stays the same |  | ||||||
|                     $attributesBody.find('input[name="position"]').each(function() { |  | ||||||
|                         const attr = self.getTargetAttribute(this); |  | ||||||
|  |  | ||||||
|                         attr().position = position++; |  | ||||||
|                     }); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         this.deleteAttribute = function(data, event) { |  | ||||||
|             const attr = self.getTargetAttribute(event.target); |  | ||||||
|             const attrData = attr(); |  | ||||||
|  |  | ||||||
|             if (attrData) { |  | ||||||
|                 attrData.isDeleted = 1; |  | ||||||
|  |  | ||||||
|                 attr(attrData); |  | ||||||
|  |  | ||||||
|                 addLastEmptyRow(); |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         function isValid() { |  | ||||||
|             for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) { |  | ||||||
|                 if (self.isEmptyName(i)) { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         this.save = async function() { |  | ||||||
|             // we need to defocus from input (in case of enter-triggered save) because value is updated |  | ||||||
|             // on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would |  | ||||||
|             // stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel. |  | ||||||
|             $saveAttributesButton.focus(); |  | ||||||
|  |  | ||||||
|             if (!isValid()) { |  | ||||||
|                 alert("Please fix all validation errors and try saving again."); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             const noteId = noteEditor.getCurrentNoteId(); |  | ||||||
|  |  | ||||||
|             const attributesToSave = self.attributes() |  | ||||||
|                 .map(attr => attr()) |  | ||||||
|                 .filter(attr => attr.attributeId !== "" || attr.name !== ""); |  | ||||||
|  |  | ||||||
|             const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave); |  | ||||||
|  |  | ||||||
|             self.attributes(attributes.map(ko.observable)); |  | ||||||
|  |  | ||||||
|             addLastEmptyRow(); |  | ||||||
|  |  | ||||||
|             showMessage("Attributes have been saved."); |  | ||||||
|  |  | ||||||
|             noteEditor.loadAttributeList(); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         function addLastEmptyRow() { |  | ||||||
|             const attrs = self.attributes().filter(attr => attr().isDeleted === 0); |  | ||||||
|             const last = attrs.length === 0 ? null : attrs[attrs.length - 1](); |  | ||||||
|  |  | ||||||
|             if (!last || last.name.trim() !== "" || last.value !== "") { |  | ||||||
|                 self.attributes.push(ko.observable({ |  | ||||||
|                     attributeId: '', |  | ||||||
|                     name: '', |  | ||||||
|                     value: '', |  | ||||||
|                     isDeleted: 0, |  | ||||||
|                     position: 0 |  | ||||||
|                 })); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         this.attributeChanged = function (data, event) { |  | ||||||
|             addLastEmptyRow(); |  | ||||||
|  |  | ||||||
|             const attr = self.getTargetAttribute(event.target); |  | ||||||
|  |  | ||||||
|             attr.valueHasMutated(); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         this.isNotUnique = function(index) { |  | ||||||
|             const cur = self.attributes()[index](); |  | ||||||
|  |  | ||||||
|             if (cur.name.trim() === "") { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) { |  | ||||||
|                 const attr = attrs[i](); |  | ||||||
|  |  | ||||||
|                 if (index !== i && cur.name === attr.name) { |  | ||||||
|                     return true; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             return false; |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         this.isEmptyName = function(index) { |  | ||||||
|             const cur = self.attributes()[index](); |  | ||||||
|  |  | ||||||
|             return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== ""); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         this.getTargetAttribute = function(target) { |  | ||||||
|             const context = ko.contextFor(target); |  | ||||||
|             const index = context.$index(); |  | ||||||
|  |  | ||||||
|             return self.attributes()[index]; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function showDialog() { |  | ||||||
|         glob.activeDialog = $dialog; |  | ||||||
|  |  | ||||||
|         await attributesModel.loadAttributes(); |  | ||||||
|  |  | ||||||
|         $dialog.dialog({ |  | ||||||
|             modal: true, |  | ||||||
|             width: 800, |  | ||||||
|             height: 500 |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'alt+a', e => { |  | ||||||
|         showDialog(); |  | ||||||
|  |  | ||||||
|         e.preventDefault(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     ko.applyBindings(attributesModel, document.getElementById('attributes-dialog')); |  | ||||||
|  |  | ||||||
|     $(document).on('focus', '.attribute-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 init.js |  | ||||||
|                 source: attributeNames.map(attr => { |  | ||||||
|                     return { |  | ||||||
|                         label: attr, |  | ||||||
|                         value: attr |  | ||||||
|                     } |  | ||||||
|                 }), |  | ||||||
|                 minLength: 0 |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         $(this).autocomplete("search", $(this).val()); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     $(document).on('focus', '.attribute-value', async function (e) { |  | ||||||
|         if (!$(this).hasClass("ui-autocomplete-input")) { |  | ||||||
|             const attributeName = $(this).parent().parent().find('.attribute-name').val(); |  | ||||||
|  |  | ||||||
|             if (attributeName.trim() === "") { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName)); |  | ||||||
|  |  | ||||||
|             if (attributeValues.length === 0) { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             $(this).autocomplete({ |  | ||||||
|                 // shouldn't be required and autocomplete should just accept array of strings, but that fails |  | ||||||
|                 // because we have overriden filter() function in init.js |  | ||||||
|                 source: attributeValues.map(attr => { |  | ||||||
|                     return { |  | ||||||
|                         label: attr, |  | ||||||
|                         value: attr |  | ||||||
|                     } |  | ||||||
|                 }), |  | ||||||
|                 minLength: 0 |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         $(this).autocomplete("search", $(this).val()); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         showDialog |  | ||||||
|     }; |  | ||||||
| })(); |  | ||||||
							
								
								
									
										51
									
								
								src/public/javascripts/dialogs/branch_prefix.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/public/javascripts/dialogs/branch_prefix.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | import treeService from '../services/tree.js'; | ||||||
|  | import server from '../services/server.js'; | ||||||
|  | import treeCache from "../services/tree_cache.js"; | ||||||
|  | import treeUtils from "../services/tree_utils.js"; | ||||||
|  |  | ||||||
|  | const $dialog = $("#edit-tree-prefix-dialog"); | ||||||
|  | const $form = $("#edit-tree-prefix-form"); | ||||||
|  | const $treePrefixInput = $("#tree-prefix-input"); | ||||||
|  | const $noteTitle = $('#tree-prefix-note-title'); | ||||||
|  |  | ||||||
|  | let branchId; | ||||||
|  |  | ||||||
|  | async function showDialog() { | ||||||
|  |     glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|  |     await $dialog.dialog({ | ||||||
|  |         modal: true, | ||||||
|  |         width: 500 | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const currentNode = treeService.getCurrentNode(); | ||||||
|  |  | ||||||
|  |     branchId = currentNode.data.branchId; | ||||||
|  |     const branch = await treeCache.getBranch(branchId); | ||||||
|  |  | ||||||
|  |     $treePrefixInput.val(branch.prefix).focus(); | ||||||
|  |  | ||||||
|  |     const noteTitle = treeUtils.getNoteTitle(currentNode.data.noteId); | ||||||
|  |  | ||||||
|  |     $noteTitle.html(noteTitle); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function savePrefix() { | ||||||
|  |     const prefix = $treePrefixInput.val(); | ||||||
|  |  | ||||||
|  |     await server.put('branches/' + branchId + '/set-prefix', { prefix: prefix }); | ||||||
|  |  | ||||||
|  |     await treeService.setPrefix(branchId, prefix); | ||||||
|  |  | ||||||
|  |     $dialog.dialog("close"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $form.submit(() => { | ||||||
|  |     savePrefix(); | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     showDialog | ||||||
|  | }; | ||||||
| @@ -1,45 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const editTreePrefix = (function() { |  | ||||||
|     const $dialog = $("#edit-tree-prefix-dialog"); |  | ||||||
|     const $form = $("#edit-tree-prefix-form"); |  | ||||||
|     const $treePrefixInput = $("#tree-prefix-input"); |  | ||||||
|     const $noteTitle = $('#tree-prefix-note-title'); |  | ||||||
|  |  | ||||||
|     let noteTreeId; |  | ||||||
|  |  | ||||||
|     async function showDialog() { |  | ||||||
|         glob.activeDialog = $dialog; |  | ||||||
|  |  | ||||||
|         await $dialog.dialog({ |  | ||||||
|             modal: true, |  | ||||||
|             width: 500 |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         const currentNode = noteTree.getCurrentNode(); |  | ||||||
|  |  | ||||||
|         noteTreeId = currentNode.data.noteTreeId; |  | ||||||
|  |  | ||||||
|         $treePrefixInput.val(currentNode.data.prefix).focus(); |  | ||||||
|  |  | ||||||
|         const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId); |  | ||||||
|  |  | ||||||
|         $noteTitle.html(noteTitle); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $form.submit(() => { |  | ||||||
|         const prefix = $treePrefixInput.val(); |  | ||||||
|  |  | ||||||
|         server.put('tree/' + noteTreeId + '/set-prefix', { |  | ||||||
|             prefix: prefix |  | ||||||
|         }).then(() => noteTree.setPrefix(noteTreeId, prefix)); |  | ||||||
|  |  | ||||||
|         $dialog.dialog("close"); |  | ||||||
|  |  | ||||||
|         return false; |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         showDialog |  | ||||||
|     }; |  | ||||||
| })(); |  | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| "use strict"; | import linkService from '../services/link.js'; | ||||||
|  | import utils from '../services/utils.js'; | ||||||
|  | import server from '../services/server.js'; | ||||||
|  |  | ||||||
| const eventLog = (function() { |  | ||||||
| const $dialog = $("#event-log-dialog"); | const $dialog = $("#event-log-dialog"); | ||||||
| const $list = $("#event-log-list"); | const $list = $("#event-log-list"); | ||||||
|  |  | ||||||
| @@ -18,10 +19,10 @@ const eventLog = (function() { | |||||||
|     $list.html(''); |     $list.html(''); | ||||||
|  |  | ||||||
|     for (const event of result) { |     for (const event of result) { | ||||||
|             const dateTime = formatDateTime(parseDate(event.dateAdded)); |         const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded)); | ||||||
|  |  | ||||||
|         if (event.noteId) { |         if (event.noteId) { | ||||||
|                 const noteLink = link.createNoteLink(event.noteId).prop('outerHTML'); |             const noteLink = linkService.createNoteLink(event.noteId).prop('outerHTML'); | ||||||
|  |  | ||||||
|             event.comment = event.comment.replace('<note>', noteLink); |             event.comment = event.comment.replace('<note>', noteLink); | ||||||
|         } |         } | ||||||
| @@ -32,7 +33,6 @@ const eventLog = (function() { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|     return { | export default { | ||||||
|     showDialog |     showDialog | ||||||
| }; | }; | ||||||
| })(); |  | ||||||
| @@ -1,6 +1,8 @@ | |||||||
| "use strict"; | import treeService from '../services/tree.js'; | ||||||
|  | import linkService from '../services/link.js'; | ||||||
|  | import utils from '../services/utils.js'; | ||||||
|  | import autocompleteService from '../services/autocomplete.js'; | ||||||
|  |  | ||||||
| const jumpToNote = (function() { |  | ||||||
| const $dialog = $("#jump-to-note-dialog"); | const $dialog = $("#jump-to-note-dialog"); | ||||||
| const $autoComplete = $("#jump-to-note-autocomplete"); | const $autoComplete = $("#jump-to-note-autocomplete"); | ||||||
| const $form = $("#jump-to-note-form"); | const $form = $("#jump-to-note-form"); | ||||||
| @@ -16,41 +18,32 @@ const jumpToNote = (function() { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     await $autoComplete.autocomplete({ |     await $autoComplete.autocomplete({ | ||||||
|             source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems), |         source: await utils.stopWatch("building autocomplete", autocompleteService.getAutocompleteItems), | ||||||
|             minLength: 0 |         minLength: 1 | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function getSelectedNotePath() { | function getSelectedNotePath() { | ||||||
|     const val = $autoComplete.val(); |     const val = $autoComplete.val(); | ||||||
|         return link.getNodePathFromLabel(val); |     return linkService.getNodePathFromLabel(val); | ||||||
| } | } | ||||||
|  |  | ||||||
| function goToNote() { | function goToNote() { | ||||||
|     const notePath = getSelectedNotePath(); |     const notePath = getSelectedNotePath(); | ||||||
|  |  | ||||||
|     if (notePath) { |     if (notePath) { | ||||||
|             noteTree.activateNode(notePath); |         treeService.activateNode(notePath); | ||||||
|  |  | ||||||
|         $dialog.dialog('close'); |         $dialog.dialog('close'); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+j', e => { |  | ||||||
|         showDialog(); |  | ||||||
|  |  | ||||||
|         e.preventDefault(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
| $form.submit(() => { | $form.submit(() => { | ||||||
|         const action = $dialog.find("button:focus").val(); |  | ||||||
|  |  | ||||||
|     goToNote(); |     goToNote(); | ||||||
|  |  | ||||||
|     return false; |     return false; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|     return { | export default { | ||||||
|     showDialog |     showDialog | ||||||
| }; | }; | ||||||
| })(); |  | ||||||
							
								
								
									
										223
									
								
								src/public/javascripts/dialogs/labels.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src/public/javascripts/dialogs/labels.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | |||||||
|  | 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"; | ||||||
|  |  | ||||||
|  | const $dialog = $("#labels-dialog"); | ||||||
|  | const $saveLabelsButton = $("#save-labels-button"); | ||||||
|  | const $labelsBody = $('#labels-table tbody'); | ||||||
|  |  | ||||||
|  | const labelsModel = new LabelsModel(); | ||||||
|  | let labelNames = []; | ||||||
|  |  | ||||||
|  | function LabelsModel() { | ||||||
|  |     const self = this; | ||||||
|  |  | ||||||
|  |     this.labels = ko.observableArray(); | ||||||
|  |  | ||||||
|  |     this.updateLabelPositions = function() { | ||||||
|  |         let position = 0; | ||||||
|  |  | ||||||
|  |         // we need to update positions by searching in the DOM, because order of the | ||||||
|  |         // labels in the viewmodel (self.labels()) stays the same | ||||||
|  |         $labelsBody.find('input[name="position"]').each(function() { | ||||||
|  |             const label = self.getTargetLabel(this); | ||||||
|  |  | ||||||
|  |             label().position = position++; | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.loadLabels = async function() { | ||||||
|  |         const noteId = noteDetailService.getCurrentNoteId(); | ||||||
|  |  | ||||||
|  |         const labels = await server.get('notes/' + noteId + '/labels'); | ||||||
|  |  | ||||||
|  |         self.labels(labels.map(ko.observable)); | ||||||
|  |  | ||||||
|  |         addLastEmptyRow(); | ||||||
|  |  | ||||||
|  |         labelNames = await server.get('labels/names'); | ||||||
|  |  | ||||||
|  |         // label might not be rendered immediatelly so could not focus | ||||||
|  |         setTimeout(() => $(".label-name:last").focus(), 100); | ||||||
|  |  | ||||||
|  |         $labelsBody.sortable({ | ||||||
|  |             handle: '.handle', | ||||||
|  |             containment: $labelsBody, | ||||||
|  |             update: this.updateLabelPositions | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.deleteLabel = function(data, event) { | ||||||
|  |         const label = self.getTargetLabel(event.target); | ||||||
|  |         const labelData = label(); | ||||||
|  |  | ||||||
|  |         if (labelData) { | ||||||
|  |             labelData.isDeleted = 1; | ||||||
|  |  | ||||||
|  |             label(labelData); | ||||||
|  |  | ||||||
|  |             addLastEmptyRow(); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     function isValid() { | ||||||
|  |         for (let labels = self.labels(), i = 0; i < labels.length; i++) { | ||||||
|  |             if (self.isEmptyName(i)) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.save = async function() { | ||||||
|  |         // we need to defocus from input (in case of enter-triggered save) because value is updated | ||||||
|  |         // on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would | ||||||
|  |         // stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel. | ||||||
|  |         $saveLabelsButton.focus(); | ||||||
|  |  | ||||||
|  |         if (!isValid()) { | ||||||
|  |             alert("Please fix all validation errors and try saving again."); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         self.updateLabelPositions(); | ||||||
|  |  | ||||||
|  |         const noteId = noteDetailService.getCurrentNoteId(); | ||||||
|  |  | ||||||
|  |         const labelsToSave = self.labels() | ||||||
|  |             .map(label => label()) | ||||||
|  |             .filter(label => label.labelId !== "" || label.name !== ""); | ||||||
|  |  | ||||||
|  |         const labels = await server.put('notes/' + noteId + '/labels', labelsToSave); | ||||||
|  |  | ||||||
|  |         self.labels(labels.map(ko.observable)); | ||||||
|  |  | ||||||
|  |         addLastEmptyRow(); | ||||||
|  |  | ||||||
|  |         infoService.showMessage("Labels have been saved."); | ||||||
|  |  | ||||||
|  |         noteDetailService.loadLabelList(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     function addLastEmptyRow() { | ||||||
|  |         const labels = self.labels().filter(attr => attr().isDeleted === 0); | ||||||
|  |         const last = labels.length === 0 ? null : labels[labels.length - 1](); | ||||||
|  |  | ||||||
|  |         if (!last || last.name.trim() !== "" || last.value !== "") { | ||||||
|  |             self.labels.push(ko.observable({ | ||||||
|  |                 labelId: '', | ||||||
|  |                 name: '', | ||||||
|  |                 value: '', | ||||||
|  |                 isDeleted: 0, | ||||||
|  |                 position: 0 | ||||||
|  |             })); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.labelChanged = function (data, event) { | ||||||
|  |         addLastEmptyRow(); | ||||||
|  |  | ||||||
|  |         const label = self.getTargetLabel(event.target); | ||||||
|  |  | ||||||
|  |         label.valueHasMutated(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.isNotUnique = function(index) { | ||||||
|  |         const cur = self.labels()[index](); | ||||||
|  |  | ||||||
|  |         if (cur.name.trim() === "") { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (let labels = self.labels(), i = 0; i < labels.length; i++) { | ||||||
|  |             const label = labels[i](); | ||||||
|  |  | ||||||
|  |             if (index !== i && cur.name === label.name) { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.isEmptyName = function(index) { | ||||||
|  |         const cur = self.labels()[index](); | ||||||
|  |  | ||||||
|  |         return cur.name.trim() === "" && (cur.labelId !== "" || cur.value !== ""); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.getTargetLabel = function(target) { | ||||||
|  |         const context = ko.contextFor(target); | ||||||
|  |         const index = context.$index(); | ||||||
|  |  | ||||||
|  |         return self.labels()[index]; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function showDialog() { | ||||||
|  |     glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|  |     await labelsModel.loadLabels(); | ||||||
|  |  | ||||||
|  |     $dialog.dialog({ | ||||||
|  |         modal: true, | ||||||
|  |         width: 800, | ||||||
|  |         height: 500 | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ko.applyBindings(labelsModel, document.getElementById('labels-dialog')); | ||||||
|  |  | ||||||
|  | $(document).on('focus', '.label-name', function (e) { | ||||||
|  |     if (!$(this).hasClass("ui-autocomplete-input")) { | ||||||
|  |         $(this).autocomplete({ | ||||||
|  |             // shouldn't be required and autocomplete should just accept array of strings, but that fails | ||||||
|  |             // because we have overriden filter() function in autocomplete.js | ||||||
|  |             source: labelNames.map(label => { | ||||||
|  |                 return { | ||||||
|  |                     label: label, | ||||||
|  |                     value: label | ||||||
|  |                 } | ||||||
|  |             }), | ||||||
|  |             minLength: 0 | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $(this).autocomplete("search", $(this).val()); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $(document).on('focus', '.label-value', async function (e) { | ||||||
|  |     if (!$(this).hasClass("ui-autocomplete-input")) { | ||||||
|  |         const labelName = $(this).parent().parent().find('.label-name').val(); | ||||||
|  |  | ||||||
|  |         if (labelName.trim() === "") { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const labelValues = await server.get('labels/values/' + encodeURIComponent(labelName)); | ||||||
|  |  | ||||||
|  |         if (labelValues.length === 0) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $(this).autocomplete({ | ||||||
|  |             // shouldn't be required and autocomplete should just accept array of strings, but that fails | ||||||
|  |             // because we have overriden filter() function in autocomplete.js | ||||||
|  |             source: labelValues.map(label => { | ||||||
|  |                 return { | ||||||
|  |                     label: label, | ||||||
|  |                     value: label | ||||||
|  |                 } | ||||||
|  |             }), | ||||||
|  |             minLength: 0 | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $(this).autocomplete("search", $(this).val()); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     showDialog | ||||||
|  | }; | ||||||
| @@ -1,78 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const noteHistory = (function() { |  | ||||||
|     const $dialog = $("#note-history-dialog"); |  | ||||||
|     const $list = $("#note-history-list"); |  | ||||||
|     const $content = $("#note-history-content"); |  | ||||||
|     const $title = $("#note-history-title"); |  | ||||||
|  |  | ||||||
|     let historyItems = []; |  | ||||||
|  |  | ||||||
|     async function showCurrentNoteHistory() { |  | ||||||
|         await showNoteHistoryDialog(noteEditor.getCurrentNoteId()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function showNoteHistoryDialog(noteId, noteRevisionId) { |  | ||||||
|         glob.activeDialog = $dialog; |  | ||||||
|  |  | ||||||
|         $dialog.dialog({ |  | ||||||
|             modal: true, |  | ||||||
|             width: 800, |  | ||||||
|             height: 700 |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         $list.empty(); |  | ||||||
|         $content.empty(); |  | ||||||
|  |  | ||||||
|         historyItems = await server.get('notes-history/' + noteId); |  | ||||||
|  |  | ||||||
|         for (const item of historyItems) { |  | ||||||
|             const dateModified = parseDate(item.dateModifiedFrom); |  | ||||||
|  |  | ||||||
|             $list.append($('<option>', { |  | ||||||
|                 value: item.noteRevisionId, |  | ||||||
|                 text: formatDateTime(dateModified) |  | ||||||
|             })); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (historyItems.length > 0) { |  | ||||||
|             if (!noteRevisionId) { |  | ||||||
|                 noteRevisionId = $list.find("option:first").val(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             $list.val(noteRevisionId).trigger('change'); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             $title.text("No history for this note yet..."); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'alt+h', e => { |  | ||||||
|         showCurrentNoteHistory(); |  | ||||||
|  |  | ||||||
|         e.preventDefault(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     $list.on('change', () => { |  | ||||||
|         const optVal = $list.find(":selected").val(); |  | ||||||
|  |  | ||||||
|         const historyItem = historyItems.find(r => r.noteRevisionId === optVal); |  | ||||||
|  |  | ||||||
|         $title.html(historyItem.title); |  | ||||||
|         $content.html(historyItem.content); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     $(document).on('click', "a[action='note-history']", event => { |  | ||||||
|         const linkEl = $(event.target); |  | ||||||
|         const noteId = linkEl.attr('note-path'); |  | ||||||
|         const noteRevisionId = linkEl.attr('note-history-id'); |  | ||||||
|  |  | ||||||
|         showNoteHistoryDialog(noteId, noteRevisionId); |  | ||||||
|  |  | ||||||
|         return false; |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         showCurrentNoteHistory |  | ||||||
|     }; |  | ||||||
| })(); |  | ||||||
							
								
								
									
										72
									
								
								src/public/javascripts/dialogs/note_revisions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/public/javascripts/dialogs/note_revisions.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | import noteDetailService from '../services/note_detail.js'; | ||||||
|  | import utils from '../services/utils.js'; | ||||||
|  | import server from '../services/server.js'; | ||||||
|  |  | ||||||
|  | const $dialog = $("#note-revisions-dialog"); | ||||||
|  | const $list = $("#note-revision-list"); | ||||||
|  | const $content = $("#note-revision-content"); | ||||||
|  | const $title = $("#note-revision-title"); | ||||||
|  |  | ||||||
|  | let revisionItems = []; | ||||||
|  |  | ||||||
|  | async function showCurrentNoteRevisions() { | ||||||
|  |     await showNoteRevisionsDialog(noteDetailService.getCurrentNoteId()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function showNoteRevisionsDialog(noteId, noteRevisionId) { | ||||||
|  |     glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|  |     $dialog.dialog({ | ||||||
|  |         modal: true, | ||||||
|  |         width: 800, | ||||||
|  |         height: 700 | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     $list.empty(); | ||||||
|  |     $content.empty(); | ||||||
|  |  | ||||||
|  |     revisionItems = await server.get('notes/' + noteId + '/revisions'); | ||||||
|  |  | ||||||
|  |     for (const item of revisionItems) { | ||||||
|  |         const dateModified = utils.parseDate(item.dateModifiedFrom); | ||||||
|  |  | ||||||
|  |         $list.append($('<option>', { | ||||||
|  |             value: item.noteRevisionId, | ||||||
|  |             text: utils.formatDateTime(dateModified) | ||||||
|  |         })); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (revisionItems.length > 0) { | ||||||
|  |         if (!noteRevisionId) { | ||||||
|  |             noteRevisionId = $list.find("option:first").val(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $list.val(noteRevisionId).trigger('change'); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         $title.text("No revisions for this note yet..."); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $list.on('change', () => { | ||||||
|  |     const optVal = $list.find(":selected").val(); | ||||||
|  |  | ||||||
|  |     const revisionItem = revisionItems.find(r => r.noteRevisionId === optVal); | ||||||
|  |  | ||||||
|  |     $title.html(revisionItem.title); | ||||||
|  |     $content.html(revisionItem.content); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $(document).on('click', "a[action='note-revision']", event => { | ||||||
|  |     const linkEl = $(event.target); | ||||||
|  |     const noteId = linkEl.attr('note-path'); | ||||||
|  |     const noteRevisionId = linkEl.attr('note-revision-id'); | ||||||
|  |  | ||||||
|  |     showNoteRevisionsDialog(noteId, noteRevisionId); | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     showCurrentNoteRevisions | ||||||
|  | }; | ||||||
| @@ -1,6 +1,5 @@ | |||||||
| "use strict"; | import noteDetailService from '../services/note_detail.js'; | ||||||
|  |  | ||||||
| const noteSource = (function() { |  | ||||||
| const $dialog = $("#note-source-dialog"); | const $dialog = $("#note-source-dialog"); | ||||||
| const $noteSource = $("#note-source"); | const $noteSource = $("#note-source"); | ||||||
|  |  | ||||||
| @@ -13,7 +12,7 @@ const noteSource = (function() { | |||||||
|         height: 500 |         height: 500 | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|         const noteText = noteEditor.getCurrentNote().detail.content; |     const noteText = noteDetailService.getCurrentNote().content; | ||||||
|  |  | ||||||
|     $noteSource.text(formatHtml(noteText)); |     $noteSource.text(formatHtml(noteText)); | ||||||
| } | } | ||||||
| @@ -30,7 +29,7 @@ const noteSource = (function() { | |||||||
|     const indentAfter  = new Array(level - 1).join('  '); |     const indentAfter  = new Array(level - 1).join('  '); | ||||||
|     let textNode; |     let textNode; | ||||||
|  |  | ||||||
|         for (let i = 0; i < node.children.length; i++) { |     for (const i = 0; i < node.children.length; i++) { | ||||||
|         textNode = document.createTextNode('\n' + indentBefore); |         textNode = document.createTextNode('\n' + indentBefore); | ||||||
|         node.insertBefore(textNode, node.children[i]); |         node.insertBefore(textNode, node.children[i]); | ||||||
|  |  | ||||||
| @@ -45,13 +44,6 @@ const noteSource = (function() { | |||||||
|     return node; |     return node; | ||||||
| } | } | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+u', e => { | export default { | ||||||
|         showDialog(); |  | ||||||
|  |  | ||||||
|         e.preventDefault(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|     showDialog |     showDialog | ||||||
| }; | }; | ||||||
| })(); |  | ||||||
| @@ -1,19 +1,23 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
| 
 | 
 | ||||||
| const settings = (function() { | import protectedSessionHolder from '../services/protected_session_holder.js'; | ||||||
|     const $dialog = $("#settings-dialog"); | import utils from '../services/utils.js'; | ||||||
|     const $tabs = $("#settings-tabs"); | import server from '../services/server.js'; | ||||||
|  | import infoService from "../services/info.js"; | ||||||
| 
 | 
 | ||||||
|     const settingModules = []; | const $dialog = $("#options-dialog"); | ||||||
|  | const $tabs = $("#options-tabs"); | ||||||
| 
 | 
 | ||||||
|     function addModule(module) { | const tabHandlers = []; | ||||||
|         settingModules.push(module); | 
 | ||||||
|  | function addTabHandler(handler) { | ||||||
|  |     tabHandlers.push(handler); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function showDialog() { | async function showDialog() { | ||||||
|     glob.activeDialog = $dialog; |     glob.activeDialog = $dialog; | ||||||
| 
 | 
 | ||||||
|         const settings = await server.get('settings'); |     const options = await server.get('options'); | ||||||
| 
 | 
 | ||||||
|     $dialog.dialog({ |     $dialog.dialog({ | ||||||
|         modal: true, |         modal: true, | ||||||
| @@ -22,36 +26,31 @@ const settings = (function() { | |||||||
| 
 | 
 | ||||||
|     $tabs.tabs(); |     $tabs.tabs(); | ||||||
| 
 | 
 | ||||||
|         for (const module of settingModules) { |     for (const handler of tabHandlers) { | ||||||
|             if (module.settingsLoaded) { |         if (handler.optionsLoaded) { | ||||||
|                 module.settingsLoaded(settings); |             handler.optionsLoaded(options); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|     async function saveSettings(settingName, settingValue) { | async function saveOptions(optionName, optionValue) { | ||||||
|         await server.post('settings', { |     await server.put('options/' + encodeURIComponent(optionName) + '/' + encodeURIComponent(optionValue)); | ||||||
|             name: settingName, |  | ||||||
|             value: settingValue |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         showMessage("Settings change have been saved."); |     infoService.showMessage("Options change have been saved."); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|     return { | export default { | ||||||
|     showDialog, |     showDialog, | ||||||
|         saveSettings, |     saveOptions | ||||||
|         addModule |  | ||||||
| }; | }; | ||||||
| })(); |  | ||||||
| 
 | 
 | ||||||
| settings.addModule((function() { | addTabHandler((function() { | ||||||
|     const $form = $("#change-password-form"); |     const $form = $("#change-password-form"); | ||||||
|     const $oldPassword = $("#old-password"); |     const $oldPassword = $("#old-password"); | ||||||
|     const $newPassword1 = $("#new-password1"); |     const $newPassword1 = $("#new-password1"); | ||||||
|     const $newPassword2 = $("#new-password2"); |     const $newPassword2 = $("#new-password2"); | ||||||
| 
 | 
 | ||||||
|     function settingsLoaded(settings) { |     function optionsLoaded(options) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     $form.submit(() => { |     $form.submit(() => { | ||||||
| @@ -76,10 +75,10 @@ settings.addModule((function() { | |||||||
|                 alert("Password has been changed. Trilium will be reloaded after you press OK."); |                 alert("Password has been changed. Trilium will be reloaded after you press OK."); | ||||||
| 
 | 
 | ||||||
|                 // password changed so current protected session is invalid and needs to be cleared
 |                 // password changed so current protected session is invalid and needs to be cleared
 | ||||||
|                 protected_session.resetProtectedSession(); |                 protectedSessionHolder.resetProtectedSession(); | ||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|                 showError(result.message); |                 infoService.showError(result.message); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
| @@ -87,55 +86,55 @@ settings.addModule((function() { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         settingsLoaded |         optionsLoaded | ||||||
|     }; |     }; | ||||||
| })()); | })()); | ||||||
| 
 | 
 | ||||||
| settings.addModule((function() { | addTabHandler((function() { | ||||||
|     const $form = $("#protected-session-timeout-form"); |     const $form = $("#protected-session-timeout-form"); | ||||||
|     const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds"); |     const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds"); | ||||||
|     const settingName = 'protected_session_timeout'; |     const optionName = 'protectedSessionTimeout'; | ||||||
| 
 | 
 | ||||||
|     function settingsLoaded(settings) { |     function optionsLoaded(options) { | ||||||
|         $protectedSessionTimeout.val(settings[settingName]); |         $protectedSessionTimeout.val(options[optionName]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     $form.submit(() => { |     $form.submit(() => { | ||||||
|         const protectedSessionTimeout = $protectedSessionTimeout.val(); |         const protectedSessionTimeout = $protectedSessionTimeout.val(); | ||||||
| 
 | 
 | ||||||
|         settings.saveSettings(settingName, protectedSessionTimeout).then(() => { |         saveOptions(optionName, protectedSessionTimeout).then(() => { | ||||||
|             protected_session.setProtectedSessionTimeout(protectedSessionTimeout); |             protectedSessionHolder.setProtectedSessionTimeout(protectedSessionTimeout); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         return false; |         return false; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         settingsLoaded |         optionsLoaded | ||||||
|     }; |     }; | ||||||
| })()); | })()); | ||||||
| 
 | 
 | ||||||
| settings.addModule((function () { | addTabHandler((function () { | ||||||
|     const $form = $("#history-snapshot-time-interval-form"); |     const $form = $("#note-revision-snapshot-time-interval-form"); | ||||||
|     const $timeInterval = $("#history-snapshot-time-interval-in-seconds"); |     const $timeInterval = $("#note-revision-snapshot-time-interval-in-seconds"); | ||||||
|     const settingName = 'history_snapshot_time_interval'; |     const optionName = 'noteRevisionSnapshotTimeInterval'; | ||||||
| 
 | 
 | ||||||
|     function settingsLoaded(settings) { |     function optionsLoaded(options) { | ||||||
|         $timeInterval.val(settings[settingName]); |         $timeInterval.val(options[optionName]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     $form.submit(() => { |     $form.submit(() => { | ||||||
|         settings.saveSettings(settingName, $timeInterval.val()); |         saveOptions(optionName, $timeInterval.val()); | ||||||
| 
 | 
 | ||||||
|         return false; |         return false; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         settingsLoaded |         optionsLoaded | ||||||
|     }; |     }; | ||||||
| })()); | })()); | ||||||
| 
 | 
 | ||||||
| settings.addModule((async function () { | addTabHandler((async function () { | ||||||
|     const $appVersion = $("#app-version"); |     const $appVersion = $("#app-version"); | ||||||
|     const $dbVersion = $("#db-version"); |     const $dbVersion = $("#db-version"); | ||||||
|     const $buildDate = $("#build-date"); |     const $buildDate = $("#build-date"); | ||||||
| @@ -143,16 +142,16 @@ settings.addModule((async function () { | |||||||
| 
 | 
 | ||||||
|     const appInfo = await server.get('app-info'); |     const appInfo = await server.get('app-info'); | ||||||
| 
 | 
 | ||||||
|     $appVersion.html(appInfo.app_version); |     $appVersion.html(appInfo.appVersion); | ||||||
|     $dbVersion.html(appInfo.db_version); |     $dbVersion.html(appInfo.dbVersion); | ||||||
|     $buildDate.html(appInfo.build_date); |     $buildDate.html(appInfo.buildDate); | ||||||
|     $buildRevision.html(appInfo.build_revision); |     $buildRevision.html(appInfo.buildRevision); | ||||||
|     $buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision); |     $buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision); | ||||||
| 
 | 
 | ||||||
|     return {}; |     return {}; | ||||||
| })()); | })()); | ||||||
| 
 | 
 | ||||||
| settings.addModule((async function () { | addTabHandler((async function () { | ||||||
|     const $forceFullSyncButton = $("#force-full-sync-button"); |     const $forceFullSyncButton = $("#force-full-sync-button"); | ||||||
|     const $fillSyncRowsButton = $("#fill-sync-rows-button"); |     const $fillSyncRowsButton = $("#fill-sync-rows-button"); | ||||||
|     const $anonymizeButton = $("#anonymize-button"); |     const $anonymizeButton = $("#anonymize-button"); | ||||||
| @@ -163,27 +162,27 @@ settings.addModule((async function () { | |||||||
|     $forceFullSyncButton.click(async () => { |     $forceFullSyncButton.click(async () => { | ||||||
|         await server.post('sync/force-full-sync'); |         await server.post('sync/force-full-sync'); | ||||||
| 
 | 
 | ||||||
|         showMessage("Full sync triggered"); |         infoService.showMessage("Full sync triggered"); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     $fillSyncRowsButton.click(async () => { |     $fillSyncRowsButton.click(async () => { | ||||||
|         await server.post('sync/fill-sync-rows'); |         await server.post('sync/fill-sync-rows'); | ||||||
| 
 | 
 | ||||||
|         showMessage("Sync rows filled successfully"); |         infoService.showMessage("Sync rows filled successfully"); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     $anonymizeButton.click(async () => { |     $anonymizeButton.click(async () => { | ||||||
|         await server.post('anonymization/anonymize'); |         await server.post('anonymization/anonymize'); | ||||||
| 
 | 
 | ||||||
|         showMessage("Created anonymized database"); |         infoService.showMessage("Created anonymized database"); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     $cleanupSoftDeletedButton.click(async () => { |     $cleanupSoftDeletedButton.click(async () => { | ||||||
|         if (confirm("Do you really want to clean up soft-deleted items?")) { |         if (confirm("Do you really want to clean up soft-deleted items?")) { | ||||||
|             await server.post('cleanup/cleanup-soft-deleted-items'); |             await server.post('cleanup/cleanup-soft-deleted-items'); | ||||||
| 
 | 
 | ||||||
|             showMessage("Soft deleted items have been cleaned up"); |             infoService.showMessage("Soft deleted items have been cleaned up"); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @@ -191,14 +190,14 @@ settings.addModule((async function () { | |||||||
|         if (confirm("Do you really want to clean up unused images?")) { |         if (confirm("Do you really want to clean up unused images?")) { | ||||||
|             await server.post('cleanup/cleanup-unused-images'); |             await server.post('cleanup/cleanup-unused-images'); | ||||||
| 
 | 
 | ||||||
|             showMessage("Unused images have been cleaned up"); |             infoService.showMessage("Unused images have been cleaned up"); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     $vacuumDatabaseButton.click(async () => { |     $vacuumDatabaseButton.click(async () => { | ||||||
|         await server.post('cleanup/vacuum-database'); |         await server.post('cleanup/vacuum-database'); | ||||||
| 
 | 
 | ||||||
|         showMessage("Database has been vacuumed"); |         infoService.showMessage("Database has been vacuumed"); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return {}; |     return {}; | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| "use strict"; | import linkService from '../services/link.js'; | ||||||
|  | import utils from '../services/utils.js'; | ||||||
|  | import server from '../services/server.js'; | ||||||
|  |  | ||||||
| const recentChanges = (function() { |  | ||||||
| const $dialog = $("#recent-changes-dialog"); | const $dialog = $("#recent-changes-dialog"); | ||||||
|  |  | ||||||
| async function showDialog() { | async function showDialog() { | ||||||
| @@ -21,17 +22,17 @@ const recentChanges = (function() { | |||||||
|     for (const [dateDay, dayChanges] of groupedByDate) { |     for (const [dateDay, dayChanges] of groupedByDate) { | ||||||
|         const changesListEl = $('<ul>'); |         const changesListEl = $('<ul>'); | ||||||
|  |  | ||||||
|             const dayEl = $('<div>').append($('<b>').html(formatDate(dateDay))).append(changesListEl); |         const dayEl = $('<div>').append($('<b>').html(utils.formatDate(dateDay))).append(changesListEl); | ||||||
|  |  | ||||||
|         for (const change of dayChanges) { |         for (const change of dayChanges) { | ||||||
|                 const formattedTime = formatTime(parseDate(change.dateModifiedTo)); |             const formattedTime = utils.formatTime(utils.parseDate(change.dateModifiedTo)); | ||||||
|  |  | ||||||
|             const revLink = $("<a>", { |             const revLink = $("<a>", { | ||||||
|                 href: 'javascript:', |                 href: 'javascript:', | ||||||
|                 text: 'rev' |                 text: 'rev' | ||||||
|                 }).attr('action', 'note-history') |             }).attr('action', 'note-revision') | ||||||
|                 .attr('note-path', change.noteId) |                 .attr('note-path', change.noteId) | ||||||
|                     .attr('note-history-id', change.noteRevisionId); |                 .attr('note-revision-id', change.noteRevisionId); | ||||||
|  |  | ||||||
|             let noteLink; |             let noteLink; | ||||||
|  |  | ||||||
| @@ -39,7 +40,7 @@ const recentChanges = (function() { | |||||||
|                 noteLink = change.current_title; |                 noteLink = change.current_title; | ||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|                     noteLink = link.createNoteLink(change.noteId, change.title); |                 noteLink = linkService.createNoteLink(change.noteId, change.title); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             changesListEl.append($('<li>') |             changesListEl.append($('<li>') | ||||||
| @@ -57,7 +58,7 @@ const recentChanges = (function() { | |||||||
|     const dayCache = {}; |     const dayCache = {}; | ||||||
|  |  | ||||||
|     for (const row of result) { |     for (const row of result) { | ||||||
|             let dateDay = parseDate(row.dateModifiedTo); |         let dateDay = utils.parseDate(row.dateModifiedTo); | ||||||
|         dateDay.setHours(0); |         dateDay.setHours(0); | ||||||
|         dateDay.setMinutes(0); |         dateDay.setMinutes(0); | ||||||
|         dateDay.setSeconds(0); |         dateDay.setSeconds(0); | ||||||
| @@ -81,9 +82,6 @@ const recentChanges = (function() { | |||||||
|     return groupedByDate; |     return groupedByDate; | ||||||
| } | } | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'alt+r', showDialog); | export default { | ||||||
|  |  | ||||||
|     return { |  | ||||||
|     showDialog |     showDialog | ||||||
| }; | }; | ||||||
| })(); |  | ||||||
| @@ -1,6 +1,9 @@ | |||||||
| "use strict"; | 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 recentNotes = (function() { |  | ||||||
| const $dialog = $("#recent-notes-dialog"); | const $dialog = $("#recent-notes-dialog"); | ||||||
| const $searchInput = $('#recent-notes-search-input'); | const $searchInput = $('#recent-notes-search-input'); | ||||||
|  |  | ||||||
| @@ -13,18 +16,33 @@ const recentNotes = (function() { | |||||||
|     list = result.map(r => r.notePath); |     list = result.map(r => r.notePath); | ||||||
| } | } | ||||||
|  |  | ||||||
|     function addRecentNote(noteTreeId, notePath) { | function addRecentNote(branchId, notePath) { | ||||||
|     setTimeout(async () => { |     setTimeout(async () => { | ||||||
|         // we include the note into recent list only if the user stayed on the note at least 5 seconds |         // we include the note into recent list only if the user stayed on the note at least 5 seconds | ||||||
|             if (notePath && notePath === noteTree.getCurrentNotePath()) { |         if (notePath && notePath === treeService.getCurrentNotePath()) { | ||||||
|                 const result = await server.put('recent-notes/' + noteTreeId + '/' + encodeURIComponent(notePath)); |             const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath)); | ||||||
|  |  | ||||||
|             list = result.map(r => r.notePath); |             list = result.map(r => r.notePath); | ||||||
|         } |         } | ||||||
|     }, 1500); |     }, 1500); | ||||||
| } | } | ||||||
|  |  | ||||||
|     function showDialog() { | 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; |     glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|     $dialog.dialog({ |     $dialog.dialog({ | ||||||
| @@ -37,30 +55,22 @@ const recentNotes = (function() { | |||||||
|     $searchInput.val(''); |     $searchInput.val(''); | ||||||
|  |  | ||||||
|     // remove the current note |     // remove the current note | ||||||
|         const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath()); |     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({ |     $searchInput.autocomplete({ | ||||||
|             source: recNotes.map(notePath => { |         source: items, | ||||||
|                 let noteTitle; |  | ||||||
|  |  | ||||||
|                 try { |  | ||||||
|                     noteTitle = noteTree.getNotePathTitle(notePath); |  | ||||||
|                 } |  | ||||||
|                 catch (e) { |  | ||||||
|                     noteTitle = "[error - can't find note title]"; |  | ||||||
|  |  | ||||||
|                     messaging.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return { |  | ||||||
|                     label: noteTitle, |  | ||||||
|                     value: notePath |  | ||||||
|                 } |  | ||||||
|             }), |  | ||||||
|         minLength: 0, |         minLength: 0, | ||||||
|         autoFocus: true, |         autoFocus: true, | ||||||
|         select: function (event, ui) { |         select: function (event, ui) { | ||||||
|                 noteTree.activateNode(ui.item.value); |             treeService.activateNode(ui.item.value); | ||||||
|  |  | ||||||
|             $searchInput.autocomplete('destroy'); |             $searchInput.autocomplete('destroy'); | ||||||
|             $dialog.dialog('close'); |             $dialog.dialog('close'); | ||||||
| @@ -86,17 +96,18 @@ const recentNotes = (function() { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | 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(); |         reload(); | ||||||
|  |     } | ||||||
|     $(document).bind('keydown', 'ctrl+e', e => { |  | ||||||
|         showDialog(); |  | ||||||
|  |  | ||||||
|         e.preventDefault(); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
|     return { | export default { | ||||||
|     showDialog, |     showDialog, | ||||||
|     addRecentNote, |     addRecentNote, | ||||||
|     reload |     reload | ||||||
| }; | }; | ||||||
| })(); |  | ||||||
| @@ -1,6 +1,8 @@ | |||||||
| "use strict"; | import utils from '../services/utils.js'; | ||||||
|  | import libraryLoader from '../services/library_loader.js'; | ||||||
|  | import server from '../services/server.js'; | ||||||
|  | import infoService from "../services/info.js"; | ||||||
|  |  | ||||||
| const sqlConsole = (function() { |  | ||||||
| const $dialog = $("#sql-console-dialog"); | const $dialog = $("#sql-console-dialog"); | ||||||
| const $query = $('#sql-console-query'); | const $query = $('#sql-console-query'); | ||||||
| const $executeButton = $('#sql-console-execute'); | const $executeButton = $('#sql-console-execute'); | ||||||
| @@ -24,7 +26,7 @@ const sqlConsole = (function() { | |||||||
|  |  | ||||||
| async function initEditor() { | async function initEditor() { | ||||||
|     if (!codeEditor) { |     if (!codeEditor) { | ||||||
|             await requireLibrary(CODE_MIRROR); |         await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR); | ||||||
|  |  | ||||||
|         CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; |         CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||||
|         CodeMirror.keyMap.default["Tab"] = "indentMore"; |         CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||||
| @@ -60,11 +62,11 @@ const sqlConsole = (function() { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (!result.success) { |     if (!result.success) { | ||||||
|             showError(result.error); |         infoService.showError(result.error); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|             showMessage("Query was executed successfully."); |         infoService.showMessage("Query was executed successfully."); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const rows = result.rows; |     const rows = result.rows; | ||||||
| @@ -94,13 +96,10 @@ const sqlConsole = (function() { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'alt+o', showDialog); |  | ||||||
|  |  | ||||||
| $query.bind('keydown', 'ctrl+return', execute); | $query.bind('keydown', 'ctrl+return', execute); | ||||||
|  |  | ||||||
| $executeButton.click(execute); | $executeButton.click(execute); | ||||||
|  |  | ||||||
|     return { | export default { | ||||||
|     showDialog |     showDialog | ||||||
| }; | }; | ||||||
| })(); |  | ||||||
							
								
								
									
										22
									
								
								src/public/javascripts/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/public/javascripts/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | class Branch { | ||||||
|  |     constructor(treeCache, row) { | ||||||
|  |         this.treeCache = treeCache; | ||||||
|  |         this.branchId = row.branchId; | ||||||
|  |         this.noteId = row.noteId; | ||||||
|  |         this.note = null; | ||||||
|  |         this.parentNoteId = row.parentNoteId; | ||||||
|  |         this.notePosition = row.notePosition; | ||||||
|  |         this.prefix = row.prefix; | ||||||
|  |         this.isExpanded = row.isExpanded; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getNote() { | ||||||
|  |         return await this.treeCache.getNote(this.noteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get toString() { | ||||||
|  |         return `Branch(branchId=${this.branchId})`; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Branch; | ||||||
							
								
								
									
										15
									
								
								src/public/javascripts/entities/note_full.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/public/javascripts/entities/note_full.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | import NoteShort from './note_short.js'; | ||||||
|  |  | ||||||
|  | class NoteFull extends NoteShort { | ||||||
|  |     constructor(treeCache, row) { | ||||||
|  |         super(treeCache, row); | ||||||
|  |  | ||||||
|  |         this.content = row.content; | ||||||
|  |  | ||||||
|  |         if (this.isJson()) { | ||||||
|  |             this.jsonContent = JSON.parse(this.content); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default NoteFull; | ||||||
							
								
								
									
										49
									
								
								src/public/javascripts/entities/note_short.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/public/javascripts/entities/note_short.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | class NoteShort { | ||||||
|  |     constructor(treeCache, row) { | ||||||
|  |         this.treeCache = treeCache; | ||||||
|  |         this.noteId = row.noteId; | ||||||
|  |         this.title = row.title; | ||||||
|  |         this.isProtected = row.isProtected; | ||||||
|  |         this.type = row.type; | ||||||
|  |         this.mime = row.mime; | ||||||
|  |         this.hideInAutocomplete = row.hideInAutocomplete; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     isJson() { | ||||||
|  |         return this.mime === "application/json"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getBranches() { | ||||||
|  |         const branches = []; | ||||||
|  |  | ||||||
|  |         for (const parent of this.treeCache.parents[this.noteId]) { | ||||||
|  |             branches.push(await this.treeCache.getBranchByChildParent(this.noteId, parent.noteId)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return branches; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getChildBranches() { | ||||||
|  |         const branches = []; | ||||||
|  |  | ||||||
|  |         for (const child of this.treeCache.children[this.noteId]) { | ||||||
|  |             branches.push(await this.treeCache.getBranchByChildParent(child.noteId, this.noteId)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return branches; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getParentNotes() { | ||||||
|  |         return this.treeCache.parents[this.noteId] || []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getChildNotes() { | ||||||
|  |         return this.treeCache.children[this.noteId] || []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get toString() { | ||||||
|  |         return `Note(noteId=${this.noteId}, title=${this.title})`; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default NoteShort; | ||||||
| @@ -1,32 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| function exportSubTree(noteId) { |  | ||||||
|     const url = getHost() + "/api/export/" + noteId + "?protectedSessionId=" |  | ||||||
|         + encodeURIComponent(protected_session.getProtectedSessionId()); |  | ||||||
|  |  | ||||||
|     download(url); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| let importNoteId; |  | ||||||
|  |  | ||||||
| function importSubTree(noteId) { |  | ||||||
|     importNoteId = noteId; |  | ||||||
|  |  | ||||||
|     $("#import-upload").trigger('click'); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| $("#import-upload").change(async function() { |  | ||||||
|     const formData = new FormData(); |  | ||||||
|     formData.append('upload', this.files[0]); |  | ||||||
|  |  | ||||||
|     await $.ajax({ |  | ||||||
|         url: baseApiUrl + 'import/' + importNoteId, |  | ||||||
|         headers: server.getHeaders(), |  | ||||||
|         data: formData, |  | ||||||
|         type: 'POST', |  | ||||||
|         contentType: false, // NEEDED, DON'T OMIT THIS |  | ||||||
|         processData: false, // NEEDED, DON'T OMIT THIS |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     await noteTree.reload(); |  | ||||||
| }); |  | ||||||
| @@ -1,248 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| // hot keys are active also inside inputs and content editables |  | ||||||
| jQuery.hotkeys.options.filterInputAcceptingElements = false; |  | ||||||
| jQuery.hotkeys.options.filterContentEditable = false; |  | ||||||
| jQuery.hotkeys.options.filterTextInputs = false; |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', 'alt+m', e => { |  | ||||||
|     $(".hide-toggle").toggleClass("suppressed"); |  | ||||||
|  |  | ||||||
|     e.preventDefault(); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| // hide (toggle) everything except for the note content for distraction free writing |  | ||||||
| $(document).bind('keydown', 'alt+t', e => { |  | ||||||
|     const date = new Date(); |  | ||||||
|     const dateString = formatDateTime(date); |  | ||||||
|  |  | ||||||
|     link.addTextToEditor(dateString); |  | ||||||
|  |  | ||||||
|     e.preventDefault(); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', 'f5', () => { |  | ||||||
|     reloadApp(); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', 'ctrl+r', () => { |  | ||||||
|     reloadApp(); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', 'ctrl+shift+i', () => { |  | ||||||
|     if (isElectron()) { |  | ||||||
|         require('electron').remote.getCurrentWindow().toggleDevTools(); |  | ||||||
|  |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', 'ctrl+f', () => { |  | ||||||
|     if (isElectron()) { |  | ||||||
|         const searchInPage = require('electron-in-page-search').default; |  | ||||||
|         const remote = require('electron').remote; |  | ||||||
|  |  | ||||||
|         const inPageSearch = searchInPage(remote.getCurrentWebContents()); |  | ||||||
|  |  | ||||||
|         inPageSearch.openSearchWindow(); |  | ||||||
|  |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', "ctrl+shift+up", () => { |  | ||||||
|     const node = noteTree.getCurrentNode(); |  | ||||||
|     node.navigate($.ui.keyCode.UP, true); |  | ||||||
|  |  | ||||||
|     $("#note-detail").focus(); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', "ctrl+shift+down", () => { |  | ||||||
|     const node = noteTree.getCurrentNode(); |  | ||||||
|     node.navigate($.ui.keyCode.DOWN, true); |  | ||||||
|  |  | ||||||
|     $("#note-detail").focus(); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', 'ctrl+-', () => { |  | ||||||
|     if (isElectron()) { |  | ||||||
|         const webFrame = require('electron').webFrame; |  | ||||||
|  |  | ||||||
|         if (webFrame.getZoomFactor() > 0.2) { |  | ||||||
|             webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', 'ctrl+=', () => { |  | ||||||
|     if (isElectron()) { |  | ||||||
|         const webFrame = require('electron').webFrame; |  | ||||||
|  |  | ||||||
|         webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1); |  | ||||||
|  |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $("#note-title").bind('keydown', 'return', () => $("#note-detail").focus()); |  | ||||||
|  |  | ||||||
| $(window).on('beforeunload', () => { |  | ||||||
|     // this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved |  | ||||||
|     // this sends the request asynchronously and doesn't wait for result |  | ||||||
|     noteEditor.saveNoteIfChanged(); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| // 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; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| $(document).tooltip({ |  | ||||||
|     items: "#note-detail a", |  | ||||||
|     content: function(callback) { |  | ||||||
|         const notePath = link.getNotePathFromLink($(this).attr("href")); |  | ||||||
|  |  | ||||||
|         if (notePath !== null) { |  | ||||||
|             const noteId = treeUtils.getNoteIdFromNotePath(notePath); |  | ||||||
|  |  | ||||||
|             noteEditor.loadNote(noteId).then(note => callback(note.detail.content)); |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     close: function(event, ui) |  | ||||||
|     { |  | ||||||
|         ui.tooltip.hover(function() |  | ||||||
|         { |  | ||||||
|             $(this).stop(true).fadeTo(400, 1); |  | ||||||
|         }, |  | ||||||
|         function() |  | ||||||
|         { |  | ||||||
|             $(this).fadeOut('400', function() |  | ||||||
|             { |  | ||||||
|                 $(this).remove(); |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| window.onerror = function (msg, url, lineNo, columnNo, error) { |  | ||||||
|     const string = msg.toLowerCase(); |  | ||||||
|  |  | ||||||
|     let message = "Uncaught error: "; |  | ||||||
|  |  | ||||||
|     if (string.indexOf("script error") > -1){ |  | ||||||
|         message += 'No details available'; |  | ||||||
|     } |  | ||||||
|     else { |  | ||||||
|         message += [ |  | ||||||
|             'Message: ' + msg, |  | ||||||
|             'URL: ' + url, |  | ||||||
|             'Line: ' + lineNo, |  | ||||||
|             'Column: ' + columnNo, |  | ||||||
|             'Error object: ' + JSON.stringify(error) |  | ||||||
|         ].join(' - '); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     messaging.logError(message); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| $("#logout-button").toggle(!isElectron()); |  | ||||||
|  |  | ||||||
| $(document).ready(() => { |  | ||||||
|     server.get("script/startup").then(scriptBundles => { |  | ||||||
|         for (const bundle of scriptBundles) { |  | ||||||
|             executeBundle(bundle); |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| if (isElectron()) { |  | ||||||
|     require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) { |  | ||||||
|         // this might occur when day note had to be created |  | ||||||
|         if (!noteTree.noteExists(parentNoteId)) { |  | ||||||
|             await noteTree.reload(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await noteTree.activateNode(parentNoteId); |  | ||||||
|  |  | ||||||
|         setTimeout(() => { |  | ||||||
|             const node = noteTree.getCurrentNode(); |  | ||||||
|  |  | ||||||
|             noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected); |  | ||||||
|         }, 500); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function uploadAttachment() { |  | ||||||
|     $("#attachment-upload").trigger('click'); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| $("#attachment-upload").change(async function() { |  | ||||||
|     const formData = new FormData(); |  | ||||||
|     formData.append('upload', this.files[0]); |  | ||||||
|  |  | ||||||
|     const resp = await $.ajax({ |  | ||||||
|         url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(), |  | ||||||
|         headers: server.getHeaders(), |  | ||||||
|         data: formData, |  | ||||||
|         type: 'POST', |  | ||||||
|         contentType: false, // NEEDED, DON'T OMIT THIS |  | ||||||
|         processData: false, // NEEDED, DON'T OMIT THIS |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     await noteTree.reload(); |  | ||||||
|  |  | ||||||
|     await noteTree.activateNode(resp.noteId); |  | ||||||
| }); |  | ||||||
| @@ -1,103 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const link = (function() { |  | ||||||
|     function getNotePathFromLink(url) { |  | ||||||
|         const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url); |  | ||||||
|  |  | ||||||
|         if (notePathMatch === null) { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             return notePathMatch[1]; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getNodePathFromLabel(label) { |  | ||||||
|         const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label); |  | ||||||
|  |  | ||||||
|         if (notePathMatch !== null) { |  | ||||||
|             return notePathMatch[1]; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function createNoteLink(notePath, noteTitle) { |  | ||||||
|         if (!noteTitle) { |  | ||||||
|             const noteId = treeUtils.getNoteIdFromNotePath(notePath); |  | ||||||
|  |  | ||||||
|             noteTitle = noteTree.getNoteTitle(noteId); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const noteLink = $("<a>", { |  | ||||||
|             href: 'javascript:', |  | ||||||
|             text: noteTitle |  | ||||||
|         }).attr('action', 'note') |  | ||||||
|             .attr('note-path', notePath); |  | ||||||
|  |  | ||||||
|         return noteLink; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function goToLink(e) { |  | ||||||
|         e.preventDefault(); |  | ||||||
|  |  | ||||||
|         const $link = $(e.target); |  | ||||||
|         let notePath = $link.attr("note-path"); |  | ||||||
|  |  | ||||||
|         if (!notePath) { |  | ||||||
|             const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href'); |  | ||||||
|  |  | ||||||
|             if (!address) { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (address.startsWith('http')) { |  | ||||||
|                 window.open(address, '_blank'); |  | ||||||
|  |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             notePath = getNotePathFromLink(address); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         noteTree.activateNode(notePath); |  | ||||||
|  |  | ||||||
|         // this is quite ugly hack, but it seems like we can't close the tooltip otherwise |  | ||||||
|         $("[role='tooltip']").remove(); |  | ||||||
|  |  | ||||||
|         if (glob.activeDialog) { |  | ||||||
|             try { |  | ||||||
|                 glob.activeDialog.dialog('close'); |  | ||||||
|             } |  | ||||||
|             catch (e) {} |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function addLinkToEditor(linkTitle, linkHref) { |  | ||||||
|         const editor = noteEditor.getEditor(); |  | ||||||
|         const doc = editor.document; |  | ||||||
|  |  | ||||||
|         doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function addTextToEditor(text) { |  | ||||||
|         const editor = noteEditor.getEditor(); |  | ||||||
|         const doc = editor.document; |  | ||||||
|  |  | ||||||
|         doc.enqueueChanges(() => editor.data.insertText(text), doc.selection); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // when click on link popup, in case of internal link, just go the the referenced note instead of default behavior |  | ||||||
|     // of opening the link in new window/tab |  | ||||||
|     $(document).on('click', "a[action='note']", goToLink); |  | ||||||
|     $(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink); |  | ||||||
|     $(document).on('dblclick', '#note-detail a', goToLink); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         getNodePathFromLabel, |  | ||||||
|         getNotePathFromLink, |  | ||||||
|         createNoteLink, |  | ||||||
|         addLinkToEditor, |  | ||||||
|         addTextToEditor |  | ||||||
|     }; |  | ||||||
| })(); |  | ||||||
| @@ -1,115 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const messaging = (function() { |  | ||||||
|     const $changesToPushCount = $("#changes-to-push-count"); |  | ||||||
|  |  | ||||||
|     function logError(message) { |  | ||||||
|         console.log(now(), message); // needs to be separate from .trace() |  | ||||||
|         console.trace(); |  | ||||||
|  |  | ||||||
|         if (ws && ws.readyState === 1) { |  | ||||||
|             ws.send(JSON.stringify({ |  | ||||||
|                 type: 'log-error', |  | ||||||
|                 error: message |  | ||||||
|             })); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function messageHandler(event) { |  | ||||||
|         const message = JSON.parse(event.data); |  | ||||||
|  |  | ||||||
|         if (message.type === 'sync') { |  | ||||||
|             lastPingTs = new Date().getTime(); |  | ||||||
|  |  | ||||||
|             if (message.data.length > 0) { |  | ||||||
|                 console.log(now(), "Sync data: ", message.data); |  | ||||||
|  |  | ||||||
|                 lastSyncId = message.data[message.data.length - 1].id; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId); |  | ||||||
|  |  | ||||||
|             if (syncData.some(sync => sync.entityName === 'note_tree') |  | ||||||
|                 || syncData.some(sync => sync.entityName === 'notes')) { |  | ||||||
|  |  | ||||||
|                 console.log(now(), "Reloading tree because of background changes"); |  | ||||||
|  |  | ||||||
|                 noteTree.reload(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === noteEditor.getCurrentNoteId())) { |  | ||||||
|                 showMessage('Reloading note because of background changes'); |  | ||||||
|  |  | ||||||
|                 noteEditor.reload(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (syncData.some(sync => sync.entityName === 'recent_notes')) { |  | ||||||
|                 console.log(now(), "Reloading recent notes because of background changes"); |  | ||||||
|  |  | ||||||
|                 recentNotes.reload(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // we don't detect image changes here since images themselves are immutable and references should be |  | ||||||
|             // updated in note detail as well |  | ||||||
|  |  | ||||||
|             $changesToPushCount.html(message.changesToPushCount); |  | ||||||
|         } |  | ||||||
|         else if (message.type === 'sync-hash-check-failed') { |  | ||||||
|             showError("Sync check failed!", 60000); |  | ||||||
|         } |  | ||||||
|         else if (message.type === 'consistency-checks-failed') { |  | ||||||
|             showError("Consistency checks failed! See logs for details.", 50 * 60000); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function connectWebSocket() { |  | ||||||
|         const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws'; |  | ||||||
|  |  | ||||||
|         // use wss for secure messaging |  | ||||||
|         const ws = new WebSocket(protocol + "://" + location.host); |  | ||||||
|         ws.onopen = event => console.log(now(), "Connected to server with WebSocket"); |  | ||||||
|         ws.onmessage = messageHandler; |  | ||||||
|         ws.onclose = function(){ |  | ||||||
|             // Try to reconnect in 5 seconds |  | ||||||
|             setTimeout(() => connectWebSocket(), 5000); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         return ws; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const ws = connectWebSocket(); |  | ||||||
|  |  | ||||||
|     let lastSyncId = glob.maxSyncIdAtLoad; |  | ||||||
|     let lastPingTs = new Date().getTime(); |  | ||||||
|     let connectionBrokenNotification = null; |  | ||||||
|  |  | ||||||
|     setInterval(async () => { |  | ||||||
|         if (new Date().getTime() - lastPingTs > 30000) { |  | ||||||
|             if (!connectionBrokenNotification) { |  | ||||||
|                 connectionBrokenNotification = $.notify({ |  | ||||||
|                     // options |  | ||||||
|                     message: "Lost connection to server" |  | ||||||
|                 },{ |  | ||||||
|                     // settings |  | ||||||
|                     type: 'danger', |  | ||||||
|                     delay: 100000000 // keep it until we explicitly close it |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         else if (connectionBrokenNotification) { |  | ||||||
|             await connectionBrokenNotification.close(); |  | ||||||
|             connectionBrokenNotification = null; |  | ||||||
|  |  | ||||||
|             showMessage("Re-connected to server"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         ws.send(JSON.stringify({ |  | ||||||
|             type: 'ping', |  | ||||||
|             lastSyncId: lastSyncId |  | ||||||
|         })); |  | ||||||
|     }, 1000); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         logError |  | ||||||
|     }; |  | ||||||
| })(); |  | ||||||
| @@ -1,9 +1,9 @@ | |||||||
| "use strict"; | import server from './services/server.js'; | ||||||
|  |  | ||||||
| $(document).ready(() => { | $(document).ready(async () => { | ||||||
|     server.get('migration').then(result => { |     const {appDbVersion, dbVersion} = await server.get('migration'); | ||||||
|         const appDbVersion = result.app_db_version; |  | ||||||
|         const dbVersion = result.db_version; |     console.log("HI", {appDbVersion, dbVersion}); | ||||||
|  |  | ||||||
|     if (appDbVersion === dbVersion) { |     if (appDbVersion === dbVersion) { | ||||||
|         $("#up-to-date").show(); |         $("#up-to-date").show(); | ||||||
| @@ -15,7 +15,6 @@ $(document).ready(() => { | |||||||
|         $("#db-version").html(dbVersion); |         $("#db-version").html(dbVersion); | ||||||
|     } |     } | ||||||
| }); | }); | ||||||
| }); |  | ||||||
|  |  | ||||||
| $("#run-migration").click(async () => { | $("#run-migration").click(async () => { | ||||||
|     $("#run-migration").prop("disabled", true); |     $("#run-migration").prop("disabled", true); | ||||||
| @@ -26,7 +25,7 @@ $("#run-migration").click(async () => { | |||||||
|  |  | ||||||
|     for (const migration of result.migrations) { |     for (const migration of result.migrations) { | ||||||
|         const row = $('<tr>') |         const row = $('<tr>') | ||||||
|             .append($('<td>').html(migration.db_version)) |             .append($('<td>').html(migration.dbVersion)) | ||||||
|             .append($('<td>').html(migration.name)) |             .append($('<td>').html(migration.name)) | ||||||
|             .append($('<td>').html(migration.success ? 'Yes' : 'No')) |             .append($('<td>').html(migration.success ? 'Yes' : 'No')) | ||||||
|             .append($('<td>').html(migration.success ? 'N/A' : migration.error)); |             .append($('<td>').html(migration.success ? 'N/A' : migration.error)); | ||||||
| @@ -38,3 +37,10 @@ $("#run-migration").click(async () => { | |||||||
|         $("#migration-table").append(row); |         $("#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,373 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const noteEditor = (function() { |  | ||||||
|     const $noteTitle = $("#note-title"); |  | ||||||
|  |  | ||||||
|     const $noteDetail = $('#note-detail'); |  | ||||||
|     const $noteDetailCode = $('#note-detail-code'); |  | ||||||
|     const $noteDetailRender = $('#note-detail-render'); |  | ||||||
|     const $noteDetailAttachment = $('#note-detail-attachment'); |  | ||||||
|  |  | ||||||
|     const $protectButton = $("#protect-button"); |  | ||||||
|     const $unprotectButton = $("#unprotect-button"); |  | ||||||
|     const $noteDetailWrapper = $("#note-detail-wrapper"); |  | ||||||
|     const $noteIdDisplay = $("#note-id-display"); |  | ||||||
|     const $attributeList = $("#attribute-list"); |  | ||||||
|     const $attributeListInner = $("#attribute-list-inner"); |  | ||||||
|     const $attachmentFileName = $("#attachment-filename"); |  | ||||||
|     const $attachmentFileType = $("#attachment-filetype"); |  | ||||||
|     const $attachmentFileSize = $("#attachment-filesize"); |  | ||||||
|     const $attachmentDownload = $("#attachment-download"); |  | ||||||
|     const $attachmentOpen = $("#attachment-open"); |  | ||||||
|  |  | ||||||
|     let editor = null; |  | ||||||
|     let codeEditor = null; |  | ||||||
|  |  | ||||||
|     let currentNote = null; |  | ||||||
|  |  | ||||||
|     let noteChangeDisabled = false; |  | ||||||
|  |  | ||||||
|     let isNoteChanged = false; |  | ||||||
|  |  | ||||||
|     function getCurrentNote() { |  | ||||||
|         return currentNote; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getCurrentNoteId() { |  | ||||||
|         return currentNote ? currentNote.detail.noteId : null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function noteChanged() { |  | ||||||
|         if (noteChangeDisabled) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         isNoteChanged = true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function reload() { |  | ||||||
|         // no saving here |  | ||||||
|  |  | ||||||
|         await loadNoteToEditor(getCurrentNoteId()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function switchToNote(noteId) { |  | ||||||
|         if (getCurrentNoteId() !== noteId) { |  | ||||||
|             await saveNoteIfChanged(); |  | ||||||
|  |  | ||||||
|             await loadNoteToEditor(noteId); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function saveNoteIfChanged() { |  | ||||||
|         if (!isNoteChanged) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const note = noteEditor.getCurrentNote(); |  | ||||||
|  |  | ||||||
|         updateNoteFromInputs(note); |  | ||||||
|  |  | ||||||
|         await saveNoteToServer(note); |  | ||||||
|  |  | ||||||
|         if (note.detail.isProtected) { |  | ||||||
|             protected_session.touchProtectedSession(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function updateNoteFromInputs(note) { |  | ||||||
|         if (note.detail.type === 'text') { |  | ||||||
|             let content = editor.getData(); |  | ||||||
|  |  | ||||||
|             // if content is only tags/whitespace (typically <p> </p>), then just make it empty |  | ||||||
|             // this is important when setting new note to code |  | ||||||
|             if (jQuery(content).text().trim() === '' && !content.includes("<img")) { |  | ||||||
|                 content = ''; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             note.detail.content = content; |  | ||||||
|         } |  | ||||||
|         else if (note.detail.type === 'code') { |  | ||||||
|             note.detail.content = codeEditor.getValue(); |  | ||||||
|         } |  | ||||||
|         else if (note.detail.type === 'render' || note.detail.type === 'file') { |  | ||||||
|             // nothing |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             throwError("Unrecognized type: " + note.detail.type); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const title = $noteTitle.val(); |  | ||||||
|  |  | ||||||
|         note.detail.title = title; |  | ||||||
|  |  | ||||||
|         noteTree.setNoteTitle(note.detail.noteId, title); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function saveNoteToServer(note) { |  | ||||||
|         await server.put('notes/' + note.detail.noteId, note); |  | ||||||
|  |  | ||||||
|         isNoteChanged = false; |  | ||||||
|  |  | ||||||
|         showMessage("Saved!"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function setNoteBackgroundIfProtected(note) { |  | ||||||
|         const isProtected = !!note.detail.isProtected; |  | ||||||
|  |  | ||||||
|         $noteDetailWrapper.toggleClass("protected", isProtected); |  | ||||||
|         $protectButton.toggle(!isProtected); |  | ||||||
|         $unprotectButton.toggle(isProtected); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let isNewNoteCreated = false; |  | ||||||
|  |  | ||||||
|     function newNoteCreated() { |  | ||||||
|         isNewNoteCreated = true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function setContent(content) { |  | ||||||
|         if (currentNote.detail.type === 'text') { |  | ||||||
|             if (!editor) { |  | ||||||
|                 await requireLibrary(CKEDITOR); |  | ||||||
|  |  | ||||||
|                 editor = await BalloonEditor.create($noteDetail[0], {}); |  | ||||||
|  |  | ||||||
|                 editor.document.on('change', noteChanged); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49 |  | ||||||
|             editor.setData(content ? content : "<p></p>"); |  | ||||||
|  |  | ||||||
|             $noteDetail.show(); |  | ||||||
|         } |  | ||||||
|         else if (currentNote.detail.type === 'code') { |  | ||||||
|             if (!codeEditor) { |  | ||||||
|                 await requireLibrary(CODE_MIRROR); |  | ||||||
|  |  | ||||||
|                 CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; |  | ||||||
|                 CodeMirror.keyMap.default["Tab"] = "indentMore"; |  | ||||||
|  |  | ||||||
|                 CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; |  | ||||||
|  |  | ||||||
|                 codeEditor = CodeMirror($("#note-detail-code")[0], { |  | ||||||
|                     value: "", |  | ||||||
|                     viewportMargin: Infinity, |  | ||||||
|                     indentUnit: 4, |  | ||||||
|                     matchBrackets: true, |  | ||||||
|                     matchTags: { bothTags: true }, |  | ||||||
|                     highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }, |  | ||||||
|                     lint: true, |  | ||||||
|                     gutters: ["CodeMirror-lint-markers"], |  | ||||||
|                     lineNumbers: true |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|                 codeEditor.on('change', noteChanged); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             $noteDetailCode.show(); |  | ||||||
|  |  | ||||||
|             // this needs to happen after the element is shown, otherwise the editor won't be refresheds |  | ||||||
|             codeEditor.setValue(content); |  | ||||||
|  |  | ||||||
|             const info = CodeMirror.findModeByMIME(currentNote.detail.mime); |  | ||||||
|  |  | ||||||
|             if (info) { |  | ||||||
|                 codeEditor.setOption("mode", info.mime); |  | ||||||
|                 CodeMirror.autoLoadMode(codeEditor, info.mode); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             codeEditor.refresh(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function loadNoteToEditor(noteId) { |  | ||||||
|         currentNote = await loadNote(noteId); |  | ||||||
|  |  | ||||||
|         if (isNewNoteCreated) { |  | ||||||
|             isNewNoteCreated = false; |  | ||||||
|  |  | ||||||
|             $noteTitle.focus().select(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         $noteIdDisplay.html(noteId); |  | ||||||
|  |  | ||||||
|         await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false); |  | ||||||
|  |  | ||||||
|         if (currentNote.detail.isProtected) { |  | ||||||
|             protected_session.touchProtectedSession(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // this might be important if we focused on protected note when not in protected note and we got a dialog |  | ||||||
|         // to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it. |  | ||||||
|         protected_session.ensureDialogIsClosed(); |  | ||||||
|  |  | ||||||
|         $noteDetailWrapper.show(); |  | ||||||
|  |  | ||||||
|         noteChangeDisabled = true; |  | ||||||
|  |  | ||||||
|         $noteTitle.val(currentNote.detail.title); |  | ||||||
|  |  | ||||||
|         noteType.setNoteType(currentNote.detail.type); |  | ||||||
|         noteType.setNoteMime(currentNote.detail.mime); |  | ||||||
|  |  | ||||||
|         $noteDetail.hide(); |  | ||||||
|         $noteDetailCode.hide(); |  | ||||||
|         $noteDetailRender.html('').hide(); |  | ||||||
|         $noteDetailAttachment.hide(); |  | ||||||
|  |  | ||||||
|         if (currentNote.detail.type === 'render') { |  | ||||||
|             $noteDetailRender.show(); |  | ||||||
|  |  | ||||||
|             const bundle = await server.get('script/bundle/' + getCurrentNoteId()); |  | ||||||
|  |  | ||||||
|             $noteDetailRender.html(bundle.html); |  | ||||||
|  |  | ||||||
|             executeBundle(bundle); |  | ||||||
|         } |  | ||||||
|         else if (currentNote.detail.type === 'file') { |  | ||||||
|             $noteDetailAttachment.show(); |  | ||||||
|  |  | ||||||
|             $attachmentFileName.text(currentNote.attributes.original_file_name); |  | ||||||
|             $attachmentFileSize.text(currentNote.attributes.file_size + " bytes"); |  | ||||||
|             $attachmentFileType.text(currentNote.detail.mime); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             await setContent(currentNote.detail.content); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         noteChangeDisabled = false; |  | ||||||
|  |  | ||||||
|         setNoteBackgroundIfProtected(currentNote); |  | ||||||
|         noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId); |  | ||||||
|  |  | ||||||
|         // after loading new note make sure editor is scrolled to the top |  | ||||||
|         $noteDetailWrapper.scrollTop(0); |  | ||||||
|  |  | ||||||
|         loadAttributeList(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function loadAttributeList() { |  | ||||||
|         const noteId = getCurrentNoteId(); |  | ||||||
|  |  | ||||||
|         const attributes = await server.get('notes/' + noteId + '/attributes'); |  | ||||||
|  |  | ||||||
|         $attributeListInner.html(''); |  | ||||||
|  |  | ||||||
|         if (attributes.length > 0) { |  | ||||||
|             for (const attr of attributes) { |  | ||||||
|                 $attributeListInner.append(formatAttribute(attr) + " "); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             $attributeList.show(); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             $attributeList.hide(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function loadNote(noteId) { |  | ||||||
|         return await server.get('notes/' + noteId); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getEditor() { |  | ||||||
|         return editor; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function focus() { |  | ||||||
|         const note = getCurrentNote(); |  | ||||||
|  |  | ||||||
|         if (note.detail.type === 'text') { |  | ||||||
|             $noteDetail.focus(); |  | ||||||
|         } |  | ||||||
|         else if (note.detail.type === 'code') { |  | ||||||
|             codeEditor.focus(); |  | ||||||
|         } |  | ||||||
|         else if (note.detail.type === 'render' || note.detail.type === 'file') { |  | ||||||
|             // do nothing |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             throwError('Unrecognized type: ' + note.detail.type); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getCurrentNoteType() { |  | ||||||
|         const currentNote = getCurrentNote(); |  | ||||||
|  |  | ||||||
|         return currentNote ? currentNote.detail.type : null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function executeCurrentNote() { |  | ||||||
|         if (getCurrentNoteType() === 'code') { |  | ||||||
|             // make sure note is saved so we load latest changes |  | ||||||
|             await saveNoteIfChanged(); |  | ||||||
|  |  | ||||||
|             if (currentNote.detail.mime.endsWith("env=frontend")) { |  | ||||||
|                 const bundle = await server.get('script/bundle/' + getCurrentNoteId()); |  | ||||||
|  |  | ||||||
|                 executeBundle(bundle); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (currentNote.detail.mime.endsWith("env=backend")) { |  | ||||||
|                 await server.post('script/run/' + getCurrentNoteId()); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             showMessage("Note executed"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $attachmentDownload.click(() => download(getAttachmentUrl())); |  | ||||||
|  |  | ||||||
|     $attachmentOpen.click(() => { |  | ||||||
|         if (isElectron()) { |  | ||||||
|             const open = require("open"); |  | ||||||
|  |  | ||||||
|             open(getAttachmentUrl()); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             window.location.href = getAttachmentUrl(); |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     function getAttachmentUrl() { |  | ||||||
|         // electron needs absolute URL so we extract current host, port, protocol |  | ||||||
|         return getHost() + "/api/attachments/download/" + getCurrentNoteId() |  | ||||||
|             + "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $(document).ready(() => { |  | ||||||
|         $noteTitle.on('input', () => { |  | ||||||
|             noteChanged(); |  | ||||||
|  |  | ||||||
|             const title = $noteTitle.val(); |  | ||||||
|  |  | ||||||
|             noteTree.setNoteTitle(getCurrentNoteId(), title); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         // so that tab jumps from note title (which has tabindex 1) |  | ||||||
|         $noteDetail.attr("tabindex", 2); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     $(document).bind('keydown', "ctrl+return", executeCurrentNote); |  | ||||||
|  |  | ||||||
|     setInterval(saveNoteIfChanged, 5000); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         reload, |  | ||||||
|         switchToNote, |  | ||||||
|         saveNoteIfChanged, |  | ||||||
|         updateNoteFromInputs, |  | ||||||
|         saveNoteToServer, |  | ||||||
|         setNoteBackgroundIfProtected, |  | ||||||
|         loadNote, |  | ||||||
|         getCurrentNote, |  | ||||||
|         getCurrentNoteType, |  | ||||||
|         getCurrentNoteId, |  | ||||||
|         newNoteCreated, |  | ||||||
|         getEditor, |  | ||||||
|         focus, |  | ||||||
|         executeCurrentNote, |  | ||||||
|         loadAttributeList, |  | ||||||
|         setContent |  | ||||||
|     }; |  | ||||||
| })(); |  | ||||||
| @@ -1,916 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const noteTree = (function() { |  | ||||||
|     const $tree = $("#tree"); |  | ||||||
|     const $parentList = $("#parent-list"); |  | ||||||
|     const $parentListList = $("#parent-list-inner"); |  | ||||||
|  |  | ||||||
|     let instanceName = null; // should have better place |  | ||||||
|  |  | ||||||
|     let startNotePath = null; |  | ||||||
|     let notesTreeMap = {}; |  | ||||||
|  |  | ||||||
|     let parentToChildren = {}; |  | ||||||
|     let childToParents = {}; |  | ||||||
|  |  | ||||||
|     let parentChildToNoteTreeId = {}; |  | ||||||
|     let noteIdToTitle = {}; |  | ||||||
|  |  | ||||||
|     let hiddenInAutocomplete = {}; |  | ||||||
|  |  | ||||||
|     function getNoteTreeId(parentNoteId, childNoteId) { |  | ||||||
|         assertArguments(parentNoteId, childNoteId); |  | ||||||
|  |  | ||||||
|         const key = parentNoteId + "-" + childNoteId; |  | ||||||
|  |  | ||||||
|         // this can return undefined and client code should deal with it somehow |  | ||||||
|  |  | ||||||
|         return parentChildToNoteTreeId[key]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getNoteTitle(noteId, parentNoteId = null) { |  | ||||||
|         assertArguments(noteId); |  | ||||||
|  |  | ||||||
|         let title = noteIdToTitle[noteId]; |  | ||||||
|  |  | ||||||
|         if (!title) { |  | ||||||
|             throwError("Can't find title for noteId='" + noteId + "'"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (parentNoteId !== null) { |  | ||||||
|             const noteTreeId = getNoteTreeId(parentNoteId, noteId); |  | ||||||
|  |  | ||||||
|             if (noteTreeId) { |  | ||||||
|                 const noteTree = notesTreeMap[noteTreeId]; |  | ||||||
|  |  | ||||||
|                 if (noteTree.prefix) { |  | ||||||
|                     title = noteTree.prefix + ' - ' + title; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return title; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // note that if you want to access data like noteId or isProtected, you need to go into "data" property |  | ||||||
|     function getCurrentNode() { |  | ||||||
|         return $tree.fancytree("getActiveNode"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getCurrentNotePath() { |  | ||||||
|         const node = getCurrentNode(); |  | ||||||
|  |  | ||||||
|         return treeUtils.getNotePath(node); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getNodesByNoteTreeId(noteTreeId) { |  | ||||||
|         assertArguments(noteTreeId); |  | ||||||
|  |  | ||||||
|         const noteTree = notesTreeMap[noteTreeId]; |  | ||||||
|  |  | ||||||
|         return getNodesByNoteId(noteTree.noteId).filter(node => node.data.noteTreeId === noteTreeId); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getNodesByNoteId(noteId) { |  | ||||||
|         assertArguments(noteId); |  | ||||||
|  |  | ||||||
|         const list = getTree().getNodesByRef(noteId); |  | ||||||
|         return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function setPrefix(noteTreeId, prefix) { |  | ||||||
|         assertArguments(noteTreeId); |  | ||||||
|  |  | ||||||
|         notesTreeMap[noteTreeId].prefix = prefix; |  | ||||||
|  |  | ||||||
|         getNodesByNoteTreeId(noteTreeId).map(node => { |  | ||||||
|             node.data.prefix = prefix; |  | ||||||
|  |  | ||||||
|             treeUtils.setNodeTitleWithPrefix(node); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function removeParentChildRelation(parentNoteId, childNoteId) { |  | ||||||
|         assertArguments(parentNoteId, childNoteId); |  | ||||||
|  |  | ||||||
|         const key = parentNoteId + "-" + childNoteId; |  | ||||||
|  |  | ||||||
|         delete parentChildToNoteTreeId[key]; |  | ||||||
|  |  | ||||||
|         parentToChildren[parentNoteId] = parentToChildren[parentNoteId].filter(noteId => noteId !== childNoteId); |  | ||||||
|         childToParents[childNoteId] = childToParents[childNoteId].filter(noteId => noteId !== parentNoteId); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function setParentChildRelation(noteTreeId, parentNoteId, childNoteId) { |  | ||||||
|         assertArguments(noteTreeId, parentNoteId, childNoteId); |  | ||||||
|  |  | ||||||
|         const key = parentNoteId + "-" + childNoteId; |  | ||||||
|  |  | ||||||
|         parentChildToNoteTreeId[key] = noteTreeId; |  | ||||||
|  |  | ||||||
|         if (!parentToChildren[parentNoteId]) { |  | ||||||
|             parentToChildren[parentNoteId] = []; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         parentToChildren[parentNoteId].push(childNoteId); |  | ||||||
|  |  | ||||||
|         if (!childToParents[childNoteId]) { |  | ||||||
|             childToParents[childNoteId] = []; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         childToParents[childNoteId].push(parentNoteId); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function prepareNoteTree(notes) { |  | ||||||
|         assertArguments(notes); |  | ||||||
|  |  | ||||||
|         parentToChildren = {}; |  | ||||||
|         childToParents = {}; |  | ||||||
|         notesTreeMap = {}; |  | ||||||
|  |  | ||||||
|         for (const note of notes) { |  | ||||||
|             notesTreeMap[note.noteTreeId] = note; |  | ||||||
|  |  | ||||||
|             noteIdToTitle[note.noteId] = note.title; |  | ||||||
|  |  | ||||||
|             delete note.title; // this should not be used. Use noteIdToTitle instead |  | ||||||
|  |  | ||||||
|             setParentChildRelation(note.noteTreeId, note.parentNoteId, note.noteId); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return prepareNoteTreeInner('root'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getExtraClasses(note) { |  | ||||||
|         assertArguments(note); |  | ||||||
|  |  | ||||||
|         const extraClasses = []; |  | ||||||
|  |  | ||||||
|         if (note.isProtected) { |  | ||||||
|             extraClasses.push("protected"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (childToParents[note.noteId].length > 1) { |  | ||||||
|             extraClasses.push("multiple-parents"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (note.type === 'code') { |  | ||||||
|             extraClasses.push("code"); |  | ||||||
|         } |  | ||||||
|         else if (note.type === 'render') { |  | ||||||
|             extraClasses.push('render'); |  | ||||||
|         } |  | ||||||
|         else if (note.type === 'file') { |  | ||||||
|             extraClasses.push('attachment'); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return extraClasses.join(" "); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function prepareNoteTreeInner(parentNoteId) { |  | ||||||
|         assertArguments(parentNoteId); |  | ||||||
|  |  | ||||||
|         const childNoteIds = parentToChildren[parentNoteId]; |  | ||||||
|         if (!childNoteIds) { |  | ||||||
|             messaging.logError("No children for " + parentNoteId + ". This shouldn't happen."); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const noteList = []; |  | ||||||
|  |  | ||||||
|         for (const noteId of childNoteIds) { |  | ||||||
|             const noteTreeId = getNoteTreeId(parentNoteId, noteId); |  | ||||||
|             const noteTree = notesTreeMap[noteTreeId]; |  | ||||||
|  |  | ||||||
|             const title = (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.noteId]; |  | ||||||
|  |  | ||||||
|             const node = { |  | ||||||
|                 noteId: noteTree.noteId, |  | ||||||
|                 parentNoteId: noteTree.parentNoteId, |  | ||||||
|                 noteTreeId: noteTree.noteTreeId, |  | ||||||
|                 isProtected: noteTree.isProtected, |  | ||||||
|                 prefix: noteTree.prefix, |  | ||||||
|                 title: escapeHtml(title), |  | ||||||
|                 extraClasses: getExtraClasses(noteTree), |  | ||||||
|                 refKey: noteTree.noteId, |  | ||||||
|                 expanded: noteTree.isExpanded |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             if (parentToChildren[noteId] && parentToChildren[noteId].length > 0) { |  | ||||||
|                 node.folder = true; |  | ||||||
|  |  | ||||||
|                 if (node.expanded) { |  | ||||||
|                     node.children = prepareNoteTreeInner(noteId); |  | ||||||
|                 } |  | ||||||
|                 else { |  | ||||||
|                     node.lazy = true; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             noteList.push(node); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return noteList; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function expandToNote(notePath, expandOpts) { |  | ||||||
|         assertArguments(notePath); |  | ||||||
|  |  | ||||||
|         const runPath = getRunPath(notePath); |  | ||||||
|  |  | ||||||
|         const noteId = treeUtils.getNoteIdFromNotePath(notePath); |  | ||||||
|  |  | ||||||
|         let parentNoteId = 'root'; |  | ||||||
|  |  | ||||||
|         for (const childNoteId of runPath) { |  | ||||||
|             const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId); |  | ||||||
|  |  | ||||||
|             if (childNoteId === noteId) { |  | ||||||
|                 return node; |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 await node.setExpanded(true, expandOpts); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             parentNoteId = childNoteId; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function activateNode(notePath) { |  | ||||||
|         assertArguments(notePath); |  | ||||||
|  |  | ||||||
|         const node = await expandToNote(notePath); |  | ||||||
|  |  | ||||||
|         await node.setActive(); |  | ||||||
|  |  | ||||||
|         clearSelectedNodes(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Accepts notePath and tries to resolve it. Part of the path might not be valid because of note moving (which causes |  | ||||||
|      * path change) or other corruption, in that case this will try to get some other valid path to the correct note. |  | ||||||
|      */ |  | ||||||
|     function getRunPath(notePath) { |  | ||||||
|         assertArguments(notePath); |  | ||||||
|  |  | ||||||
|         const path = notePath.split("/").reverse(); |  | ||||||
|         path.push('root'); |  | ||||||
|  |  | ||||||
|         const effectivePath = []; |  | ||||||
|         let childNoteId = null; |  | ||||||
|         let i = 0; |  | ||||||
|  |  | ||||||
|         while (true) { |  | ||||||
|             if (i >= path.length) { |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             const parentNoteId = path[i++]; |  | ||||||
|  |  | ||||||
|             if (childNoteId !== null) { |  | ||||||
|                 const parents = childToParents[childNoteId]; |  | ||||||
|  |  | ||||||
|                 if (!parents) { |  | ||||||
|                     messaging.logError("No parents found for " + childNoteId); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (!parents.includes(parentNoteId)) { |  | ||||||
|                     console.log(now(), "Did not find parent " + parentNoteId + " for child " + childNoteId); |  | ||||||
|  |  | ||||||
|                     if (parents.length > 0) { |  | ||||||
|                         console.log(now(), "Available parents:", parents); |  | ||||||
|  |  | ||||||
|                         const someNotePath = getSomeNotePath(parents[0]); |  | ||||||
|  |  | ||||||
|                         if (someNotePath) { // in case it's root the path may be empty |  | ||||||
|                             const pathToRoot = someNotePath.split("/").reverse(); |  | ||||||
|  |  | ||||||
|                             for (const noteId of pathToRoot) { |  | ||||||
|                                 effectivePath.push(noteId); |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         break; |  | ||||||
|                     } |  | ||||||
|                     else { |  | ||||||
|                         messaging.logError("No parents, can't activate node."); |  | ||||||
|                         return; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (parentNoteId === 'root') { |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 effectivePath.push(parentNoteId); |  | ||||||
|                 childNoteId = parentNoteId; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return effectivePath.reverse(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function showParentList(noteId, node) { |  | ||||||
|         assertArguments(noteId, node); |  | ||||||
|  |  | ||||||
|         const parents = childToParents[noteId]; |  | ||||||
|  |  | ||||||
|         if (!parents) { |  | ||||||
|             throwError("Can't find parents for noteId=" + noteId); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (parents.length <= 1) { |  | ||||||
|             $parentList.hide(); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             $parentList.show(); |  | ||||||
|             $parentListList.empty(); |  | ||||||
|  |  | ||||||
|             for (const parentNoteId of parents) { |  | ||||||
|                 const parentNotePath = getSomeNotePath(parentNoteId); |  | ||||||
|                 // this is to avoid having root notes leading '/' |  | ||||||
|                 const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId; |  | ||||||
|                 const title = getNotePathTitle(notePath); |  | ||||||
|  |  | ||||||
|                 let item; |  | ||||||
|  |  | ||||||
|                 if (node.getParent().data.noteId === parentNoteId) { |  | ||||||
|                     item = $("<span/>").attr("title", "Current note").append(title); |  | ||||||
|                 } |  | ||||||
|                 else { |  | ||||||
|                     item = link.createNoteLink(notePath, title); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $parentListList.append($("<li/>").append(item)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getNotePathTitle(notePath) { |  | ||||||
|         assertArguments(notePath); |  | ||||||
|  |  | ||||||
|         const titlePath = []; |  | ||||||
|  |  | ||||||
|         let parentNoteId = 'root'; |  | ||||||
|  |  | ||||||
|         for (const noteId of notePath.split('/')) { |  | ||||||
|             titlePath.push(getNoteTitle(noteId, parentNoteId)); |  | ||||||
|  |  | ||||||
|             parentNoteId = noteId; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return titlePath.join(' / '); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getSomeNotePath(noteId) { |  | ||||||
|         assertArguments(noteId); |  | ||||||
|  |  | ||||||
|         const path = []; |  | ||||||
|  |  | ||||||
|         let cur = noteId; |  | ||||||
|  |  | ||||||
|         while (cur !== 'root') { |  | ||||||
|             path.push(cur); |  | ||||||
|  |  | ||||||
|             if (!childToParents[cur]) { |  | ||||||
|                 throwError("Can't find parents for " + cur); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             cur = childToParents[cur][0]; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return path.reverse().join('/'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function setExpandedToServer(noteTreeId, isExpanded) { |  | ||||||
|         assertArguments(noteTreeId); |  | ||||||
|  |  | ||||||
|         const expandedNum = isExpanded ? 1 : 0; |  | ||||||
|  |  | ||||||
|         await server.put('tree/' + noteTreeId + '/expanded/' + expandedNum); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function setCurrentNotePathToHash(node) { |  | ||||||
|         assertArguments(node); |  | ||||||
|  |  | ||||||
|         const currentNotePath = treeUtils.getNotePath(node); |  | ||||||
|         const currentNoteTreeId = node.data.noteTreeId; |  | ||||||
|  |  | ||||||
|         document.location.hash = currentNotePath; |  | ||||||
|  |  | ||||||
|         recentNotes.addRecentNote(currentNoteTreeId, currentNotePath); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getSelectedNodes(stopOnParents = false) { |  | ||||||
|         return getTree().getSelectedNodes(stopOnParents); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function clearSelectedNodes() { |  | ||||||
|         for (const selectedNode of getSelectedNodes()) { |  | ||||||
|             selectedNode.setSelected(false); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const currentNode = getCurrentNode(); |  | ||||||
|  |  | ||||||
|         if (currentNode) { |  | ||||||
|             currentNode.setSelected(true); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function initFancyTree(noteTree) { |  | ||||||
|         assertArguments(noteTree); |  | ||||||
|  |  | ||||||
|         const keybindings = { |  | ||||||
|             "del": node => { |  | ||||||
|                 treeChanges.deleteNodes(getSelectedNodes(true)); |  | ||||||
|             }, |  | ||||||
|             "ctrl+up": node => { |  | ||||||
|                 const beforeNode = node.getPrevSibling(); |  | ||||||
|  |  | ||||||
|                 if (beforeNode !== null) { |  | ||||||
|                     treeChanges.moveBeforeNode([node], beforeNode); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "ctrl+down": node => { |  | ||||||
|                 let afterNode = node.getNextSibling(); |  | ||||||
|                 if (afterNode !== null) { |  | ||||||
|                     treeChanges.moveAfterNode([node], afterNode); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "ctrl+left": node => { |  | ||||||
|                 treeChanges.moveNodeUpInHierarchy(node); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "ctrl+right": node => { |  | ||||||
|                 let toNode = node.getPrevSibling(); |  | ||||||
|  |  | ||||||
|                 if (toNode !== null) { |  | ||||||
|                     treeChanges.moveToNode([node], toNode); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "shift+up": node => { |  | ||||||
|                 node.navigate($.ui.keyCode.UP, true).then(() => { |  | ||||||
|                     const currentNode = getCurrentNode(); |  | ||||||
|  |  | ||||||
|                     if (currentNode.isSelected()) { |  | ||||||
|                         node.setSelected(false); |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     currentNode.setSelected(true); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "shift+down": node => { |  | ||||||
|                 node.navigate($.ui.keyCode.DOWN, true).then(() => { |  | ||||||
|                     const currentNode = getCurrentNode(); |  | ||||||
|  |  | ||||||
|                     if (currentNode.isSelected()) { |  | ||||||
|                         node.setSelected(false); |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     currentNode.setSelected(true); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "f2": node => { |  | ||||||
|                 editTreePrefix.showDialog(node); |  | ||||||
|             }, |  | ||||||
|             "alt+-": node => { |  | ||||||
|                 collapseTree(node); |  | ||||||
|             }, |  | ||||||
|             "alt+s": node => { |  | ||||||
|                 sortAlphabetically(node.data.noteId); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "ctrl+a": node => { |  | ||||||
|                 for (const child of node.getParent().getChildren()) { |  | ||||||
|                     child.setSelected(true); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "ctrl+c": () => { |  | ||||||
|                 contextMenu.copy(getSelectedNodes()); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "ctrl+x": () => { |  | ||||||
|                 contextMenu.cut(getSelectedNodes()); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "ctrl+v": node => { |  | ||||||
|                 contextMenu.pasteInto(node); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "return": node => { |  | ||||||
|                 noteEditor.focus(); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "backspace": node => { |  | ||||||
|                 if (!isTopLevelNode(node)) { |  | ||||||
|                     node.getParent().setActive().then(() => clearSelectedNodes()); |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             // code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin |  | ||||||
|             // after opening context menu, standard shortcuts don't work, but they are detected here |  | ||||||
|             // so we essentially takeover the standard handling with our implementation. |  | ||||||
|             "left": node => { |  | ||||||
|                 node.navigate($.ui.keyCode.LEFT, true).then(() => clearSelectedNodes()); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "right": node => { |  | ||||||
|                 node.navigate($.ui.keyCode.RIGHT, true).then(() => clearSelectedNodes()); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "up": node => { |  | ||||||
|                 node.navigate($.ui.keyCode.UP, true).then(() => clearSelectedNodes()); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             }, |  | ||||||
|             "down": node => { |  | ||||||
|                 node.navigate($.ui.keyCode.DOWN, true).then(() => clearSelectedNodes()); |  | ||||||
|  |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         $tree.fancytree({ |  | ||||||
|             autoScroll: true, |  | ||||||
|             keyboard: false, // we takover keyboard handling in the hotkeys plugin |  | ||||||
|             extensions: ["hotkeys", "filter", "dnd", "clones"], |  | ||||||
|             source: noteTree, |  | ||||||
|             scrollParent: $("#tree"), |  | ||||||
|             click: (event, data) => { |  | ||||||
|                 const targetType = data.targetType; |  | ||||||
|                 const node = data.node; |  | ||||||
|  |  | ||||||
|                 if (targetType === 'title' || targetType === 'icon') { |  | ||||||
|                     if (!event.ctrlKey) { |  | ||||||
|                         node.setActive(); |  | ||||||
|                         node.setSelected(true); |  | ||||||
|  |  | ||||||
|                         clearSelectedNodes(); |  | ||||||
|                     } |  | ||||||
|                     else { |  | ||||||
|                         node.setSelected(!node.isSelected()); |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             activate: (event, data) => { |  | ||||||
|                 const node = data.node.data; |  | ||||||
|  |  | ||||||
|                 setCurrentNotePathToHash(data.node); |  | ||||||
|  |  | ||||||
|                 noteEditor.switchToNote(node.noteId); |  | ||||||
|  |  | ||||||
|                 showParentList(node.noteId, data.node); |  | ||||||
|             }, |  | ||||||
|             expand: (event, data) => { |  | ||||||
|                 setExpandedToServer(data.node.data.noteTreeId, true); |  | ||||||
|             }, |  | ||||||
|             collapse: (event, data) => { |  | ||||||
|                 setExpandedToServer(data.node.data.noteTreeId, false); |  | ||||||
|             }, |  | ||||||
|             init: (event, data) => { |  | ||||||
|                 const noteId = treeUtils.getNoteIdFromNotePath(startNotePath); |  | ||||||
|  |  | ||||||
|                 if (noteIdToTitle[noteId] === undefined) { |  | ||||||
|                     // note doesn't exist so don't try to activate it |  | ||||||
|                     startNotePath = null; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (startNotePath) { |  | ||||||
|                     activateNode(startNotePath); |  | ||||||
|  |  | ||||||
|                     // looks like this this doesn't work when triggered immediatelly after activating node |  | ||||||
|                     // so waiting a second helps |  | ||||||
|                     setTimeout(scrollToCurrentNote, 1000); |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             hotkeys: { |  | ||||||
|                 keydown: keybindings |  | ||||||
|             }, |  | ||||||
|             filter: { |  | ||||||
|                 autoApply: true,   // Re-apply last filter if lazy data is loaded |  | ||||||
|                 autoExpand: true, // Expand all branches that contain matches while filtered |  | ||||||
|                 counter: false,     // Show a badge with number of matching child nodes near parent icons |  | ||||||
|                 fuzzy: false,      // Match single characters in order, e.g. 'fb' will match 'FooBar' |  | ||||||
|                 hideExpandedCounter: true,  // Hide counter badge if parent is expanded |  | ||||||
|                 hideExpanders: false,       // Hide expanders if all child nodes are hidden by filter |  | ||||||
|                 highlight: true,   // Highlight matches by wrapping inside <mark> tags |  | ||||||
|                 leavesOnly: false, // Match end nodes only |  | ||||||
|                 nodata: true,      // Display a 'no data' status node if result is empty |  | ||||||
|                 mode: "hide"       // Grayout unmatched nodes (pass "hide" to remove unmatched node instead) |  | ||||||
|             }, |  | ||||||
|             dnd: dragAndDropSetup, |  | ||||||
|             lazyLoad: function(event, data){ |  | ||||||
|                 const node = data.node.data; |  | ||||||
|  |  | ||||||
|                 data.result = prepareNoteTreeInner(node.noteId); |  | ||||||
|             }, |  | ||||||
|             clones: { |  | ||||||
|                 highlightActiveClones: true |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         $tree.contextmenu(contextMenu.contextMenuSettings); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getTree() { |  | ||||||
|         return $tree.fancytree('getTree'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function reload() { |  | ||||||
|         const notes = await loadTree(); |  | ||||||
|  |  | ||||||
|         // this will also reload the note content |  | ||||||
|         await getTree().reload(notes); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getNotePathFromAddress() { |  | ||||||
|         return document.location.hash.substr(1); // strip initial # |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function loadTree() { |  | ||||||
|         const resp = await server.get('tree'); |  | ||||||
|         startNotePath = resp.start_note_path; |  | ||||||
|         instanceName = resp.instanceName; |  | ||||||
|  |  | ||||||
|         if (document.location.hash) { |  | ||||||
|             startNotePath = getNotePathFromAddress(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         hiddenInAutocomplete = {}; |  | ||||||
|  |  | ||||||
|         for (const noteId of resp.hiddenInAutocomplete) { |  | ||||||
|             hiddenInAutocomplete[noteId] = true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return prepareNoteTree(resp.notes); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $(() => loadTree().then(noteTree => initFancyTree(noteTree))); |  | ||||||
|  |  | ||||||
|     function collapseTree(node = null) { |  | ||||||
|         if (!node) { |  | ||||||
|             node = $tree.fancytree("getRootNode"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         node.setExpanded(false); |  | ||||||
|  |  | ||||||
|         node.visit(node => node.setExpanded(false)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument |  | ||||||
|  |  | ||||||
|     function scrollToCurrentNote() { |  | ||||||
|         const node = getCurrentNode(); |  | ||||||
|  |  | ||||||
|         if (node) { |  | ||||||
|             node.makeVisible({scrollIntoView: true}); |  | ||||||
|  |  | ||||||
|             node.setFocus(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function setNoteTreeBackgroundBasedOnProtectedStatus(noteId) { |  | ||||||
|         getNodesByNoteId(noteId).map(node => node.toggleClass("protected", !!node.data.isProtected)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function setProtected(noteId, isProtected) { |  | ||||||
|         getNodesByNoteId(noteId).map(node => node.data.isProtected = isProtected); |  | ||||||
|  |  | ||||||
|         setNoteTreeBackgroundBasedOnProtectedStatus(noteId); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getAutocompleteItems(parentNoteId, notePath, titlePath) { |  | ||||||
|         if (!parentNoteId) { |  | ||||||
|             parentNoteId = 'root'; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!parentToChildren[parentNoteId]) { |  | ||||||
|             return []; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!notePath) { |  | ||||||
|             notePath = ''; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!titlePath) { |  | ||||||
|             titlePath = ''; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // https://github.com/zadam/trilium/issues/46 |  | ||||||
|         // unfortunately not easy to implement because we don't have an easy access to note's isProtected property |  | ||||||
|  |  | ||||||
|         const autocompleteItems = []; |  | ||||||
|  |  | ||||||
|         for (const childNoteId of parentToChildren[parentNoteId]) { |  | ||||||
|             if (hiddenInAutocomplete[childNoteId]) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId; |  | ||||||
|             const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId); |  | ||||||
|  |  | ||||||
|             autocompleteItems.push({ |  | ||||||
|                 value: childTitlePath + ' (' + childNotePath + ')', |  | ||||||
|                 label: childTitlePath |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             const childItems = getAutocompleteItems(childNoteId, childNotePath, childTitlePath); |  | ||||||
|  |  | ||||||
|             for (const childItem of childItems) { |  | ||||||
|                 autocompleteItems.push(childItem); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return autocompleteItems; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function setNoteTitle(noteId, title) { |  | ||||||
|         assertArguments(noteId); |  | ||||||
|  |  | ||||||
|         noteIdToTitle[noteId] = title; |  | ||||||
|  |  | ||||||
|         getNodesByNoteId(noteId).map(clone => treeUtils.setNodeTitleWithPrefix(clone)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function createNewTopLevelNote() { |  | ||||||
|         const rootNode = $tree.fancytree("getRootNode"); |  | ||||||
|  |  | ||||||
|         await createNote(rootNode, "root", "into"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function createNote(node, parentNoteId, target, isProtected) { |  | ||||||
|         assertArguments(node, parentNoteId, target); |  | ||||||
|  |  | ||||||
|         // if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted |  | ||||||
|         // but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often |  | ||||||
|         if (!isProtected || !protected_session.isProtectedSessionAvailable()) { |  | ||||||
|             isProtected = false; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const newNoteName = "new note"; |  | ||||||
|  |  | ||||||
|         const result = await server.post('notes/' + parentNoteId + '/children', { |  | ||||||
|             title: newNoteName, |  | ||||||
|             target: target, |  | ||||||
|             target_noteTreeId: node.data.noteTreeId, |  | ||||||
|             isProtected: isProtected |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         setParentChildRelation(result.noteTreeId, parentNoteId, result.noteId); |  | ||||||
|  |  | ||||||
|         notesTreeMap[result.noteTreeId] = result; |  | ||||||
|  |  | ||||||
|         noteIdToTitle[result.noteId] = newNoteName; |  | ||||||
|  |  | ||||||
|         noteEditor.newNoteCreated(); |  | ||||||
|  |  | ||||||
|         const newNode = { |  | ||||||
|             title: newNoteName, |  | ||||||
|             noteId: result.noteId, |  | ||||||
|             parentNoteId: parentNoteId, |  | ||||||
|             refKey: result.noteId, |  | ||||||
|             noteTreeId: result.noteTreeId, |  | ||||||
|             isProtected: isProtected, |  | ||||||
|             extraClasses: getExtraClasses(result.note) |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         if (target === 'after') { |  | ||||||
|             await node.appendSibling(newNode).setActive(true); |  | ||||||
|         } |  | ||||||
|         else if (target === 'into') { |  | ||||||
|             if (!node.getChildren() && node.isFolder()) { |  | ||||||
|                 await node.setExpanded(); |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 node.addChildren(newNode); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             await node.getLastChild().setActive(true); |  | ||||||
|  |  | ||||||
|             node.folder = true; |  | ||||||
|             node.renderTitle(); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             throwError("Unrecognized target: " + target); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         clearSelectedNodes(); // to unmark previously active node |  | ||||||
|  |  | ||||||
|         showMessage("Created!"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function sortAlphabetically(noteId) { |  | ||||||
|         await server.put('notes/' + noteId + '/sort'); |  | ||||||
|  |  | ||||||
|         await reload(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function noteExists(noteId) { |  | ||||||
|         return !!childToParents[noteId]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getInstanceName() { |  | ||||||
|         return instanceName; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+o', e => { |  | ||||||
|         const node = getCurrentNode(); |  | ||||||
|         const parentNoteId = node.data.parentNoteId; |  | ||||||
|         const isProtected = treeUtils.getParentProtectedStatus(node); |  | ||||||
|  |  | ||||||
|         createNote(node, parentNoteId, 'after', isProtected); |  | ||||||
|  |  | ||||||
|         e.preventDefault(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+p', e => { |  | ||||||
|         const node = getCurrentNode(); |  | ||||||
|  |  | ||||||
|         createNote(node, node.data.noteId, 'into', node.data.isProtected); |  | ||||||
|  |  | ||||||
|         e.preventDefault(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+del', e => { |  | ||||||
|         const node = getCurrentNode(); |  | ||||||
|  |  | ||||||
|         treeChanges.deleteNodes([node]); |  | ||||||
|  |  | ||||||
|         e.preventDefault(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+.', scrollToCurrentNote); |  | ||||||
|  |  | ||||||
|     $(window).bind('hashchange', function() { |  | ||||||
|         const notePath = getNotePathFromAddress(); |  | ||||||
|  |  | ||||||
|         if (getCurrentNotePath() !== notePath) { |  | ||||||
|             console.log("Switching to " + notePath + " because of hash change"); |  | ||||||
|  |  | ||||||
|             activateNode(notePath); |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     if (isElectron()) { |  | ||||||
|         $(document).bind('keydown', 'alt+left', e => { |  | ||||||
|             window.history.back(); |  | ||||||
|  |  | ||||||
|             e.preventDefault(); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         $(document).bind('keydown', 'alt+right', e => { |  | ||||||
|             window.history.forward(); |  | ||||||
|  |  | ||||||
|             e.preventDefault(); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         reload, |  | ||||||
|         collapseTree, |  | ||||||
|         scrollToCurrentNote, |  | ||||||
|         setNoteTreeBackgroundBasedOnProtectedStatus, |  | ||||||
|         setProtected, |  | ||||||
|         getCurrentNode, |  | ||||||
|         expandToNote, |  | ||||||
|         activateNode, |  | ||||||
|         getCurrentNotePath, |  | ||||||
|         getNoteTitle, |  | ||||||
|         setCurrentNotePathToHash, |  | ||||||
|         getAutocompleteItems, |  | ||||||
|         setNoteTitle, |  | ||||||
|         createNewTopLevelNote, |  | ||||||
|         createNote, |  | ||||||
|         setPrefix, |  | ||||||
|         getNotePathTitle, |  | ||||||
|         removeParentChildRelation, |  | ||||||
|         setParentChildRelation, |  | ||||||
|         getSelectedNodes, |  | ||||||
|         sortAlphabetically, |  | ||||||
|         noteExists, |  | ||||||
|         getInstanceName |  | ||||||
|     }; |  | ||||||
| })(); |  | ||||||
| @@ -1,142 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const noteType = (function() { |  | ||||||
|     const $executeScriptButton = $("#execute-script-button"); |  | ||||||
|     const noteTypeModel = new NoteTypeModel(); |  | ||||||
|  |  | ||||||
|     function NoteTypeModel() { |  | ||||||
|         const self = this; |  | ||||||
|  |  | ||||||
|         this.type = ko.observable('text'); |  | ||||||
|         this.mime = ko.observable(''); |  | ||||||
|  |  | ||||||
|         this.codeMimeTypes = ko.observableArray([ |  | ||||||
|             { mime: 'text/x-csrc', title: 'C' }, |  | ||||||
|             { mime: 'text/x-c++src', title: 'C++' }, |  | ||||||
|             { mime: 'text/x-csharp', title: 'C#' }, |  | ||||||
|             { mime: 'text/x-clojure', title: 'Clojure' }, |  | ||||||
|             { mime: 'text/css', title: 'CSS' }, |  | ||||||
|             { mime: 'text/x-dockerfile', title: 'Dockerfile' }, |  | ||||||
|             { mime: 'text/x-erlang', title: 'Erlang' }, |  | ||||||
|             { mime: 'text/x-feature', title: 'Gherkin' }, |  | ||||||
|             { mime: 'text/x-go', title: 'Go' }, |  | ||||||
|             { mime: 'text/x-groovy', title: 'Groovy' }, |  | ||||||
|             { mime: 'text/x-haskell', title: 'Haskell' }, |  | ||||||
|             { mime: 'text/html', title: 'HTML' }, |  | ||||||
|             { mime: 'message/http', title: 'HTTP' }, |  | ||||||
|             { mime: 'text/x-java', title: 'Java' }, |  | ||||||
|             { mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' }, |  | ||||||
|             { mime: 'application/javascript;env=backend', title: 'JavaScript backend' }, |  | ||||||
|             { mime: 'application/json', title: 'JSON' }, |  | ||||||
|             { mime: 'text/x-kotlin', title: 'Kotlin' }, |  | ||||||
|             { mime: 'text/x-lua', title: 'Lua' }, |  | ||||||
|             { mime: 'text/x-markdown', title: 'Markdown' }, |  | ||||||
|             { mime: 'text/x-objectivec', title: 'Objective C' }, |  | ||||||
|             { mime: 'text/x-pascal', title: 'Pascal' }, |  | ||||||
|             { mime: 'text/x-perl', title: 'Perl' }, |  | ||||||
|             { mime: 'text/x-php', title: 'PHP' }, |  | ||||||
|             { mime: 'text/x-python', title: 'Python' }, |  | ||||||
|             { mime: 'text/x-ruby', title: 'Ruby' }, |  | ||||||
|             { mime: 'text/x-rustsrc', title: 'Rust' }, |  | ||||||
|             { mime: 'text/x-scala', title: 'Scala' }, |  | ||||||
|             { mime: 'text/x-sh', title: 'Shell' }, |  | ||||||
|             { mime: 'text/x-sql', title: 'SQL' }, |  | ||||||
|             { mime: 'text/x-swift', title: 'Swift' }, |  | ||||||
|             { mime: 'text/xml', title: 'XML' }, |  | ||||||
|             { mime: 'text/x-yaml', title: 'YAML' } |  | ||||||
|         ]); |  | ||||||
|  |  | ||||||
|         this.typeString = function() { |  | ||||||
|             const type = self.type(); |  | ||||||
|             const mime = self.mime(); |  | ||||||
|  |  | ||||||
|             if (type === 'text') { |  | ||||||
|                 return 'Text'; |  | ||||||
|             } |  | ||||||
|             else if (type === 'code') { |  | ||||||
|                 if (!mime) { |  | ||||||
|                     return 'Code'; |  | ||||||
|                 } |  | ||||||
|                 else { |  | ||||||
|                     const found = self.codeMimeTypes().find(x => x.mime === mime); |  | ||||||
|  |  | ||||||
|                     return found ? found.title : mime; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             else if (type === 'render') { |  | ||||||
|                 return 'Render HTML note'; |  | ||||||
|             } |  | ||||||
|             else if (type === 'file') { |  | ||||||
|                 return 'Attachment'; |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 throwError('Unrecognized type: ' + type); |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         this.isDisabled = function() { |  | ||||||
|             return self.type() === "file"; |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         async function save() { |  | ||||||
|             const note = noteEditor.getCurrentNote(); |  | ||||||
|  |  | ||||||
|             await server.put('notes/' + note.detail.noteId |  | ||||||
|                 + '/type/' + encodeURIComponent(self.type()) |  | ||||||
|                 + '/mime/' + encodeURIComponent(self.mime())); |  | ||||||
|  |  | ||||||
|             await noteEditor.reload(); |  | ||||||
|  |  | ||||||
|             // for the note icon to be updated in the tree |  | ||||||
|             await noteTree.reload(); |  | ||||||
|  |  | ||||||
|             self.updateExecuteScriptButtonVisibility(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         this.selectText = function() { |  | ||||||
|             self.type('text'); |  | ||||||
|             self.mime(''); |  | ||||||
|  |  | ||||||
|             save(); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         this.selectRender = function() { |  | ||||||
|             self.type('render'); |  | ||||||
|             self.mime(''); |  | ||||||
|  |  | ||||||
|             save(); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         this.selectCode = function() { |  | ||||||
|             self.type('code'); |  | ||||||
|             self.mime(''); |  | ||||||
|  |  | ||||||
|             save(); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         this.selectCodeMime = function(el) { |  | ||||||
|             self.type('code'); |  | ||||||
|             self.mime(el.mime); |  | ||||||
|  |  | ||||||
|             save(); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         this.updateExecuteScriptButtonVisibility = function() { |  | ||||||
|             $executeScriptButton.toggle(self.mime().startsWith('application/javascript')); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     ko.applyBindings(noteTypeModel, document.getElementById('note-type')); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         getNoteType: () => noteTypeModel.type(), |  | ||||||
|         setNoteType: type => noteTypeModel.type(type), |  | ||||||
|  |  | ||||||
|         getNoteMime: () => noteTypeModel.mime(), |  | ||||||
|         setNoteMime: mime => { |  | ||||||
|             noteTypeModel.mime(mime); |  | ||||||
|  |  | ||||||
|             noteTypeModel.updateExecuteScriptButtonVisibility(); |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
| })(); |  | ||||||
| @@ -1,184 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const protected_session = (function() { |  | ||||||
|     const $dialog = $("#protected-session-password-dialog"); |  | ||||||
|     const $passwordForm = $("#protected-session-password-form"); |  | ||||||
|     const $password = $("#protected-session-password"); |  | ||||||
|     const $noteDetailWrapper = $("#note-detail-wrapper"); |  | ||||||
|  |  | ||||||
|     let protectedSessionDeferred = null; |  | ||||||
|     let lastProtectedSessionOperationDate = null; |  | ||||||
|     let protectedSessionTimeout = null; |  | ||||||
|     let protectedSessionId = null; |  | ||||||
|  |  | ||||||
|     $(document).ready(() => { |  | ||||||
|         server.get('settings/all').then(settings => protectedSessionTimeout = settings.protected_session_timeout); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     function setProtectedSessionTimeout(encSessTimeout) { |  | ||||||
|         protectedSessionTimeout = encSessTimeout; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function ensureProtectedSession(requireProtectedSession, modal) { |  | ||||||
|         const dfd = $.Deferred(); |  | ||||||
|  |  | ||||||
|         if (requireProtectedSession && !isProtectedSessionAvailable()) { |  | ||||||
|             protectedSessionDeferred = dfd; |  | ||||||
|  |  | ||||||
|             if (noteTree.getCurrentNode().data.isProtected) { |  | ||||||
|                 $noteDetailWrapper.hide(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             $dialog.dialog({ |  | ||||||
|                 modal: modal, |  | ||||||
|                 width: 400, |  | ||||||
|                 open: () => { |  | ||||||
|                     if (!modal) { |  | ||||||
|                         // dialog steals focus for itself, which is not what we want for non-modal (viewing) |  | ||||||
|                         noteTree.getCurrentNode().setFocus(); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             dfd.resolve(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return dfd.promise(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function setupProtectedSession() { |  | ||||||
|         const password = $password.val(); |  | ||||||
|         $password.val(""); |  | ||||||
|  |  | ||||||
|         const response = await enterProtectedSession(password); |  | ||||||
|  |  | ||||||
|         if (!response.success) { |  | ||||||
|             showError("Wrong password."); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         protectedSessionId = response.protectedSessionId; |  | ||||||
|  |  | ||||||
|         $dialog.dialog("close"); |  | ||||||
|  |  | ||||||
|         noteEditor.reload(); |  | ||||||
|         noteTree.reload(); |  | ||||||
|  |  | ||||||
|         if (protectedSessionDeferred !== null) { |  | ||||||
|             ensureDialogIsClosed($dialog, $password); |  | ||||||
|  |  | ||||||
|             $noteDetailWrapper.show(); |  | ||||||
|  |  | ||||||
|             protectedSessionDeferred.resolve(); |  | ||||||
|  |  | ||||||
|             protectedSessionDeferred = null; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function ensureDialogIsClosed() { |  | ||||||
|         // this may fal if the dialog has not been previously opened |  | ||||||
|         try { |  | ||||||
|             $dialog.dialog('close'); |  | ||||||
|         } |  | ||||||
|         catch (e) {} |  | ||||||
|  |  | ||||||
|         $password.val(''); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function enterProtectedSession(password) { |  | ||||||
|         return await server.post('login/protected', { |  | ||||||
|             password: password |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getProtectedSessionId() { |  | ||||||
|         return protectedSessionId; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function resetProtectedSession() { |  | ||||||
|         protectedSessionId = null; |  | ||||||
|  |  | ||||||
|         // most secure solution - guarantees nothing remained in memory |  | ||||||
|         // since this expires because user doesn't use the app, it shouldn't be disruptive |  | ||||||
|         reloadApp(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function isProtectedSessionAvailable() { |  | ||||||
|         return protectedSessionId !== null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function protectNoteAndSendToServer() { |  | ||||||
|         await ensureProtectedSession(true, true); |  | ||||||
|  |  | ||||||
|         const note = noteEditor.getCurrentNote(); |  | ||||||
|  |  | ||||||
|         noteEditor.updateNoteFromInputs(note); |  | ||||||
|  |  | ||||||
|         note.detail.isProtected = true; |  | ||||||
|  |  | ||||||
|         await noteEditor.saveNoteToServer(note); |  | ||||||
|  |  | ||||||
|         noteTree.setProtected(note.detail.noteId, note.detail.isProtected); |  | ||||||
|  |  | ||||||
|         noteEditor.setNoteBackgroundIfProtected(note); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function unprotectNoteAndSendToServer() { |  | ||||||
|         await ensureProtectedSession(true, true); |  | ||||||
|  |  | ||||||
|         const note = noteEditor.getCurrentNote(); |  | ||||||
|  |  | ||||||
|         noteEditor.updateNoteFromInputs(note); |  | ||||||
|  |  | ||||||
|         note.detail.isProtected = false; |  | ||||||
|  |  | ||||||
|         await noteEditor.saveNoteToServer(note); |  | ||||||
|  |  | ||||||
|         noteTree.setProtected(note.detail.noteId, note.detail.isProtected); |  | ||||||
|  |  | ||||||
|         noteEditor.setNoteBackgroundIfProtected(note); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function touchProtectedSession() { |  | ||||||
|         if (isProtectedSessionAvailable()) { |  | ||||||
|             lastProtectedSessionOperationDate = new Date(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function protectSubTree(noteId, protect) { |  | ||||||
|         await ensureProtectedSession(true, true); |  | ||||||
|  |  | ||||||
|         await server.put('notes/' + noteId + "/protect-sub-tree/" + (protect ? 1 : 0)); |  | ||||||
|  |  | ||||||
|         showMessage("Request to un/protect sub tree has finished successfully"); |  | ||||||
|  |  | ||||||
|         noteTree.reload(); |  | ||||||
|         noteEditor.reload(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $passwordForm.submit(() => { |  | ||||||
|         setupProtectedSession(); |  | ||||||
|  |  | ||||||
|         return false; |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     setInterval(() => { |  | ||||||
|         if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) { |  | ||||||
|             resetProtectedSession(); |  | ||||||
|         } |  | ||||||
|     }, 5000); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         setProtectedSessionTimeout, |  | ||||||
|         ensureProtectedSession, |  | ||||||
|         resetProtectedSession, |  | ||||||
|         isProtectedSessionAvailable, |  | ||||||
|         protectNoteAndSendToServer, |  | ||||||
|         unprotectNoteAndSendToServer, |  | ||||||
|         getProtectedSessionId, |  | ||||||
|         touchProtectedSession, |  | ||||||
|         protectSubTree, |  | ||||||
|         ensureDialogIsClosed |  | ||||||
|     }; |  | ||||||
| })(); |  | ||||||
| @@ -1,62 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const searchTree = (function() { |  | ||||||
|     const $tree = $("#tree"); |  | ||||||
|     const $searchInput = $("input[name='search-text']"); |  | ||||||
|     const $resetSearchButton = $("button#reset-search-button"); |  | ||||||
|     const $searchBox = $("#search-box"); |  | ||||||
|  |  | ||||||
|     $resetSearchButton.click(resetSearch); |  | ||||||
|  |  | ||||||
|     function toggleSearch() { |  | ||||||
|         if ($searchBox.is(":hidden")) { |  | ||||||
|             $searchBox.show(); |  | ||||||
|             $searchInput.focus(); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             resetSearch(); |  | ||||||
|  |  | ||||||
|             $searchBox.hide(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function resetSearch() { |  | ||||||
|         $searchInput.val(""); |  | ||||||
|  |  | ||||||
|         getTree().clearFilter(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getTree() { |  | ||||||
|         return $tree.fancytree('getTree'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $searchInput.keyup(async e => { |  | ||||||
|         const searchText = $searchInput.val(); |  | ||||||
|  |  | ||||||
|         if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") { |  | ||||||
|             $resetSearchButton.click(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (e && e.which === $.ui.keyCode.ENTER) { |  | ||||||
|             const noteIds = await server.get('notes?search=' + encodeURIComponent(searchText)); |  | ||||||
|  |  | ||||||
|             for (const noteId of noteIds) { |  | ||||||
|                 await noteTree.expandToNote(noteId, {noAnimation: true, noEvents: true}); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Pass a string to perform case insensitive matching |  | ||||||
|             getTree().filterBranches(node => noteIds.includes(node.data.noteId)); |  | ||||||
|         } |  | ||||||
|     }).focus(); |  | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+s', e => { |  | ||||||
|         toggleSearch(); |  | ||||||
|  |  | ||||||
|         e.preventDefault(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         toggleSearch |  | ||||||
|     }; |  | ||||||
| })(); |  | ||||||
| @@ -1,101 +0,0 @@ | |||||||
| const server = (function() { |  | ||||||
|     function getHeaders() { |  | ||||||
|         let protectedSessionId = null; |  | ||||||
|  |  | ||||||
|         try { // this is because protected session might not be declared in some cases - like when it's included in migration page |  | ||||||
|             protectedSessionId = protected_session.getProtectedSessionId(); |  | ||||||
|         } |  | ||||||
|         catch(e) {} |  | ||||||
|  |  | ||||||
|         // headers need to be lowercase because node.js automatically converts them to lower case |  | ||||||
|         // so hypothetical protectedSessionId becomes protectedsessionid on the backend |  | ||||||
|         return { |  | ||||||
|             protected_session_id: protectedSessionId, |  | ||||||
|             source_id: glob.sourceId |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function get(url) { |  | ||||||
|         return await call('GET', url); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function post(url, data) { |  | ||||||
|         return await call('POST', url, data); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function put(url, data) { |  | ||||||
|         return await call('PUT', url, data); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function remove(url) { |  | ||||||
|         return await call('DELETE', url); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let i = 1; |  | ||||||
|     const reqResolves = {}; |  | ||||||
|  |  | ||||||
|     async function call(method, url, data) { |  | ||||||
|         if (isElectron()) { |  | ||||||
|             const ipc = require('electron').ipcRenderer; |  | ||||||
|             const requestId = i++; |  | ||||||
|  |  | ||||||
|             return new Promise((resolve, reject) => { |  | ||||||
|                 reqResolves[requestId] = resolve; |  | ||||||
|  |  | ||||||
|                 console.log(now(), "Request #" + requestId + " to " + method + " " + url); |  | ||||||
|  |  | ||||||
|                 ipc.send('server-request', { |  | ||||||
|                     requestId: requestId, |  | ||||||
|                     headers: getHeaders(), |  | ||||||
|                     method: method, |  | ||||||
|                     url: "/" + baseApiUrl + url, |  | ||||||
|                     data: data |  | ||||||
|                 }); |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             return await ajax(url, method, data); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (isElectron()) { |  | ||||||
|         const ipc = require('electron').ipcRenderer; |  | ||||||
|  |  | ||||||
|         ipc.on('server-response', (event, arg) => { |  | ||||||
|             console.log(now(), "Response #" + arg.requestId + ": " + arg.statusCode); |  | ||||||
|  |  | ||||||
|             reqResolves[arg.requestId](arg.body); |  | ||||||
|  |  | ||||||
|             delete reqResolves[arg.requestId]; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function ajax(url, method, data) { |  | ||||||
|         const options = { |  | ||||||
|             url: baseApiUrl + url, |  | ||||||
|             type: method, |  | ||||||
|             headers: getHeaders() |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         if (data) { |  | ||||||
|             options.data = JSON.stringify(data); |  | ||||||
|             options.contentType = "application/json"; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return await $.ajax(options).catch(e => { |  | ||||||
|             const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText; |  | ||||||
|             showError(message); |  | ||||||
|             throwError(message); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         get, |  | ||||||
|         post, |  | ||||||
|         put, |  | ||||||
|         remove, |  | ||||||
|         ajax, |  | ||||||
|         // don't remove, used from CKEditor image upload! |  | ||||||
|         getHeaders |  | ||||||
|     } |  | ||||||
| })(); |  | ||||||
							
								
								
									
										104
									
								
								src/public/javascripts/services/autocomplete.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/public/javascripts/services/autocomplete.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | |||||||
|  | import treeCache from "./tree_cache.js"; | ||||||
|  | import treeUtils from "./tree_utils.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 = ''; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // https://github.com/zadam/trilium/issues/46 | ||||||
|  |     // unfortunately not easy to implement because we don't have an easy access to note's isProtected property | ||||||
|  |  | ||||||
|  |     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); | ||||||
|  |  | ||||||
|  |         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 | ||||||
|  | }; | ||||||
							
								
								
									
										92
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | import addLinkDialog from '../dialogs/add_link.js'; | ||||||
|  | import jumpToNoteDialog from '../dialogs/jump_to_note.js'; | ||||||
|  | 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'; | ||||||
|  |  | ||||||
|  | import cloning from './cloning.js'; | ||||||
|  | import contextMenu from './context_menu.js'; | ||||||
|  | import dragAndDropSetup from './drag_and_drop.js'; | ||||||
|  | import exportService from './export.js'; | ||||||
|  | import link from './link.js'; | ||||||
|  | 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 ScriptApi from './script_api.js'; | ||||||
|  | import ScriptContext from './script_context.js'; | ||||||
|  | import sync from './sync.js'; | ||||||
|  | import treeService from './tree.js'; | ||||||
|  | import treeChanges from './branches.js'; | ||||||
|  | import treeUtils from './tree_utils.js'; | ||||||
|  | import utils from './utils.js'; | ||||||
|  | import server from './server.js'; | ||||||
|  | import entrypoints from './entrypoints.js'; | ||||||
|  | import tooltip from './tooltip.js'; | ||||||
|  | import bundle from "./bundle.js"; | ||||||
|  | import treeCache from "./tree_cache.js"; | ||||||
|  | import libraryLoader from "./library_loader.js"; | ||||||
|  |  | ||||||
|  | // required for CKEditor image upload plugin | ||||||
|  | window.glob.getCurrentNode = treeService.getCurrentNode; | ||||||
|  | window.glob.getHeaders = server.getHeaders; | ||||||
|  |  | ||||||
|  | // required for ESLint plugin | ||||||
|  | window.glob.getCurrentNote = noteDetailService.getCurrentNote; | ||||||
|  | window.glob.requireLibrary = libraryLoader.requireLibrary; | ||||||
|  | window.glob.ESLINT = libraryLoader.ESLINT; | ||||||
|  |  | ||||||
|  | window.onerror = function (msg, url, lineNo, columnNo, error) { | ||||||
|  |     const string = msg.toLowerCase(); | ||||||
|  |  | ||||||
|  |     let message = "Uncaught error: "; | ||||||
|  |  | ||||||
|  |     if (string.indexOf("script error") > -1){ | ||||||
|  |         message += 'No details available'; | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         message += [ | ||||||
|  |             'Message: ' + msg, | ||||||
|  |             'URL: ' + url, | ||||||
|  |             'Line: ' + lineNo, | ||||||
|  |             'Column: ' + columnNo, | ||||||
|  |             'Error object: ' + JSON.stringify(error) | ||||||
|  |         ].join(' - '); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     messagingService.logError(message); | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | $("#logout-button").toggle(!utils.isElectron()); | ||||||
|  |  | ||||||
|  | if (utils.isElectron()) { | ||||||
|  |     require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) { | ||||||
|  |         // this might occur when day note had to be created | ||||||
|  |         if (!await treeCache.getNote(parentNoteId)) { | ||||||
|  |             await treeService.reload(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await treeService.activateNode(parentNoteId); | ||||||
|  |  | ||||||
|  |         setTimeout(() => { | ||||||
|  |             const node = treeService.getCurrentNode(); | ||||||
|  |  | ||||||
|  |             treeService.createNote(node, node.data.noteId, 'into', node.data.isProtected); | ||||||
|  |         }, 500); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | treeService.showTree(); | ||||||
|  |  | ||||||
|  | entrypoints.registerEntrypoints(); | ||||||
|  |  | ||||||
|  | tooltip.setupTooltip(); | ||||||
|  |  | ||||||
|  | bundle.executeStartupBundles(); | ||||||
							
								
								
									
										135
									
								
								src/public/javascripts/services/branches.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/public/javascripts/services/branches.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | |||||||
|  | import treeService from './tree.js'; | ||||||
|  | import utils from './utils.js'; | ||||||
|  | import server from './server.js'; | ||||||
|  | import infoService from "./info.js"; | ||||||
|  | import treeCache from "./tree_cache.js"; | ||||||
|  |  | ||||||
|  | async function moveBeforeNode(nodesToMove, beforeNode) { | ||||||
|  |     for (const nodeToMove of nodesToMove) { | ||||||
|  |         const resp = await server.put('branches/' + nodeToMove.data.branchId + '/move-before/' + beforeNode.data.branchId); | ||||||
|  |  | ||||||
|  |         if (!resp.success) { | ||||||
|  |             alert(resp.message); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before')); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function moveAfterNode(nodesToMove, afterNode) { | ||||||
|  |     nodesToMove.reverse(); // need to reverse to keep the note order | ||||||
|  |  | ||||||
|  |     for (const nodeToMove of nodesToMove) { | ||||||
|  |         const resp = await server.put('branches/' + nodeToMove.data.branchId + '/move-after/' + afterNode.data.branchId); | ||||||
|  |  | ||||||
|  |         if (!resp.success) { | ||||||
|  |             alert(resp.message); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await changeNode(nodeToMove, node => node.moveTo(afterNode, 'after')); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function moveToNode(nodesToMove, toNode) { | ||||||
|  |     for (const nodeToMove of nodesToMove) { | ||||||
|  |         const resp = await server.put('branches/' + nodeToMove.data.branchId + '/move-to/' + toNode.data.noteId); | ||||||
|  |  | ||||||
|  |         if (!resp.success) { | ||||||
|  |             alert(resp.message); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await changeNode(nodeToMove, async node => { | ||||||
|  |             // first expand which will force lazy load and only then move the node | ||||||
|  |             // if this is not expanded before moving, then lazy load won't happen because it already contains node | ||||||
|  |             // this doesn't work if this isn't a folder yet, that's why we expand second time below | ||||||
|  |             await toNode.setExpanded(true); | ||||||
|  |  | ||||||
|  |             node.moveTo(toNode); | ||||||
|  |  | ||||||
|  |             toNode.folder = true; | ||||||
|  |             toNode.renderTitle(); | ||||||
|  |  | ||||||
|  |             // this expands the note in case it become the folder only after the move | ||||||
|  |             await toNode.setExpanded(true); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function deleteNodes(nodes) { | ||||||
|  |     if (nodes.length === 0 || !confirm('Are you sure you want to delete select note(s) and all the sub-notes?')) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (const node of nodes) { | ||||||
|  |         await server.remove('branches/' + node.data.branchId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // following code assumes that nodes contain only top-most selected nodes - getSelectedNodes has been | ||||||
|  |     // called with stopOnParent=true | ||||||
|  |     let next = nodes[nodes.length - 1].getNextSibling(); | ||||||
|  |  | ||||||
|  |     if (!next) { | ||||||
|  |         next = nodes[0].getPrevSibling(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!next && !utils.isTopLevelNode(nodes[0])) { | ||||||
|  |         next = nodes[0].getParent(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (next) { | ||||||
|  |         // activate next element after this one is deleted so we don't lose focus | ||||||
|  |         next.setActive(); | ||||||
|  |  | ||||||
|  |         treeService.setCurrentNotePathToHash(next); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     infoService.showMessage("Note(s) has been deleted."); | ||||||
|  |  | ||||||
|  |     await treeService.reload(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function moveNodeUpInHierarchy(node) { | ||||||
|  |     if (utils.isTopLevelNode(node)) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const resp = await server.put('branches/' + node.data.branchId + '/move-after/' + node.getParent().data.branchId); | ||||||
|  |  | ||||||
|  |     if (!resp.success) { | ||||||
|  |         alert(resp.message); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!utils.isTopLevelNode(node) && node.getParent().getChildren().length <= 1) { | ||||||
|  |         node.getParent().folder = false; | ||||||
|  |         node.getParent().renderTitle(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await changeNode(node, node => node.moveTo(node.getParent(), 'after')); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function changeNode(node, func) { | ||||||
|  |     utils.assertArguments(node.data.parentNoteId, node.data.noteId); | ||||||
|  |  | ||||||
|  |     const childNoteId = node.data.noteId; | ||||||
|  |     const oldParentNoteId = node.data.parentNoteId; | ||||||
|  |  | ||||||
|  |     await func(node); | ||||||
|  |  | ||||||
|  |     const newParentNoteId = node.data.parentNoteId = utils.isTopLevelNode(node) ? 'root' : node.getParent().data.noteId; | ||||||
|  |  | ||||||
|  |     await treeCache.moveNote(childNoteId, oldParentNoteId, newParentNoteId); | ||||||
|  |  | ||||||
|  |     treeService.setCurrentNotePathToHash(node); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     moveBeforeNode, | ||||||
|  |     moveAfterNode, | ||||||
|  |     moveToNode, | ||||||
|  |     deleteNodes, | ||||||
|  |     moveNodeUpInHierarchy | ||||||
|  | }; | ||||||
							
								
								
									
										23
									
								
								src/public/javascripts/services/bundle.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/public/javascripts/services/bundle.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import ScriptContext from "./script_context.js"; | ||||||
|  | import server from "./server.js"; | ||||||
|  |  | ||||||
|  | async function executeBundle(bundle) { | ||||||
|  |     const apiContext = ScriptContext(bundle.note, bundle.allNotes); | ||||||
|  |  | ||||||
|  |     return await (function () { | ||||||
|  |         return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); | ||||||
|  |     }.call(apiContext)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function executeStartupBundles() { | ||||||
|  |     const scriptBundles = await server.get("script/startup"); | ||||||
|  |  | ||||||
|  |     for (const bundle of scriptBundles) { | ||||||
|  |         await executeBundle(bundle); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     executeBundle, | ||||||
|  |     executeStartupBundles | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								src/public/javascripts/services/cloning.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/public/javascripts/services/cloning.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | import treeService from './tree.js'; | ||||||
|  | import server from './server.js'; | ||||||
|  |  | ||||||
|  | async function cloneNoteTo(childNoteId, parentNoteId, prefix) { | ||||||
|  |     const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, { | ||||||
|  |         prefix: prefix | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (!resp.success) { | ||||||
|  |         alert(resp.message); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await treeService.reload(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // beware that first arg is noteId and second is branchId! | ||||||
|  | async function cloneNoteAfter(noteId, afterBranchId) { | ||||||
|  |     const resp = await server.put('notes/' + noteId + '/clone-after/' + afterBranchId); | ||||||
|  |  | ||||||
|  |     if (!resp.success) { | ||||||
|  |         alert(resp.message); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await treeService.reload(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     cloneNoteAfter, | ||||||
|  |     cloneNoteTo | ||||||
|  | }; | ||||||
							
								
								
									
										189
									
								
								src/public/javascripts/services/context_menu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/public/javascripts/services/context_menu.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | |||||||
|  | import treeService from './tree.js'; | ||||||
|  | import cloningService from './cloning.js'; | ||||||
|  | import exportService from './export.js'; | ||||||
|  | import messagingService from './messaging.js'; | ||||||
|  | import protectedSessionService from './protected_session.js'; | ||||||
|  | import treeChangesService from './branches.js'; | ||||||
|  | import treeUtils from './tree_utils.js'; | ||||||
|  | import branchPrefixDialog from '../dialogs/branch_prefix.js'; | ||||||
|  | import infoService from "./info.js"; | ||||||
|  | import treeCache from "./tree_cache.js"; | ||||||
|  | import syncService from "./sync.js"; | ||||||
|  |  | ||||||
|  | const $tree = $("#tree"); | ||||||
|  |  | ||||||
|  | let clipboardIds = []; | ||||||
|  | let clipboardMode = null; | ||||||
|  |  | ||||||
|  | async function pasteAfter(node) { | ||||||
|  |     if (clipboardMode === 'cut') { | ||||||
|  |         const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey)); | ||||||
|  |  | ||||||
|  |         await treeChangesService.moveAfterNode(nodes, node); | ||||||
|  |  | ||||||
|  |         clipboardIds = []; | ||||||
|  |         clipboardMode = null; | ||||||
|  |     } | ||||||
|  |     else if (clipboardMode === 'copy') { | ||||||
|  |         for (const noteId of clipboardIds) { | ||||||
|  |             await cloningService.cloneNoteAfter(noteId, node.data.branchId); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places | ||||||
|  |     } | ||||||
|  |     else if (clipboardIds.length === 0) { | ||||||
|  |         // just do nothing | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         infoService.throwError("Unrecognized clipboard mode=" + clipboardMode); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function pasteInto(node) { | ||||||
|  |     if (clipboardMode === 'cut') { | ||||||
|  |         const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey)); | ||||||
|  |  | ||||||
|  |         await treeChangesService.moveToNode(nodes, node); | ||||||
|  |  | ||||||
|  |         clipboardIds = []; | ||||||
|  |         clipboardMode = null; | ||||||
|  |     } | ||||||
|  |     else if (clipboardMode === 'copy') { | ||||||
|  |         for (const noteId of clipboardIds) { | ||||||
|  |             await cloningService.cloneNoteTo(noteId, node.data.noteId); | ||||||
|  |         } | ||||||
|  |         // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places | ||||||
|  |     } | ||||||
|  |     else if (clipboardIds.length === 0) { | ||||||
|  |         // just do nothing | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         infoService.throwError("Unrecognized clipboard mode=" + mode); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function copy(nodes) { | ||||||
|  |     clipboardIds = nodes.map(node => node.data.noteId); | ||||||
|  |     clipboardMode = 'copy'; | ||||||
|  |  | ||||||
|  |     infoService.showMessage("Note(s) have been copied into clipboard."); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function cut(nodes) { | ||||||
|  |     clipboardIds = nodes.map(node => node.key); | ||||||
|  |     clipboardMode = 'cut'; | ||||||
|  |  | ||||||
|  |     infoService.showMessage("Note(s) have been cut into clipboard."); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const contextMenuOptions = { | ||||||
|  |     delegate: "span.fancytree-title", | ||||||
|  |     autoFocus: true, | ||||||
|  |     menu: [ | ||||||
|  |         {title: "Insert note here <kbd>Ctrl+O</kbd>", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"}, | ||||||
|  |         {title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "ui-icon-plus"}, | ||||||
|  |         {title: "Delete <kbd>Ctrl+Del</kbd>", cmd: "delete", uiIcon: "ui-icon-trash"}, | ||||||
|  |         {title: "----"}, | ||||||
|  |         {title: "Edit branch prefix <kbd>F2</kbd>", cmd: "editBranchPrefix", uiIcon: "ui-icon-pencil"}, | ||||||
|  |         {title: "----"}, | ||||||
|  |         {title: "Protect branch", cmd: "protectBranch", uiIcon: "ui-icon-locked"}, | ||||||
|  |         {title: "Unprotect branch", cmd: "unprotectBranch", uiIcon: "ui-icon-unlocked"}, | ||||||
|  |         {title: "----"}, | ||||||
|  |         {title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "ui-icon-copy"}, | ||||||
|  |         {title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "ui-icon-scissors"}, | ||||||
|  |         {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: "----"}, | ||||||
|  |         {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); | ||||||
|  |  | ||||||
|  |         // 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", "insertChildNote", note.type !== 'search'); | ||||||
|  |         $tree.contextmenu("enableEntry", "importBranch", note.type !== 'search'); | ||||||
|  |         $tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search'); | ||||||
|  |  | ||||||
|  |         // Activate node on right-click | ||||||
|  |         node.setActive(); | ||||||
|  |         // Disable tree keyboard handling | ||||||
|  |         ui.menu.prevKeyboard = node.tree.options.keyboard; | ||||||
|  |         node.tree.options.keyboard = false; | ||||||
|  |     }, | ||||||
|  |     close: (event, ui) => {}, | ||||||
|  |     select: (event, ui) => { | ||||||
|  |         const node = $.ui.fancytree.getNode(ui.target); | ||||||
|  |  | ||||||
|  |         if (ui.cmd === "insertNoteHere") { | ||||||
|  |             const parentNoteId = node.data.parentNoteId; | ||||||
|  |             const isProtected = treeUtils.getParentProtectedStatus(node); | ||||||
|  |  | ||||||
|  |             treeService.createNote(node, parentNoteId, 'after', isProtected); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "insertChildNote") { | ||||||
|  |             treeService.createNote(node, node.data.noteId, 'into'); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "editBranchPrefix") { | ||||||
|  |             branchPrefixDialog.showDialog(node); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "protectBranch") { | ||||||
|  |             protectedSessionService.protectBranch(node.data.noteId, true); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "unprotectBranch") { | ||||||
|  |             protectedSessionService.protectBranch(node.data.noteId, false); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "copy") { | ||||||
|  |             copy(treeService.getSelectedNodes()); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "cut") { | ||||||
|  |             cut(treeService.getSelectedNodes()); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "pasteAfter") { | ||||||
|  |             pasteAfter(node); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "pasteInto") { | ||||||
|  |             pasteInto(node); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "delete") { | ||||||
|  |             treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "exportBranch") { | ||||||
|  |             exportService.exportBranch(node.data.noteId); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "importBranch") { | ||||||
|  |             exportService.importBranch(node.data.noteId); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "collapseBranch") { | ||||||
|  |             treeService.collapseTree(node); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "forceNoteSync") { | ||||||
|  |             syncService.forceNoteSync(node.data.noteId); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "sortAlphabetically") { | ||||||
|  |             treeService.sortAlphabetically(node.data.noteId); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             messagingService.logError("Unknown command: " + ui.cmd); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     pasteAfter, | ||||||
|  |     pasteInto, | ||||||
|  |     cut, | ||||||
|  |     copy, | ||||||
|  |     contextMenuOptions | ||||||
|  | }; | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| "use strict"; | import treeService from './tree.js'; | ||||||
|  | import treeChangesService from './branches.js'; | ||||||
| 
 | 
 | ||||||
| const dragAndDropSetup = { | const dragAndDropSetup = { | ||||||
|     autoExpandMS: 600, |     autoExpandMS: 600, | ||||||
| @@ -49,19 +50,21 @@ const dragAndDropSetup = { | |||||||
|         const nodeToMove = data.otherNode; |         const nodeToMove = data.otherNode; | ||||||
|         nodeToMove.setSelected(true); |         nodeToMove.setSelected(true); | ||||||
| 
 | 
 | ||||||
|         const selectedNodes = noteTree.getSelectedNodes(); |         const selectedNodes = treeService.getSelectedNodes(); | ||||||
| 
 | 
 | ||||||
|         if (data.hitMode === "before") { |         if (data.hitMode === "before") { | ||||||
|             treeChanges.moveBeforeNode(selectedNodes, node); |             treeChangesService.moveBeforeNode(selectedNodes, node); | ||||||
|         } |         } | ||||||
|         else if (data.hitMode === "after") { |         else if (data.hitMode === "after") { | ||||||
|             treeChanges.moveAfterNode(selectedNodes, node); |             treeChangesService.moveAfterNode(selectedNodes, node); | ||||||
|         } |         } | ||||||
|         else if (data.hitMode === "over") { |         else if (data.hitMode === "over") { | ||||||
|             treeChanges.moveToNode(selectedNodes, node); |             treeChangesService.moveToNode(selectedNodes, node); | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             throw new Exception("Unknown hitMode=" + data.hitMode); |             throw new Exception("Unknown hitMode=" + data.hitMode); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export default dragAndDropSetup; | ||||||
							
								
								
									
										133
									
								
								src/public/javascripts/services/entrypoints.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/public/javascripts/services/entrypoints.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | |||||||
|  | import utils from "./utils.js"; | ||||||
|  | import treeService from "./tree.js"; | ||||||
|  | import linkService from "./link.js"; | ||||||
|  | import fileService from "./file.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 labelsDialog from "../dialogs/labels.js"; | ||||||
|  |  | ||||||
|  | function registerEntrypoints() { | ||||||
|  |     // hot keys are active also inside inputs and content editables | ||||||
|  |     jQuery.hotkeys.options.filterInputAcceptingElements = false; | ||||||
|  |     jQuery.hotkeys.options.filterContentEditable = false; | ||||||
|  |     jQuery.hotkeys.options.filterTextInputs = false; | ||||||
|  |  | ||||||
|  |     utils.bindShortcut('ctrl+l', addLinkDialog.showDialog); | ||||||
|  |  | ||||||
|  |     $("#jump-to-note-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); | ||||||
|  |  | ||||||
|  |     $("#toggle-search-button").click(searchTreeService.toggleSearch); | ||||||
|  |     utils.bindShortcut('ctrl+s', searchTreeService.toggleSearch); | ||||||
|  |  | ||||||
|  |     $(".show-labels-button").click(labelsDialog.showDialog); | ||||||
|  |     utils.bindShortcut('alt+l', labelsDialog.showDialog); | ||||||
|  |  | ||||||
|  |     $("#options-button").click(optionsDialog.showDialog); | ||||||
|  |  | ||||||
|  |     utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog); | ||||||
|  |  | ||||||
|  |     if (utils.isElectron()) { | ||||||
|  |         utils.bindShortcut('alt+left', window.history.back); | ||||||
|  |         utils.bindShortcut('alt+right', window.history.forward); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     utils.bindShortcut('alt+m', e => $(".hide-toggle").toggleClass("suppressed")); | ||||||
|  |  | ||||||
|  |     // hide (toggle) everything except for the note content for distraction free writing | ||||||
|  |     utils.bindShortcut('alt+t', e => { | ||||||
|  |         const date = new Date(); | ||||||
|  |         const dateString = utils.formatDateTime(date); | ||||||
|  |  | ||||||
|  |         linkService.addTextToEditor(dateString); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     utils.bindShortcut('f5', utils.reloadApp); | ||||||
|  |  | ||||||
|  |     utils.bindShortcut('ctrl+r', utils.reloadApp); | ||||||
|  |  | ||||||
|  |     $(document).bind('keydown', 'ctrl+shift+i', () => { | ||||||
|  |         if (utils.isElectron()) { | ||||||
|  |             require('electron').remote.getCurrentWindow().toggleDevTools(); | ||||||
|  |  | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     $(document).bind('keydown', 'ctrl+f', () => { | ||||||
|  |         if (utils.isElectron()) { | ||||||
|  |             const searchInPage = require('electron-in-page-search').default; | ||||||
|  |             const remote = require('electron').remote; | ||||||
|  |  | ||||||
|  |             const inPageSearch = searchInPage(remote.getCurrentWebContents()); | ||||||
|  |  | ||||||
|  |             inPageSearch.openSearchWindow(); | ||||||
|  |  | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // FIXME: do we really need these at this point? | ||||||
|  |     utils.bindShortcut("ctrl+shift+up", () => { | ||||||
|  |         const node = treeService.getCurrentNode(); | ||||||
|  |         node.navigate($.ui.keyCode.UP, true); | ||||||
|  |  | ||||||
|  |         $("#note-detail-text").focus(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     // FIXME: do we really need these at this point? | ||||||
|  |     utils.bindShortcut("ctrl+shift+down", () => { | ||||||
|  |         const node = treeService.getCurrentNode(); | ||||||
|  |         node.navigate($.ui.keyCode.DOWN, true); | ||||||
|  |  | ||||||
|  |         $("#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); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             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); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     registerEntrypoints | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								src/public/javascripts/services/export.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/public/javascripts/services/export.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | import treeService from './tree.js'; | ||||||
|  | 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()); | ||||||
|  |  | ||||||
|  |     utils.download(url); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | let importNoteId; | ||||||
|  |  | ||||||
|  | function importBranch(noteId) { | ||||||
|  |     importNoteId = noteId; | ||||||
|  |  | ||||||
|  |     $("#import-upload").trigger('click'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $("#import-upload").change(async function() { | ||||||
|  |     const formData = new FormData(); | ||||||
|  |     formData.append('upload', this.files[0]); | ||||||
|  |  | ||||||
|  |     await $.ajax({ | ||||||
|  |         url: baseApiUrl + 'notes/' + importNoteId + '/import', | ||||||
|  |         headers: server.getHeaders(), | ||||||
|  |         data: formData, | ||||||
|  |         type: 'POST', | ||||||
|  |         contentType: false, // NEEDED, DON'T OMIT THIS | ||||||
|  |         processData: false, // NEEDED, DON'T OMIT THIS | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     await treeService.reload(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     exportBranch, | ||||||
|  |     importBranch | ||||||
|  | }; | ||||||
							
								
								
									
										29
									
								
								src/public/javascripts/services/file.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/public/javascripts/services/file.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | import noteDetailService from "./note_detail.js"; | ||||||
|  | import treeService from "./tree.js"; | ||||||
|  | import server from "./server.js"; | ||||||
|  |  | ||||||
|  | function uploadFile() { | ||||||
|  |     $("#file-upload").trigger('click'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $("#file-upload").change(async function() { | ||||||
|  |     const formData = new FormData(); | ||||||
|  |     formData.append('upload', this.files[0]); | ||||||
|  |  | ||||||
|  |     const resp = await $.ajax({ | ||||||
|  |         url: baseApiUrl + 'notes/' + noteDetailService.getCurrentNoteId() + '/upload', | ||||||
|  |         headers: server.getHeaders(), | ||||||
|  |         data: formData, | ||||||
|  |         type: 'POST', | ||||||
|  |         contentType: false, // NEEDED, DON'T OMIT THIS | ||||||
|  |         processData: false, // NEEDED, DON'T OMIT THIS | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     await treeService.reload(); | ||||||
|  |  | ||||||
|  |     await treeService.activateNode(resp.noteId); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     uploadFile | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								src/public/javascripts/services/info.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/public/javascripts/services/info.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | import messagingService from "./messaging.js"; | ||||||
|  | import utils from "./utils.js"; | ||||||
|  |  | ||||||
|  | function showMessage(message) { | ||||||
|  |     console.log(utils.now(), "message: ", message); | ||||||
|  |  | ||||||
|  |     $.notify({ | ||||||
|  |         // options | ||||||
|  |         message: message | ||||||
|  |     }, { | ||||||
|  |         // options | ||||||
|  |         type: 'success', | ||||||
|  |         delay: 3000 | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function showError(message, delay = 10000) { | ||||||
|  |     console.log(utils.now(), "error: ", message); | ||||||
|  |  | ||||||
|  |     $.notify({ | ||||||
|  |         // options | ||||||
|  |         message: message | ||||||
|  |     }, { | ||||||
|  |         // options | ||||||
|  |         type: 'danger', | ||||||
|  |         delay: delay | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function throwError(message) { | ||||||
|  |     messagingService.logError(message); | ||||||
|  |  | ||||||
|  |     throw new Error(message); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     showMessage, | ||||||
|  |     showError, | ||||||
|  |     throwError | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								src/public/javascripts/services/library_loader.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/public/javascripts/services/library_loader.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]}; | ||||||
|  |  | ||||||
|  | const CODE_MIRROR = { | ||||||
|  |     js: [ | ||||||
|  |         "libraries/codemirror/codemirror.js", | ||||||
|  |         "libraries/codemirror/addon/mode/loadmode.js", | ||||||
|  |         "libraries/codemirror/addon/fold/xml-fold.js", | ||||||
|  |         "libraries/codemirror/addon/edit/matchbrackets.js", | ||||||
|  |         "libraries/codemirror/addon/edit/matchtags.js", | ||||||
|  |         "libraries/codemirror/addon/search/match-highlighter.js", | ||||||
|  |         "libraries/codemirror/mode/meta.js", | ||||||
|  |         "libraries/codemirror/addon/lint/lint.js", | ||||||
|  |         "libraries/codemirror/addon/lint/eslint.js" | ||||||
|  |     ], | ||||||
|  |     css: [ | ||||||
|  |         "libraries/codemirror/codemirror.css", | ||||||
|  |         "libraries/codemirror/addon/lint/lint.css" | ||||||
|  |     ] | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const ESLINT = {js: ["libraries/eslint.js"]}; | ||||||
|  |  | ||||||
|  | async function requireLibrary(library) { | ||||||
|  |     if (library.css) { | ||||||
|  |         library.css.map(cssUrl => requireCss(cssUrl)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (library.js) { | ||||||
|  |         for (const scriptUrl of library.js) { | ||||||
|  |             await requireScript(scriptUrl); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // we save the promises in case of the same script being required concurrently multiple times | ||||||
|  | const loadedScriptPromises = {}; | ||||||
|  |  | ||||||
|  | async function requireScript(url) { | ||||||
|  |     if (!loadedScriptPromises[url]) { | ||||||
|  |         loadedScriptPromises[url] = $.ajax({ | ||||||
|  |             url: url, | ||||||
|  |             dataType: "script", | ||||||
|  |             cache: true | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await loadedScriptPromises[url]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function requireCss(url) { | ||||||
|  |     const css = Array | ||||||
|  |         .from(document.querySelectorAll('link')) | ||||||
|  |         .map(scr => scr.href); | ||||||
|  |  | ||||||
|  |     if (!css.includes(url)) { | ||||||
|  |         $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     requireLibrary, | ||||||
|  |     CKEDITOR, | ||||||
|  |     CODE_MIRROR, | ||||||
|  |     ESLINT | ||||||
|  | } | ||||||
							
								
								
									
										103
									
								
								src/public/javascripts/services/link.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/public/javascripts/services/link.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | import treeService from './tree.js'; | ||||||
|  | import noteDetailText from './note_detail_text.js'; | ||||||
|  | import treeUtils from './tree_utils.js'; | ||||||
|  |  | ||||||
|  | function getNotePathFromLink(url) { | ||||||
|  |     const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url); | ||||||
|  |  | ||||||
|  |     if (notePathMatch === null) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         return notePathMatch[1]; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getNodePathFromLabel(label) { | ||||||
|  |     const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label); | ||||||
|  |  | ||||||
|  |     if (notePathMatch !== null) { | ||||||
|  |         return notePathMatch[1]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function createNoteLink(notePath, noteTitle) { | ||||||
|  |     if (!noteTitle) { | ||||||
|  |         const noteId = treeUtils.getNoteIdFromNotePath(notePath); | ||||||
|  |  | ||||||
|  |         noteTitle = treeUtils.getNoteTitle(noteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const noteLink = $("<a>", { | ||||||
|  |         href: 'javascript:', | ||||||
|  |         text: noteTitle | ||||||
|  |     }).attr('action', 'note') | ||||||
|  |         .attr('note-path', notePath); | ||||||
|  |  | ||||||
|  |     return noteLink; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function goToLink(e) { | ||||||
|  |     e.preventDefault(); | ||||||
|  |  | ||||||
|  |     const $link = $(e.target); | ||||||
|  |     let notePath = $link.attr("note-path"); | ||||||
|  |  | ||||||
|  |     if (!notePath) { | ||||||
|  |         const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href'); | ||||||
|  |  | ||||||
|  |         if (!address) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (address.startsWith('http')) { | ||||||
|  |             window.open(address, '_blank'); | ||||||
|  |  | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         notePath = getNotePathFromLink(address); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     treeService.activateNode(notePath); | ||||||
|  |  | ||||||
|  |     // this is quite ugly hack, but it seems like we can't close the tooltip otherwise | ||||||
|  |     $("[role='tooltip']").remove(); | ||||||
|  |  | ||||||
|  |     if (glob.activeDialog) { | ||||||
|  |         try { | ||||||
|  |             glob.activeDialog.dialog('close'); | ||||||
|  |         } | ||||||
|  |         catch (e) {} | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function addLinkToEditor(linkTitle, linkHref) { | ||||||
|  |     const editor = noteDetailText.getEditor(); | ||||||
|  |     const doc = editor.document; | ||||||
|  |  | ||||||
|  |     doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function addTextToEditor(text) { | ||||||
|  |     const editor = noteDetailText.getEditor(); | ||||||
|  |     const doc = editor.document; | ||||||
|  |  | ||||||
|  |     doc.enqueueChanges(() => editor.data.insertText(text), doc.selection); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // when click on link popup, in case of internal link, just go the the referenced note instead of default behavior | ||||||
|  | // of opening the link in new window/tab | ||||||
|  | $(document).on('click', "a[action='note']", goToLink); | ||||||
|  | $(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink); | ||||||
|  | $(document).on('dblclick', '#note-detail-text a', goToLink); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     getNodePathFromLabel, | ||||||
|  |     getNotePathFromLink, | ||||||
|  |     createNoteLink, | ||||||
|  |     addLinkToEditor, | ||||||
|  |     addTextToEditor | ||||||
|  | }; | ||||||
							
								
								
									
										108
									
								
								src/public/javascripts/services/messaging.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/public/javascripts/services/messaging.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | import utils from './utils.js'; | ||||||
|  | import infoService from "./info.js"; | ||||||
|  |  | ||||||
|  | const $changesToPushCount = $("#changes-to-push-count"); | ||||||
|  |  | ||||||
|  | const messageHandlers = []; | ||||||
|  |  | ||||||
|  | let ws; | ||||||
|  | let lastSyncId; | ||||||
|  | let lastPingTs; | ||||||
|  |  | ||||||
|  | function logError(message) { | ||||||
|  |     console.log(utils.now(), message); // needs to be separate from .trace() | ||||||
|  |     console.trace(); | ||||||
|  |  | ||||||
|  |     if (ws && ws.readyState === 1) { | ||||||
|  |         ws.send(JSON.stringify({ | ||||||
|  |             type: 'log-error', | ||||||
|  |             error: message | ||||||
|  |         })); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function subscribeToMessages(messageHandler) { | ||||||
|  |     messageHandlers.push(messageHandler); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function handleMessage(event) { | ||||||
|  |     const message = JSON.parse(event.data); | ||||||
|  |  | ||||||
|  |     if (message.type === 'sync') { | ||||||
|  |         lastPingTs = new Date().getTime(); | ||||||
|  |  | ||||||
|  |         if (message.data.length > 0) { | ||||||
|  |             console.log(utils.now(), "Sync data: ", message.data); | ||||||
|  |  | ||||||
|  |             lastSyncId = message.data[message.data.length - 1].id; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId); | ||||||
|  |  | ||||||
|  |         for (const messageHandler of messageHandlers) { | ||||||
|  |             messageHandler(syncData); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $changesToPushCount.html(message.changesToPushCount); | ||||||
|  |     } | ||||||
|  |     else if (message.type === 'sync-hash-check-failed') { | ||||||
|  |         infoService.showError("Sync check failed!", 60000); | ||||||
|  |     } | ||||||
|  |     else if (message.type === 'consistency-checks-failed') { | ||||||
|  |         infoService.showError("Consistency checks failed! See logs for details.", 50 * 60000); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function connectWebSocket() { | ||||||
|  |     const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws'; | ||||||
|  |  | ||||||
|  |     // use wss for secure messaging | ||||||
|  |     const ws = new WebSocket(protocol + "://" + location.host); | ||||||
|  |     ws.onopen = event => console.log(utils.now(), "Connected to server with WebSocket"); | ||||||
|  |     ws.onmessage = handleMessage; | ||||||
|  |     ws.onclose = function(){ | ||||||
|  |         // Try to reconnect in 5 seconds | ||||||
|  |         setTimeout(() => connectWebSocket(), 5000); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     return ws; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | setTimeout(() => { | ||||||
|  |     ws = connectWebSocket(); | ||||||
|  |  | ||||||
|  |     lastSyncId = glob.maxSyncIdAtLoad; | ||||||
|  |     lastPingTs = new Date().getTime(); | ||||||
|  |     let connectionBrokenNotification = null; | ||||||
|  |  | ||||||
|  |     setInterval(async () => { | ||||||
|  |         if (new Date().getTime() - lastPingTs > 30000) { | ||||||
|  |             if (!connectionBrokenNotification) { | ||||||
|  |                 connectionBrokenNotification = $.notify({ | ||||||
|  |                     // options | ||||||
|  |                     message: "Lost connection to server" | ||||||
|  |                 },{ | ||||||
|  |                     // options | ||||||
|  |                     type: 'danger', | ||||||
|  |                     delay: 100000000 // keep it until we explicitly close it | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         else if (connectionBrokenNotification) { | ||||||
|  |             await connectionBrokenNotification.close(); | ||||||
|  |             connectionBrokenNotification = null; | ||||||
|  |  | ||||||
|  |             infoService.showMessage("Re-connected to server"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ws.send(JSON.stringify({ | ||||||
|  |             type: 'ping', | ||||||
|  |             lastSyncId: lastSyncId | ||||||
|  |         })); | ||||||
|  |     }, 1000); | ||||||
|  | }, 0); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     logError, | ||||||
|  |     subscribeToMessages | ||||||
|  | }; | ||||||
							
								
								
									
										260
									
								
								src/public/javascripts/services/note_detail.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								src/public/javascripts/services/note_detail.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,260 @@ | |||||||
|  | import treeService from './tree.js'; | ||||||
|  | import noteTypeService from './note_type.js'; | ||||||
|  | import protectedSessionService from './protected_session.js'; | ||||||
|  | import protectedSessionHolder from './protected_session_holder.js'; | ||||||
|  | import utils from './utils.js'; | ||||||
|  | import server from './server.js'; | ||||||
|  | import messagingService from "./messaging.js"; | ||||||
|  | import infoService from "./info.js"; | ||||||
|  | import treeCache from "./tree_cache.js"; | ||||||
|  | import NoteFull from "../entities/note_full.js"; | ||||||
|  | import noteDetailCode from './note_detail_code.js'; | ||||||
|  | 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'; | ||||||
|  |  | ||||||
|  | const $noteTitle = $("#note-title"); | ||||||
|  |  | ||||||
|  | const $noteDetailComponents = $(".note-detail-component"); | ||||||
|  |  | ||||||
|  | const $protectButton = $("#protect-button"); | ||||||
|  | const $unprotectButton = $("#unprotect-button"); | ||||||
|  | const $noteDetailWrapper = $("#note-detail-wrapper"); | ||||||
|  | const $noteIdDisplay = $("#note-id-display"); | ||||||
|  | const $labelList = $("#label-list"); | ||||||
|  | const $labelListInner = $("#label-list-inner"); | ||||||
|  |  | ||||||
|  | let currentNote = null; | ||||||
|  |  | ||||||
|  | let noteChangeDisabled = false; | ||||||
|  |  | ||||||
|  | let isNoteChanged = false; | ||||||
|  |  | ||||||
|  | const components = { | ||||||
|  |     'code': noteDetailCode, | ||||||
|  |     'text': noteDetailText, | ||||||
|  |     'file': noteDetailFile, | ||||||
|  |     'search': noteDetailSearch, | ||||||
|  |     'render': noteDetailRender | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function getComponent(type) { | ||||||
|  |     if (components[type]) { | ||||||
|  |         return components[type]; | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         infoService.throwError("Unrecognized type: " + type); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getCurrentNote() { | ||||||
|  |     return currentNote; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getCurrentNoteId() { | ||||||
|  |     return currentNote ? currentNote.noteId : null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getCurrentNoteType() { | ||||||
|  |     const currentNote = getCurrentNote(); | ||||||
|  |  | ||||||
|  |     return currentNote ? currentNote.type : null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function noteChanged() { | ||||||
|  |     if (noteChangeDisabled) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     isNoteChanged = true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function reload() { | ||||||
|  |     // no saving here | ||||||
|  |  | ||||||
|  |     await loadNoteToEditor(getCurrentNoteId()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function switchToNote(noteId) { | ||||||
|  |     if (getCurrentNoteId() !== noteId) { | ||||||
|  |         await saveNoteIfChanged(); | ||||||
|  |  | ||||||
|  |         await loadNoteToEditor(noteId); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function saveNoteIfChanged() { | ||||||
|  |     if (!isNoteChanged) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const note = getCurrentNote(); | ||||||
|  |  | ||||||
|  |     updateNoteFromInputs(note); | ||||||
|  |  | ||||||
|  |     await saveNoteToServer(note); | ||||||
|  |  | ||||||
|  |     if (note.isProtected) { | ||||||
|  |         protectedSessionHolder.touchProtectedSession(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function updateNoteFromInputs(note) { | ||||||
|  |     note.title = $noteTitle.val(); | ||||||
|  |     note.content = getComponent(note.type).getContent(); | ||||||
|  |  | ||||||
|  |     treeService.setNoteTitle(note.noteId, note.title); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function saveNoteToServer(note) { | ||||||
|  |     const dto = Object.assign({}, note); | ||||||
|  |     delete dto.treeCache; | ||||||
|  |     delete dto.hideInAutocomplete; | ||||||
|  |  | ||||||
|  |     await server.put('notes/' + dto.noteId, dto); | ||||||
|  |  | ||||||
|  |     isNoteChanged = false; | ||||||
|  |  | ||||||
|  |     infoService.showMessage("Saved!"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function setNoteBackgroundIfProtected(note) { | ||||||
|  |     const isProtected = !!note.isProtected; | ||||||
|  |  | ||||||
|  |     $noteDetailWrapper.toggleClass("protected", isProtected); | ||||||
|  |     $protectButton.toggle(!isProtected); | ||||||
|  |     $unprotectButton.toggle(isProtected); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | let isNewNoteCreated = false; | ||||||
|  |  | ||||||
|  | function newNoteCreated() { | ||||||
|  |     isNewNoteCreated = true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function handleProtectedSession() { | ||||||
|  |     await protectedSessionService.ensureProtectedSession(currentNote.isProtected, false); | ||||||
|  |  | ||||||
|  |     if (currentNote.isProtected) { | ||||||
|  |         protectedSessionHolder.touchProtectedSession(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // this might be important if we focused on protected note when not in protected note and we got a dialog | ||||||
|  |     // to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it. | ||||||
|  |     protectedSessionService.ensureDialogIsClosed(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function loadNoteToEditor(noteId) { | ||||||
|  |     currentNote = await loadNote(noteId); | ||||||
|  |  | ||||||
|  |     if (isNewNoteCreated) { | ||||||
|  |         isNewNoteCreated = false; | ||||||
|  |  | ||||||
|  |         $noteTitle.focus().select(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $noteIdDisplay.html(noteId); | ||||||
|  |  | ||||||
|  |     await handleProtectedSession(); | ||||||
|  |  | ||||||
|  |     $noteDetailWrapper.show(); | ||||||
|  |  | ||||||
|  |     noteChangeDisabled = true; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |         $noteTitle.val(currentNote.title); | ||||||
|  |  | ||||||
|  |         noteTypeService.setNoteType(currentNote.type); | ||||||
|  |         noteTypeService.setNoteMime(currentNote.mime); | ||||||
|  |  | ||||||
|  |         $noteDetailComponents.hide(); | ||||||
|  |  | ||||||
|  |         await getComponent(currentNote.type).show(); | ||||||
|  |     } | ||||||
|  |     finally { | ||||||
|  |         noteChangeDisabled = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     setNoteBackgroundIfProtected(currentNote); | ||||||
|  |     treeService.setBranchBackgroundBasedOnProtectedStatus(noteId); | ||||||
|  |  | ||||||
|  |     // after loading new note make sure editor is scrolled to the top | ||||||
|  |     $noteDetailWrapper.scrollTop(0); | ||||||
|  |  | ||||||
|  |     await loadLabelList(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function loadLabelList() { | ||||||
|  |     const noteId = getCurrentNoteId(); | ||||||
|  |  | ||||||
|  |     const labels = await server.get('notes/' + noteId + '/labels'); | ||||||
|  |  | ||||||
|  |     $labelListInner.html(''); | ||||||
|  |  | ||||||
|  |     if (labels.length > 0) { | ||||||
|  |         for (const label of labels) { | ||||||
|  |             $labelListInner.append(utils.formatLabel(label) + " "); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $labelList.show(); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         $labelList.hide(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function loadNote(noteId) { | ||||||
|  |     const row = await server.get('notes/' + noteId); | ||||||
|  |  | ||||||
|  |     return new NoteFull(treeCache, row); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function focus() { | ||||||
|  |     const note = getCurrentNote(); | ||||||
|  |  | ||||||
|  |     getComponent(note.type).focus(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | messagingService.subscribeToMessages(syncData => { | ||||||
|  |     if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getCurrentNoteId())) { | ||||||
|  |         infoService.showMessage('Reloading note because of background changes'); | ||||||
|  |  | ||||||
|  |         reload(); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $(document).ready(() => { | ||||||
|  |     $noteTitle.on('input', () => { | ||||||
|  |         noteChanged(); | ||||||
|  |  | ||||||
|  |         const title = $noteTitle.val(); | ||||||
|  |  | ||||||
|  |         treeService.setNoteTitle(getCurrentNoteId(), title); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     noteDetailText.focus(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved | ||||||
|  | // this sends the request asynchronously and doesn't wait for result | ||||||
|  | $(window).on('beforeunload', () => { saveNoteIfChanged(); }); // don't convert to short form, handler doesn't like returned promise | ||||||
|  |  | ||||||
|  | setInterval(saveNoteIfChanged, 5000); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     reload, | ||||||
|  |     switchToNote, | ||||||
|  |     updateNoteFromInputs, | ||||||
|  |     saveNoteToServer, | ||||||
|  |     setNoteBackgroundIfProtected, | ||||||
|  |     loadNote, | ||||||
|  |     getCurrentNote, | ||||||
|  |     getCurrentNoteType, | ||||||
|  |     getCurrentNoteId, | ||||||
|  |     newNoteCreated, | ||||||
|  |     focus, | ||||||
|  |     loadLabelList, | ||||||
|  |     saveNoteIfChanged, | ||||||
|  |     noteChanged | ||||||
|  | }; | ||||||
							
								
								
									
										90
									
								
								src/public/javascripts/services/note_detail_code.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/public/javascripts/services/note_detail_code.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | import libraryLoader from "./library_loader.js"; | ||||||
|  | import bundleService from "./bundle.js"; | ||||||
|  | import infoService from "./info.js"; | ||||||
|  | import server from "./server.js"; | ||||||
|  | import noteDetailService from "./note_detail.js"; | ||||||
|  |  | ||||||
|  | let codeEditor = null; | ||||||
|  |  | ||||||
|  | const $noteDetailCode = $('#note-detail-code'); | ||||||
|  | const $executeScriptButton = $("#execute-script-button"); | ||||||
|  |  | ||||||
|  | async function show() { | ||||||
|  |     await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR); | ||||||
|  |  | ||||||
|  |     if (!codeEditor) { | ||||||
|  |         CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||||
|  |         CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||||
|  |  | ||||||
|  |         CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; | ||||||
|  |  | ||||||
|  |         codeEditor = CodeMirror($noteDetailCode[0], { | ||||||
|  |             value: "", | ||||||
|  |             viewportMargin: Infinity, | ||||||
|  |             indentUnit: 4, | ||||||
|  |             matchBrackets: true, | ||||||
|  |             matchTags: {bothTags: true}, | ||||||
|  |             highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false}, | ||||||
|  |             lint: true, | ||||||
|  |             gutters: ["CodeMirror-lint-markers"], | ||||||
|  |             lineNumbers: true | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         codeEditor.on('change', noteDetailService.noteChanged); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $noteDetailCode.show(); | ||||||
|  |  | ||||||
|  |     const currentNote = noteDetailService.getCurrentNote(); | ||||||
|  |  | ||||||
|  |     // this needs to happen after the element is shown, otherwise the editor won't be refreshed | ||||||
|  |     codeEditor.setValue(currentNote.content); | ||||||
|  |  | ||||||
|  |     const info = CodeMirror.findModeByMIME(currentNote.mime); | ||||||
|  |  | ||||||
|  |     if (info) { | ||||||
|  |         codeEditor.setOption("mode", info.mime); | ||||||
|  |         CodeMirror.autoLoadMode(codeEditor, info.mode); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     codeEditor.refresh(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getContent() { | ||||||
|  |     return codeEditor.getValue(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function focus() { | ||||||
|  |     codeEditor.focus(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function executeCurrentNote() { | ||||||
|  |     if (noteDetailService.getCurrentNoteType() === 'code') { | ||||||
|  |         // 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); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (currentNote.mime.endsWith("env=backend")) { | ||||||
|  |             await server.post('script/run/' + noteDetailService.getCurrentNoteId()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         infoService.showMessage("Note executed"); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $(document).bind('keydown', "ctrl+return", executeCurrentNote); | ||||||
|  |  | ||||||
|  | $executeScriptButton.click(executeCurrentNote); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     show, | ||||||
|  |     getContent, | ||||||
|  |     focus | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								src/public/javascripts/services/note_detail_file.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/public/javascripts/services/note_detail_file.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | import utils from "./utils.js"; | ||||||
|  | import server from "./server.js"; | ||||||
|  | import protectedSessionHolder from "./protected_session_holder.js"; | ||||||
|  | import noteDetailService from "./note_detail.js"; | ||||||
|  |  | ||||||
|  | const $noteDetailFile = $('#note-detail-file'); | ||||||
|  |  | ||||||
|  | const $fileFileName = $("#file-filename"); | ||||||
|  | const $fileFileType = $("#file-filetype"); | ||||||
|  | const $fileFileSize = $("#file-filesize"); | ||||||
|  | const $fileDownload = $("#file-download"); | ||||||
|  | const $fileOpen = $("#file-open"); | ||||||
|  |  | ||||||
|  | async function show() { | ||||||
|  |     const currentNote = noteDetailService.getCurrentNote(); | ||||||
|  |  | ||||||
|  |     const labels = await server.get('notes/' + currentNote.noteId + '/labels'); | ||||||
|  |     const labelMap = utils.toObject(labels, l => [l.name, l.value]); | ||||||
|  |  | ||||||
|  |     $noteDetailFile.show(); | ||||||
|  |  | ||||||
|  |     $fileFileName.text(labelMap.original_file_name); | ||||||
|  |     $fileFileSize.text(labelMap.file_size + " bytes"); | ||||||
|  |     $fileFileType.text(currentNote.mime); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $fileDownload.click(() => utils.download(getFileUrl())); | ||||||
|  |  | ||||||
|  | $fileOpen.click(() => { | ||||||
|  |     if (utils.isElectron()) { | ||||||
|  |         const open = require("open"); | ||||||
|  |  | ||||||
|  |         open(getFileUrl()); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         window.location.href = getFileUrl(); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | function getFileUrl() { | ||||||
|  |     // electron needs absolute URL so we extract current host, port, protocol | ||||||
|  |     return utils.getHost() + "/api/notes/" + noteDetailService.getCurrentNoteId() | ||||||
|  |         + "/download?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     show, | ||||||
|  |     getContent: () => null, | ||||||
|  |     focus: () => null | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								src/public/javascripts/services/note_detail_render.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/public/javascripts/services/note_detail_render.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import bundleService from "./bundle.js"; | ||||||
|  | import server from "./server.js"; | ||||||
|  | import noteDetailService from "./note_detail.js"; | ||||||
|  |  | ||||||
|  | const $noteDetailRender = $('#note-detail-render'); | ||||||
|  |  | ||||||
|  | async function show() { | ||||||
|  |     $noteDetailRender.show(); | ||||||
|  |  | ||||||
|  |     const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId()); | ||||||
|  |  | ||||||
|  |     $noteDetailRender.html(bundle.html); | ||||||
|  |  | ||||||
|  |     await bundleService.executeBundle(bundle); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     show, | ||||||
|  |     getContent: () => null, | ||||||
|  |     focus: () => null | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								src/public/javascripts/services/note_detail_search.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/public/javascripts/services/note_detail_search.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | import noteDetailService from "./note_detail.js"; | ||||||
|  |  | ||||||
|  | const $searchString = $("#search-string"); | ||||||
|  | const $noteDetailSearch = $('#note-detail-search'); | ||||||
|  |  | ||||||
|  | function getContent() { | ||||||
|  |     JSON.stringify({ | ||||||
|  |         searchString: $searchString.val() | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function show() { | ||||||
|  |     $noteDetailSearch.show(); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |         const json = JSON.parse(noteDetailService.getCurrentNote().content); | ||||||
|  |  | ||||||
|  |         $searchString.val(json.searchString); | ||||||
|  |     } | ||||||
|  |     catch (e) { | ||||||
|  |         console.log(e); | ||||||
|  |         $searchString.val(''); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $searchString.on('input', noteDetailService.noteChanged); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     getContent, | ||||||
|  |     show, | ||||||
|  |     focus: () => null | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								src/public/javascripts/services/note_detail_text.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/public/javascripts/services/note_detail_text.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | import libraryLoader from "./library_loader.js"; | ||||||
|  | import noteDetailService from './note_detail.js'; | ||||||
|  |  | ||||||
|  | const $noteDetailText = $('#note-detail-text'); | ||||||
|  |  | ||||||
|  | let textEditor = null; | ||||||
|  |  | ||||||
|  | async function show() { | ||||||
|  |     if (!textEditor) { | ||||||
|  |         await libraryLoader.requireLibrary(libraryLoader.CKEDITOR); | ||||||
|  |  | ||||||
|  |         textEditor = await BalloonEditor.create($noteDetailText[0], {}); | ||||||
|  |  | ||||||
|  |         textEditor.document.on('change', noteDetailService.noteChanged); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49 | ||||||
|  |     textEditor.setData(noteDetailService.getCurrentNote().content || "<p></p>"); | ||||||
|  |  | ||||||
|  |     $noteDetailText.show(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getContent() { | ||||||
|  |     let content = textEditor.getData(); | ||||||
|  |  | ||||||
|  |     // if content is only tags/whitespace (typically <p> </p>), then just make it empty | ||||||
|  |     // this is important when setting new note to code | ||||||
|  |     if (jQuery(content).text().trim() === '' && !content.includes("<img")) { | ||||||
|  |         content = ''; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return content; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function focus() { | ||||||
|  |     $noteDetailText.focus(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getEditor() { | ||||||
|  |     return textEditor; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     show, | ||||||
|  |     getEditor, | ||||||
|  |     getContent, | ||||||
|  |     focus | ||||||
|  | } | ||||||
							
								
								
									
										146
									
								
								src/public/javascripts/services/note_type.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/public/javascripts/services/note_type.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | |||||||
|  | import treeService from './tree.js'; | ||||||
|  | import noteDetail from './note_detail.js'; | ||||||
|  | import server from './server.js'; | ||||||
|  | import infoService from "./info.js"; | ||||||
|  |  | ||||||
|  | const $executeScriptButton = $("#execute-script-button"); | ||||||
|  | const noteTypeModel = new NoteTypeModel(); | ||||||
|  |  | ||||||
|  | function NoteTypeModel() { | ||||||
|  |     const self = this; | ||||||
|  |  | ||||||
|  |     this.type = ko.observable('text'); | ||||||
|  |     this.mime = ko.observable(''); | ||||||
|  |  | ||||||
|  |     this.codeMimeTypes = ko.observableArray([ | ||||||
|  |         { mime: 'text/x-csrc', title: 'C' }, | ||||||
|  |         { mime: 'text/x-c++src', title: 'C++' }, | ||||||
|  |         { mime: 'text/x-csharp', title: 'C#' }, | ||||||
|  |         { mime: 'text/x-clojure', title: 'Clojure' }, | ||||||
|  |         { mime: 'text/css', title: 'CSS' }, | ||||||
|  |         { mime: 'text/x-dockerfile', title: 'Dockerfile' }, | ||||||
|  |         { mime: 'text/x-erlang', title: 'Erlang' }, | ||||||
|  |         { mime: 'text/x-feature', title: 'Gherkin' }, | ||||||
|  |         { mime: 'text/x-go', title: 'Go' }, | ||||||
|  |         { mime: 'text/x-groovy', title: 'Groovy' }, | ||||||
|  |         { mime: 'text/x-haskell', title: 'Haskell' }, | ||||||
|  |         { mime: 'text/html', title: 'HTML' }, | ||||||
|  |         { mime: 'message/http', title: 'HTTP' }, | ||||||
|  |         { mime: 'text/x-java', title: 'Java' }, | ||||||
|  |         { mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' }, | ||||||
|  |         { mime: 'application/javascript;env=backend', title: 'JavaScript backend' }, | ||||||
|  |         { mime: 'application/json', title: 'JSON' }, | ||||||
|  |         { mime: 'text/x-kotlin', title: 'Kotlin' }, | ||||||
|  |         { mime: 'text/x-lua', title: 'Lua' }, | ||||||
|  |         { mime: 'text/x-markdown', title: 'Markdown' }, | ||||||
|  |         { mime: 'text/x-objectivec', title: 'Objective C' }, | ||||||
|  |         { mime: 'text/x-pascal', title: 'Pascal' }, | ||||||
|  |         { mime: 'text/x-perl', title: 'Perl' }, | ||||||
|  |         { mime: 'text/x-php', title: 'PHP' }, | ||||||
|  |         { mime: 'text/x-python', title: 'Python' }, | ||||||
|  |         { mime: 'text/x-ruby', title: 'Ruby' }, | ||||||
|  |         { mime: 'text/x-rustsrc', title: 'Rust' }, | ||||||
|  |         { mime: 'text/x-scala', title: 'Scala' }, | ||||||
|  |         { mime: 'text/x-sh', title: 'Shell' }, | ||||||
|  |         { mime: 'text/x-sql', title: 'SQL' }, | ||||||
|  |         { mime: 'text/x-swift', title: 'Swift' }, | ||||||
|  |         { mime: 'text/xml', title: 'XML' }, | ||||||
|  |         { mime: 'text/x-yaml', title: 'YAML' } | ||||||
|  |     ]); | ||||||
|  |  | ||||||
|  |     this.typeString = function() { | ||||||
|  |         const type = self.type(); | ||||||
|  |         const mime = self.mime(); | ||||||
|  |  | ||||||
|  |         if (type === 'text') { | ||||||
|  |             return 'Text'; | ||||||
|  |         } | ||||||
|  |         else if (type === 'code') { | ||||||
|  |             if (!mime) { | ||||||
|  |                 return 'Code'; | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 const found = self.codeMimeTypes().find(x => x.mime === mime); | ||||||
|  |  | ||||||
|  |                 return found ? found.title : mime; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         else if (type === 'render') { | ||||||
|  |             return 'Render HTML note'; | ||||||
|  |         } | ||||||
|  |         else if (type === 'file') { | ||||||
|  |             return 'File'; | ||||||
|  |         } | ||||||
|  |         else if (type === 'search') { | ||||||
|  |             // ignore and do nothing, "type" will be hidden since it's not possible to switch to and from search | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             infoService.throwError('Unrecognized type: ' + type); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.isDisabled = function() { | ||||||
|  |         return self.type() === "file"; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     async function save() { | ||||||
|  |         const note = noteDetail.getCurrentNote(); | ||||||
|  |  | ||||||
|  |         await server.put('notes/' + note.noteId | ||||||
|  |             + '/type/' + encodeURIComponent(self.type()) | ||||||
|  |             + '/mime/' + encodeURIComponent(self.mime())); | ||||||
|  |  | ||||||
|  |         await noteDetail.reload(); | ||||||
|  |  | ||||||
|  |         // for the note icon to be updated in the tree | ||||||
|  |         await treeService.reload(); | ||||||
|  |  | ||||||
|  |         self.updateExecuteScriptButtonVisibility(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.selectText = function() { | ||||||
|  |         self.type('text'); | ||||||
|  |         self.mime(''); | ||||||
|  |  | ||||||
|  |         save(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.selectRender = function() { | ||||||
|  |         self.type('render'); | ||||||
|  |         self.mime(''); | ||||||
|  |  | ||||||
|  |         save(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.selectCode = function() { | ||||||
|  |         self.type('code'); | ||||||
|  |         self.mime(''); | ||||||
|  |  | ||||||
|  |         save(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.selectCodeMime = function(el) { | ||||||
|  |         self.type('code'); | ||||||
|  |         self.mime(el.mime); | ||||||
|  |  | ||||||
|  |         save(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.updateExecuteScriptButtonVisibility = function() { | ||||||
|  |         $executeScriptButton.toggle(self.mime().startsWith('application/javascript')); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ko.applyBindings(noteTypeModel, document.getElementById('note-type')); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     getNoteType: () => noteTypeModel.type(), | ||||||
|  |     setNoteType: type => noteTypeModel.type(type), | ||||||
|  |  | ||||||
|  |     getNoteMime: () => noteTypeModel.mime(), | ||||||
|  |     setNoteMime: mime => { | ||||||
|  |         noteTypeModel.mime(mime); | ||||||
|  |  | ||||||
|  |         noteTypeModel.updateExecuteScriptButtonVisibility(); | ||||||
|  |     } | ||||||
|  | }; | ||||||
							
								
								
									
										148
									
								
								src/public/javascripts/services/protected_session.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								src/public/javascripts/services/protected_session.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | |||||||
|  | import treeService from './tree.js'; | ||||||
|  | import noteDetail from './note_detail.js'; | ||||||
|  | import utils from './utils.js'; | ||||||
|  | import server from './server.js'; | ||||||
|  | import protectedSessionHolder from './protected_session_holder.js'; | ||||||
|  | import infoService from "./info.js"; | ||||||
|  |  | ||||||
|  | const $dialog = $("#protected-session-password-dialog"); | ||||||
|  | const $passwordForm = $("#protected-session-password-form"); | ||||||
|  | const $password = $("#protected-session-password"); | ||||||
|  | const $noteDetailWrapper = $("#note-detail-wrapper"); | ||||||
|  | const $protectButton = $("#protect-button"); | ||||||
|  | const $unprotectButton = $("#unprotect-button"); | ||||||
|  |  | ||||||
|  | let protectedSessionDeferred = null; | ||||||
|  |  | ||||||
|  | function ensureProtectedSession(requireProtectedSession, modal) { | ||||||
|  |     const dfd = $.Deferred(); | ||||||
|  |  | ||||||
|  |     if (requireProtectedSession && !protectedSessionHolder.isProtectedSessionAvailable()) { | ||||||
|  |         protectedSessionDeferred = dfd; | ||||||
|  |  | ||||||
|  |         if (treeService.getCurrentNode().data.isProtected) { | ||||||
|  |             $noteDetailWrapper.hide(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $dialog.dialog({ | ||||||
|  |             modal: modal, | ||||||
|  |             width: 400, | ||||||
|  |             open: () => { | ||||||
|  |                 if (!modal) { | ||||||
|  |                     // dialog steals focus for itself, which is not what we want for non-modal (viewing) | ||||||
|  |                     treeService.getCurrentNode().setFocus(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         dfd.resolve(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return dfd.promise(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function setupProtectedSession() { | ||||||
|  |     const password = $password.val(); | ||||||
|  |     $password.val(""); | ||||||
|  |  | ||||||
|  |     const response = await enterProtectedSession(password); | ||||||
|  |  | ||||||
|  |     if (!response.success) { | ||||||
|  |         infoService.showError("Wrong password."); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protectedSessionHolder.setProtectedSessionId(response.protectedSessionId); | ||||||
|  |  | ||||||
|  |     $dialog.dialog("close"); | ||||||
|  |  | ||||||
|  |     noteDetail.reload(); | ||||||
|  |     treeService.reload(); | ||||||
|  |  | ||||||
|  |     if (protectedSessionDeferred !== null) { | ||||||
|  |         ensureDialogIsClosed($dialog, $password); | ||||||
|  |  | ||||||
|  |         $noteDetailWrapper.show(); | ||||||
|  |  | ||||||
|  |         protectedSessionDeferred.resolve(); | ||||||
|  |  | ||||||
|  |         protectedSessionDeferred = null; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function ensureDialogIsClosed() { | ||||||
|  |     // this may fal if the dialog has not been previously opened | ||||||
|  |     try { | ||||||
|  |         $dialog.dialog('close'); | ||||||
|  |     } | ||||||
|  |     catch (e) {} | ||||||
|  |  | ||||||
|  |     $password.val(''); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function enterProtectedSession(password) { | ||||||
|  |     return await server.post('login/protected', { | ||||||
|  |         password: password | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function protectNoteAndSendToServer() { | ||||||
|  |     await ensureProtectedSession(true, true); | ||||||
|  |  | ||||||
|  |     const note = noteDetail.getCurrentNote(); | ||||||
|  |  | ||||||
|  |     noteDetail.updateNoteFromInputs(note); | ||||||
|  |  | ||||||
|  |     note.isProtected = true; | ||||||
|  |  | ||||||
|  |     await noteDetail.saveNoteToServer(note); | ||||||
|  |  | ||||||
|  |     treeService.setProtected(note.noteId, note.isProtected); | ||||||
|  |  | ||||||
|  |     noteDetail.setNoteBackgroundIfProtected(note); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function unprotectNoteAndSendToServer() { | ||||||
|  |     await ensureProtectedSession(true, true); | ||||||
|  |  | ||||||
|  |     const note = noteDetail.getCurrentNote(); | ||||||
|  |  | ||||||
|  |     noteDetail.updateNoteFromInputs(note); | ||||||
|  |  | ||||||
|  |     note.isProtected = false; | ||||||
|  |  | ||||||
|  |     await noteDetail.saveNoteToServer(note); | ||||||
|  |  | ||||||
|  |     treeService.setProtected(note.noteId, note.isProtected); | ||||||
|  |  | ||||||
|  |     noteDetail.setNoteBackgroundIfProtected(note); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function protectBranch(noteId, protect) { | ||||||
|  |     await ensureProtectedSession(true, true); | ||||||
|  |  | ||||||
|  |     await server.put('notes/' + noteId + "/protect/" + (protect ? 1 : 0)); | ||||||
|  |  | ||||||
|  |     infoService.showMessage("Request to un/protect sub tree has finished successfully"); | ||||||
|  |  | ||||||
|  |     treeService.reload(); | ||||||
|  |     noteDetail.reload(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $passwordForm.submit(() => { | ||||||
|  |     setupProtectedSession(); | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $protectButton.click(protectNoteAndSendToServer); | ||||||
|  | $unprotectButton.click(unprotectNoteAndSendToServer); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     ensureProtectedSession, | ||||||
|  |     protectNoteAndSendToServer, | ||||||
|  |     unprotectNoteAndSendToServer, | ||||||
|  |     protectBranch, | ||||||
|  |     ensureDialogIsClosed | ||||||
|  | }; | ||||||
							
								
								
									
										55
									
								
								src/public/javascripts/services/protected_session_holder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/public/javascripts/services/protected_session_holder.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | import utils from "./utils.js"; | ||||||
|  | import server from "./server.js"; | ||||||
|  |  | ||||||
|  | let lastProtectedSessionOperationDate = null; | ||||||
|  | let protectedSessionTimeout = null; | ||||||
|  | let protectedSessionId = null; | ||||||
|  |  | ||||||
|  | $(document).ready(() => { | ||||||
|  |     server.get('options').then(options => protectedSessionTimeout = options.protectedSessionTimeout); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | setInterval(() => { | ||||||
|  |     if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) { | ||||||
|  |         resetProtectedSession(); | ||||||
|  |     } | ||||||
|  | }, 5000); | ||||||
|  |  | ||||||
|  | function setProtectedSessionTimeout(encSessTimeout) { | ||||||
|  |     protectedSessionTimeout = encSessTimeout; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getProtectedSessionId() { | ||||||
|  |     return protectedSessionId; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function setProtectedSessionId(id) { | ||||||
|  |     protectedSessionId = id; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function resetProtectedSession() { | ||||||
|  |     protectedSessionId = null; | ||||||
|  |  | ||||||
|  |     // most secure solution - guarantees nothing remained in memory | ||||||
|  |     // since this expires because user doesn't use the app, it shouldn't be disruptive | ||||||
|  |     utils.reloadApp(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function isProtectedSessionAvailable() { | ||||||
|  |     return protectedSessionId !== null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function touchProtectedSession() { | ||||||
|  |     if (isProtectedSessionAvailable()) { | ||||||
|  |         lastProtectedSessionOperationDate = new Date(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     getProtectedSessionId, | ||||||
|  |     setProtectedSessionId, | ||||||
|  |     resetProtectedSession, | ||||||
|  |     isProtectedSessionAvailable, | ||||||
|  |     setProtectedSessionTimeout, | ||||||
|  |     touchProtectedSession | ||||||
|  | }; | ||||||
| @@ -1,16 +1,12 @@ | |||||||
| function ScriptContext(startNote, allNotes) { | import treeService from './tree.js'; | ||||||
|     return { | import server from './server.js'; | ||||||
|         modules: {}, | import utils from './utils.js'; | ||||||
|         notes: toObject(allNotes, note => [note.noteId, note]), |  | ||||||
|         apis: toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]), |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| function ScriptApi(startNote, currentNote) { | function ScriptApi(startNote, currentNote) { | ||||||
|     const $pluginButtons = $("#plugin-buttons"); |     const $pluginButtons = $("#plugin-buttons"); | ||||||
| 
 | 
 | ||||||
|     async function activateNote(notePath) { |     async function activateNote(notePath) { | ||||||
|         await noteTree.activateNode(notePath); |         await treeService.activateNode(notePath); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function addButtonToToolbar(buttonId, button) { |     function addButtonToToolbar(buttonId, button) { | ||||||
| @@ -56,7 +52,14 @@ function ScriptApi(startNote, currentNote) { | |||||||
|         currentNote: currentNote, |         currentNote: currentNote, | ||||||
|         addButtonToToolbar, |         addButtonToToolbar, | ||||||
|         activateNote, |         activateNote, | ||||||
|         getInstanceName: noteTree.getInstanceName, |         getInstanceName: () => window.glob.instanceName, | ||||||
|         runOnServer |         runOnServer, | ||||||
|  |         formatDateISO: utils.formatDateISO, | ||||||
|  |         parseDate: utils.parseDate, | ||||||
|  |         showMessage: utils.showMessage, | ||||||
|  |         showError: utils.showError, | ||||||
|  |         reloadTree: treeService.reload | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export default ScriptApi; | ||||||
							
								
								
									
										26
									
								
								src/public/javascripts/services/script_context.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/public/javascripts/services/script_context.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | import ScriptApi from './script_api.js'; | ||||||
|  | import utils from './utils.js'; | ||||||
|  |  | ||||||
|  | function ScriptContext(startNote, allNotes) { | ||||||
|  |     const modules = {}; | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |         modules: modules, | ||||||
|  |         notes: utils.toObject(allNotes, note => [note.noteId, note]), | ||||||
|  |         apis: utils.toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]), | ||||||
|  |         require: moduleNoteIds => { | ||||||
|  |             return moduleName => { | ||||||
|  |                 const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId)); | ||||||
|  |                 const note = candidates.find(c => c.title === moduleName); | ||||||
|  |  | ||||||
|  |                 if (!note) { | ||||||
|  |                     throw new Error("Could not find module note " + moduleName); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 return modules[note.noteId].exports; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default ScriptContext; | ||||||
							
								
								
									
										76
									
								
								src/public/javascripts/services/search_tree.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/public/javascripts/services/search_tree.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | import treeService from './tree.js'; | ||||||
|  | import server from './server.js'; | ||||||
|  |  | ||||||
|  | const $tree = $("#tree"); | ||||||
|  | const $searchInput = $("input[name='search-text']"); | ||||||
|  | const $resetSearchButton = $("#reset-search-button"); | ||||||
|  | const $doSearchButton = $("#do-search-button"); | ||||||
|  | const $saveSearchButton = $("#save-search-button"); | ||||||
|  | const $searchBox = $("#search-box"); | ||||||
|  |  | ||||||
|  | function toggleSearch() { | ||||||
|  |     if ($searchBox.is(":hidden")) { | ||||||
|  |         $searchBox.show(); | ||||||
|  |         $searchInput.focus(); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         resetSearch(); | ||||||
|  |  | ||||||
|  |         $searchBox.hide(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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}); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Pass a string to perform case insensitive matching | ||||||
|  |     getTree().filterBranches(node => noteIds.includes(node.data.noteId)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function saveSearch() { | ||||||
|  |     const {noteId} = await server.post('search/' + encodeURIComponent($searchInput.val())); | ||||||
|  |  | ||||||
|  |     resetSearch(); | ||||||
|  |  | ||||||
|  |     await treeService.reload(); | ||||||
|  |  | ||||||
|  |     await treeService.activateNode(noteId); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $searchInput.keyup(e => { | ||||||
|  |     const searchText = $searchInput.val(); | ||||||
|  |  | ||||||
|  |     if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") { | ||||||
|  |         $resetSearchButton.click(); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (e && e.which === $.ui.keyCode.ENTER) { | ||||||
|  |         doSearch(); | ||||||
|  |     } | ||||||
|  | }).focus(); | ||||||
|  |  | ||||||
|  | $doSearchButton.click(doSearch); | ||||||
|  | $resetSearchButton.click(resetSearch); | ||||||
|  |  | ||||||
|  | $saveSearchButton.click(saveSearch); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     toggleSearch | ||||||
|  | }; | ||||||
							
								
								
									
										108
									
								
								src/public/javascripts/services/server.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/public/javascripts/services/server.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | import protectedSessionHolder from './protected_session_holder.js'; | ||||||
|  | import utils from './utils.js'; | ||||||
|  | import infoService from "./info.js"; | ||||||
|  |  | ||||||
|  | function getHeaders() { | ||||||
|  |     let protectedSessionId = null; | ||||||
|  |  | ||||||
|  |     try { // this is because protected session might not be declared in some cases - like when it's included in migration page | ||||||
|  |         protectedSessionId = protectedSessionHolder.getProtectedSessionId(); | ||||||
|  |     } | ||||||
|  |     catch(e) {} | ||||||
|  |  | ||||||
|  |     // headers need to be lowercase because node.js automatically converts them to lower case | ||||||
|  |     // so hypothetical protectedSessionId becomes protectedsessionid on the backend | ||||||
|  |     return { | ||||||
|  |         protected_session_id: protectedSessionId, | ||||||
|  |         source_id: glob.sourceId | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function get(url) { | ||||||
|  |     return await call('GET', url); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function post(url, data) { | ||||||
|  |     return await call('POST', url, data); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function put(url, data) { | ||||||
|  |     return await call('PUT', url, data); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function remove(url) { | ||||||
|  |     return await call('DELETE', url); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | let i = 1; | ||||||
|  | const reqResolves = {}; | ||||||
|  |  | ||||||
|  | async function call(method, url, data) { | ||||||
|  |     if (utils.isElectron()) { | ||||||
|  |         const ipc = require('electron').ipcRenderer; | ||||||
|  |         const requestId = i++; | ||||||
|  |  | ||||||
|  |         return new Promise((resolve, reject) => { | ||||||
|  |             reqResolves[requestId] = resolve; | ||||||
|  |  | ||||||
|  |             console.log(utils.now(), "Request #" + requestId + " to " + method + " " + url); | ||||||
|  |  | ||||||
|  |             ipc.send('server-request', { | ||||||
|  |                 requestId: requestId, | ||||||
|  |                 headers: getHeaders(), | ||||||
|  |                 method: method, | ||||||
|  |                 url: "/" + baseApiUrl + url, | ||||||
|  |                 data: data | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         return await ajax(url, method, data); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function ajax(url, method, data) { | ||||||
|  |     const options = { | ||||||
|  |         url: baseApiUrl + url, | ||||||
|  |         type: method, | ||||||
|  |         headers: getHeaders() | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (data) { | ||||||
|  |         try { | ||||||
|  |             options.data = JSON.stringify(data); | ||||||
|  |         } | ||||||
|  |         catch (e) { | ||||||
|  |             console.log("Can't stringify data: ", data, " because of error: ", e) | ||||||
|  |         } | ||||||
|  |         options.contentType = "application/json"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return await $.ajax(options).catch(e => { | ||||||
|  |         const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText; | ||||||
|  |         infoService.showError(message); | ||||||
|  |         infoService.throwError(message); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | if (utils.isElectron()) { | ||||||
|  |     const ipc = require('electron').ipcRenderer; | ||||||
|  |  | ||||||
|  |     ipc.on('server-response', (event, arg) => { | ||||||
|  |         console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode); | ||||||
|  |  | ||||||
|  |         reqResolves[arg.requestId](arg.body); | ||||||
|  |  | ||||||
|  |         delete reqResolves[arg.requestId]; | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     get, | ||||||
|  |     post, | ||||||
|  |     put, | ||||||
|  |     remove, | ||||||
|  |     ajax, | ||||||
|  |     // don't remove, used from CKEditor image upload! | ||||||
|  |     getHeaders | ||||||
|  | }; | ||||||
							
								
								
									
										30
									
								
								src/public/javascripts/services/sync.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/public/javascripts/services/sync.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | import server from './server.js'; | ||||||
|  | import infoService from "./info.js"; | ||||||
|  |  | ||||||
|  | async function syncNow() { | ||||||
|  |     const result = await server.post('sync/now'); | ||||||
|  |  | ||||||
|  |     if (result.success) { | ||||||
|  |         infoService.showMessage("Sync finished successfully."); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         if (result.message.length > 50) { | ||||||
|  |             result.message = result.message.substr(0, 50); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         infoService.showError("Sync failed: " + result.message); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $("#sync-now-button").click(syncNow); | ||||||
|  |  | ||||||
|  | async function forceNoteSync(noteId) { | ||||||
|  |     await server.post('sync/force-note-sync/' + noteId); | ||||||
|  |  | ||||||
|  |     infoService.showMessage("Note added to sync queue."); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     syncNow, | ||||||
|  |     forceNoteSync | ||||||
|  | }; | ||||||
							
								
								
									
										32
									
								
								src/public/javascripts/services/tooltip.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/public/javascripts/services/tooltip.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | import noteDetailService from "./note_detail.js"; | ||||||
|  | import treeUtils from "./tree_utils.js"; | ||||||
|  | import linkService from "./link.js"; | ||||||
|  |  | ||||||
|  | function setupTooltip() { | ||||||
|  |     $(document).tooltip({ | ||||||
|  |         items: "#note-detail-text a", | ||||||
|  |         content: function (callback) { | ||||||
|  |             const notePath = linkService.getNotePathFromLink($(this).attr("href")); | ||||||
|  |  | ||||||
|  |             if (notePath !== null) { | ||||||
|  |                 const noteId = treeUtils.getNoteIdFromNotePath(notePath); | ||||||
|  |  | ||||||
|  |                 noteDetailService.loadNote(noteId).then(note => callback(note.content)); | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         close: function (event, ui) { | ||||||
|  |             ui.tooltip.hover(function () { | ||||||
|  |                     $(this).stop(true).fadeTo(400, 1); | ||||||
|  |                 }, | ||||||
|  |                 function () { | ||||||
|  |                     $(this).fadeOut('400', function () { | ||||||
|  |                         $(this).remove(); | ||||||
|  |                     }); | ||||||
|  |                 }); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     setupTooltip | ||||||
|  | } | ||||||
							
								
								
									
										568
									
								
								src/public/javascripts/services/tree.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										568
									
								
								src/public/javascripts/services/tree.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,568 @@ | |||||||
|  | import contextMenuService from './context_menu.js'; | ||||||
|  | import dragAndDropSetup from './drag_and_drop.js'; | ||||||
|  | import linkService from './link.js'; | ||||||
|  | import messagingService from './messaging.js'; | ||||||
|  | import noteDetailService from './note_detail.js'; | ||||||
|  | import protectedSessionHolder from './protected_session_holder.js'; | ||||||
|  | import treeChangesService from './branches.js'; | ||||||
|  | import treeUtils from './tree_utils.js'; | ||||||
|  | import utils from './utils.js'; | ||||||
|  | import server from './server.js'; | ||||||
|  | import recentNotesDialog from '../dialogs/recent_notes.js'; | ||||||
|  | import treeCache from './tree_cache.js'; | ||||||
|  | import infoService from "./info.js"; | ||||||
|  | import treeBuilder from "./tree_builder.js"; | ||||||
|  | import treeKeyBindings from "./tree_keybindings.js"; | ||||||
|  | import Branch from '../entities/branch.js'; | ||||||
|  | import NoteShort from '../entities/note_short.js'; | ||||||
|  |  | ||||||
|  | const $tree = $("#tree"); | ||||||
|  | const $parentList = $("#parent-list"); | ||||||
|  | const $parentListList = $("#parent-list-inner"); | ||||||
|  | const $createTopLevelNoteButton = $("#create-top-level-note-button"); | ||||||
|  | const $collapseTreeButton = $("#collapse-tree-button"); | ||||||
|  | const $scrollToCurrentNoteButton = $("#scroll-to-current-note-button"); | ||||||
|  |  | ||||||
|  | let startNotePath = null; | ||||||
|  |  | ||||||
|  | // note that if you want to access data like noteId or isProtected, you need to go into "data" property | ||||||
|  | function getCurrentNode() { | ||||||
|  |     return $tree.fancytree("getActiveNode"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getCurrentNotePath() { | ||||||
|  |     const node = getCurrentNode(); | ||||||
|  |  | ||||||
|  |     return treeUtils.getNotePath(node); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getNodesByBranchId(branchId) { | ||||||
|  |     utils.assertArguments(branchId); | ||||||
|  |  | ||||||
|  |     const branch = await treeCache.getBranch(branchId); | ||||||
|  |  | ||||||
|  |     return getNodesByNoteId(branch.noteId).filter(node => node.data.branchId === branchId); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getNodesByNoteId(noteId) { | ||||||
|  |     utils.assertArguments(noteId); | ||||||
|  |  | ||||||
|  |     const list = getTree().getNodesByRef(noteId); | ||||||
|  |     return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function setPrefix(branchId, prefix) { | ||||||
|  |     utils.assertArguments(branchId); | ||||||
|  |  | ||||||
|  |     const branch = await treeCache.getBranch(branchId); | ||||||
|  |  | ||||||
|  |     branch.prefix = prefix; | ||||||
|  |  | ||||||
|  |     for (const node of await getNodesByBranchId(branchId)) { | ||||||
|  |         await setNodeTitleWithPrefix(node); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function setNodeTitleWithPrefix(node) { | ||||||
|  |     const noteTitle = await treeUtils.getNoteTitle(node.data.noteId); | ||||||
|  |     const branch = await treeCache.getBranch(node.data.branchId); | ||||||
|  |  | ||||||
|  |     const prefix = branch.prefix; | ||||||
|  |  | ||||||
|  |     const title = (prefix ? (prefix + " - ") : "") + noteTitle; | ||||||
|  |  | ||||||
|  |     node.setTitle(utils.escapeHtml(title)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function expandToNote(notePath, expandOpts) { | ||||||
|  |     utils.assertArguments(notePath); | ||||||
|  |  | ||||||
|  |     const runPath = await getRunPath(notePath); | ||||||
|  |  | ||||||
|  |     const noteId = treeUtils.getNoteIdFromNotePath(notePath); | ||||||
|  |  | ||||||
|  |     let parentNoteId = 'root'; | ||||||
|  |  | ||||||
|  |     for (const childNoteId of runPath) { | ||||||
|  |         const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId); | ||||||
|  |  | ||||||
|  |         if (childNoteId === noteId) { | ||||||
|  |             return node; | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             await node.setExpanded(true, expandOpts); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         parentNoteId = childNoteId; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function activateNode(notePath) { | ||||||
|  |     utils.assertArguments(notePath); | ||||||
|  |  | ||||||
|  |     const node = await expandToNote(notePath); | ||||||
|  |  | ||||||
|  |     await node.setActive(); | ||||||
|  |  | ||||||
|  |     clearSelectedNodes(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Accepts notePath and tries to resolve it. Part of the path might not be valid because of note moving (which causes | ||||||
|  |  * path change) or other corruption, in that case this will try to get some other valid path to the correct note. | ||||||
|  |  */ | ||||||
|  | async function getRunPath(notePath) { | ||||||
|  |     utils.assertArguments(notePath); | ||||||
|  |  | ||||||
|  |     const path = notePath.split("/").reverse(); | ||||||
|  |     path.push('root'); | ||||||
|  |  | ||||||
|  |     const effectivePath = []; | ||||||
|  |     let childNoteId = null; | ||||||
|  |     let i = 0; | ||||||
|  |  | ||||||
|  |     while (true) { | ||||||
|  |         if (i >= path.length) { | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const parentNoteId = path[i++]; | ||||||
|  |  | ||||||
|  |         if (childNoteId !== null) { | ||||||
|  |             const child = await treeCache.getNote(childNoteId); | ||||||
|  |             const parents = await child.getParentNotes(); | ||||||
|  |  | ||||||
|  |             if (!parents) { | ||||||
|  |                 messagingService.logError("No parents found for " + childNoteId); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (!parents.some(p => p.noteId === parentNoteId)) { | ||||||
|  |                 console.log(utils.now(), "Did not find parent " + parentNoteId + " for child " + childNoteId); | ||||||
|  |  | ||||||
|  |                 if (parents.length > 0) { | ||||||
|  |                     console.log(utils.now(), "Available parents:", parents); | ||||||
|  |  | ||||||
|  |                     const someNotePath = await getSomeNotePath(parents[0]); | ||||||
|  |  | ||||||
|  |                     if (someNotePath) { // in case it's root the path may be empty | ||||||
|  |                         const pathToRoot = someNotePath.split("/").reverse(); | ||||||
|  |  | ||||||
|  |                         for (const noteId of pathToRoot) { | ||||||
|  |                             effectivePath.push(noteId); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     messagingService.logError("No parents, can't activate node."); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (parentNoteId === 'root') { | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             effectivePath.push(parentNoteId); | ||||||
|  |             childNoteId = parentNoteId; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return effectivePath.reverse(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function showParentList(noteId, node) { | ||||||
|  |     utils.assertArguments(noteId, node); | ||||||
|  |  | ||||||
|  |     const note = await treeCache.getNote(noteId); | ||||||
|  |     const parents = await note.getParentNotes(); | ||||||
|  |  | ||||||
|  |     if (!parents.length) { | ||||||
|  |         infoService.throwError("Can't find parents for noteId=" + noteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (parents.length <= 1) { | ||||||
|  |         $parentList.hide(); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         $parentList.show(); | ||||||
|  |         $parentListList.empty(); | ||||||
|  |  | ||||||
|  |         for (const parentNote of parents) { | ||||||
|  |             const parentNotePath = await getSomeNotePath(parentNote); | ||||||
|  |             // this is to avoid having root notes leading '/' | ||||||
|  |             const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId; | ||||||
|  |             const title = await treeUtils.getNotePathTitle(notePath); | ||||||
|  |  | ||||||
|  |             let item; | ||||||
|  |  | ||||||
|  |             if (node.getParent().data.noteId === parentNote.noteId) { | ||||||
|  |                 item = $("<span/>").attr("title", "Current note").append(title); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 item = linkService.createNoteLink(notePath, title); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             $parentListList.append($("<li/>").append(item)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getSomeNotePath(note) { | ||||||
|  |     utils.assertArguments(note); | ||||||
|  |  | ||||||
|  |     const path = []; | ||||||
|  |  | ||||||
|  |     let cur = note; | ||||||
|  |  | ||||||
|  |     while (cur.noteId !== 'root') { | ||||||
|  |         path.push(cur.noteId); | ||||||
|  |  | ||||||
|  |         const parents = await cur.getParentNotes(); | ||||||
|  |  | ||||||
|  |         if (!parents.length) { | ||||||
|  |             infoService.throwError("Can't find parents for " + cur); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         cur = parents[0]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return path.reverse().join('/'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function setExpandedToServer(branchId, isExpanded) { | ||||||
|  |     utils.assertArguments(branchId); | ||||||
|  |  | ||||||
|  |     const expandedNum = isExpanded ? 1 : 0; | ||||||
|  |  | ||||||
|  |     await server.put('branches/' + branchId + '/expanded/' + expandedNum); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function setCurrentNotePathToHash(node) { | ||||||
|  |     utils.assertArguments(node); | ||||||
|  |  | ||||||
|  |     const currentNotePath = treeUtils.getNotePath(node); | ||||||
|  |     const currentBranchId = node.data.branchId; | ||||||
|  |  | ||||||
|  |     document.location.hash = currentNotePath; | ||||||
|  |  | ||||||
|  |     recentNotesDialog.addRecentNote(currentBranchId, currentNotePath); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getSelectedNodes(stopOnParents = false) { | ||||||
|  |     return getTree().getSelectedNodes(stopOnParents); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function clearSelectedNodes() { | ||||||
|  |     for (const selectedNode of getSelectedNodes()) { | ||||||
|  |         selectedNode.setSelected(false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const currentNode = getCurrentNode(); | ||||||
|  |  | ||||||
|  |     if (currentNode) { | ||||||
|  |         currentNode.setSelected(true); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function treeInitialized() { | ||||||
|  |     const noteId = treeUtils.getNoteIdFromNotePath(startNotePath); | ||||||
|  |  | ||||||
|  |     if (!await treeCache.getNote(noteId)) { | ||||||
|  |         // note doesn't exist so don't try to activate it | ||||||
|  |         startNotePath = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (startNotePath) { | ||||||
|  |         await activateNode(startNotePath); | ||||||
|  |  | ||||||
|  |         // looks like this this doesn't work when triggered immediatelly after activating node | ||||||
|  |         // so waiting a second helps | ||||||
|  |         setTimeout(scrollToCurrentNote, 1000); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function initFancyTree(branch) { | ||||||
|  |     utils.assertArguments(branch); | ||||||
|  |  | ||||||
|  |     $tree.fancytree({ | ||||||
|  |         autoScroll: true, | ||||||
|  |         keyboard: false, // we takover keyboard handling in the hotkeys plugin | ||||||
|  |         extensions: ["hotkeys", "filter", "dnd", "clones"], | ||||||
|  |         source: branch, | ||||||
|  |         scrollParent: $("#tree"), | ||||||
|  |         click: (event, data) => { | ||||||
|  |             const targetType = data.targetType; | ||||||
|  |             const node = data.node; | ||||||
|  |  | ||||||
|  |             if (targetType === 'title' || targetType === 'icon') { | ||||||
|  |                 if (!event.ctrlKey) { | ||||||
|  |                     node.setActive(); | ||||||
|  |                     node.setSelected(true); | ||||||
|  |  | ||||||
|  |                     clearSelectedNodes(); | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     node.setSelected(!node.isSelected()); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         activate: (event, data) => { | ||||||
|  |             const node = data.node.data; | ||||||
|  |  | ||||||
|  |             setCurrentNotePathToHash(data.node); | ||||||
|  |  | ||||||
|  |             noteDetailService.switchToNote(node.noteId); | ||||||
|  |  | ||||||
|  |             showParentList(node.noteId, data.node); | ||||||
|  |         }, | ||||||
|  |         expand: (event, data) => setExpandedToServer(data.node.data.branchId, true), | ||||||
|  |         collapse: (event, data) => setExpandedToServer(data.node.data.branchId, false), | ||||||
|  |         init: (event, data) => treeInitialized(), // don't collapse to short form | ||||||
|  |         hotkeys: { | ||||||
|  |             keydown: treeKeyBindings | ||||||
|  |         }, | ||||||
|  |         filter: { | ||||||
|  |             autoApply: true,   // Re-apply last filter if lazy data is loaded | ||||||
|  |             autoExpand: true, // Expand all branches that contain matches while filtered | ||||||
|  |             counter: false,     // Show a badge with number of matching child nodes near parent icons | ||||||
|  |             fuzzy: false,      // Match single characters in order, e.g. 'fb' will match 'FooBar' | ||||||
|  |             hideExpandedCounter: true,  // Hide counter badge if parent is expanded | ||||||
|  |             hideExpanders: false,       // Hide expanders if all child nodes are hidden by filter | ||||||
|  |             highlight: true,   // Highlight matches by wrapping inside <mark> tags | ||||||
|  |             leavesOnly: false, // Match end nodes only | ||||||
|  |             nodata: true,      // Display a 'no data' status node if result is empty | ||||||
|  |             mode: "hide"       // Grayout unmatched nodes (pass "hide" to remove unmatched node instead) | ||||||
|  |         }, | ||||||
|  |         dnd: dragAndDropSetup, | ||||||
|  |         lazyLoad: function(event, data) { | ||||||
|  |             const noteId = data.node.data.noteId; | ||||||
|  |             data.result = treeCache.getNote(noteId).then(note => treeBuilder.prepareBranch(note)); | ||||||
|  |         }, | ||||||
|  |         clones: { | ||||||
|  |             highlightActiveClones: true | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     $tree.contextmenu(contextMenuService.contextMenuOptions); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getTree() { | ||||||
|  |     return $tree.fancytree('getTree'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function reload() { | ||||||
|  |     const notes = await loadTree(); | ||||||
|  |  | ||||||
|  |     // this will also reload the note content | ||||||
|  |     await getTree().reload(notes); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getNotePathFromAddress() { | ||||||
|  |     return document.location.hash.substr(1); // strip initial # | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function loadTree() { | ||||||
|  |     const resp = await server.get('tree'); | ||||||
|  |     startNotePath = resp.startNotePath; | ||||||
|  |  | ||||||
|  |     if (document.location.hash) { | ||||||
|  |         startNotePath = getNotePathFromAddress(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return await treeBuilder.prepareTree(resp.notes, resp.branches); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function collapseTree(node = null) { | ||||||
|  |     if (!node) { | ||||||
|  |         node = $tree.fancytree("getRootNode"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     node.setExpanded(false); | ||||||
|  |  | ||||||
|  |     node.visit(node => node.setExpanded(false)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function scrollToCurrentNote() { | ||||||
|  |     const node = getCurrentNode(); | ||||||
|  |  | ||||||
|  |     if (node) { | ||||||
|  |         node.makeVisible({scrollIntoView: true}); | ||||||
|  |  | ||||||
|  |         node.setFocus(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function setBranchBackgroundBasedOnProtectedStatus(noteId) { | ||||||
|  |     getNodesByNoteId(noteId).map(node => node.toggleClass("protected", !!node.data.isProtected)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function setProtected(noteId, isProtected) { | ||||||
|  |     getNodesByNoteId(noteId).map(node => node.data.isProtected = isProtected); | ||||||
|  |  | ||||||
|  |     setBranchBackgroundBasedOnProtectedStatus(noteId); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function setNoteTitle(noteId, title) { | ||||||
|  |     utils.assertArguments(noteId); | ||||||
|  |  | ||||||
|  |     const note = await treeCache.getNote(noteId); | ||||||
|  |  | ||||||
|  |     note.title = title; | ||||||
|  |  | ||||||
|  |     for (const clone of getNodesByNoteId(noteId)) { | ||||||
|  |         await setNodeTitleWithPrefix(clone); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function createNewTopLevelNote() { | ||||||
|  |     const rootNode = $tree.fancytree("getRootNode"); | ||||||
|  |  | ||||||
|  |     await createNote(rootNode, "root", "into"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function createNote(node, parentNoteId, target, isProtected) { | ||||||
|  |     utils.assertArguments(node, parentNoteId, target); | ||||||
|  |  | ||||||
|  |     // if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted | ||||||
|  |     // but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often | ||||||
|  |     if (!isProtected || !protectedSessionHolder.isProtectedSessionAvailable()) { | ||||||
|  |         isProtected = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const newNoteName = "new note"; | ||||||
|  |  | ||||||
|  |     const {note, branch} = await server.post('notes/' + parentNoteId + '/children', { | ||||||
|  |         title: newNoteName, | ||||||
|  |         target: target, | ||||||
|  |         target_branchId: node.data.branchId, | ||||||
|  |         isProtected: isProtected | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const noteEntity = new NoteShort(treeCache, note); | ||||||
|  |     const branchEntity = new Branch(treeCache, branch); | ||||||
|  |  | ||||||
|  |     treeCache.add(noteEntity, branchEntity); | ||||||
|  |  | ||||||
|  |     noteDetailService.newNoteCreated(); | ||||||
|  |  | ||||||
|  |     const newNode = { | ||||||
|  |         title: newNoteName, | ||||||
|  |         noteId: branchEntity.noteId, | ||||||
|  |         parentNoteId: parentNoteId, | ||||||
|  |         refKey: branchEntity.noteId, | ||||||
|  |         branchId: branchEntity.branchId, | ||||||
|  |         isProtected: isProtected, | ||||||
|  |         extraClasses: await treeBuilder.getExtraClasses(noteEntity) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (target === 'after') { | ||||||
|  |         await node.appendSibling(newNode).setActive(true); | ||||||
|  |     } | ||||||
|  |     else if (target === 'into') { | ||||||
|  |         if (!node.getChildren() && node.isFolder()) { | ||||||
|  |             await node.setExpanded(); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             node.addChildren(newNode); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await node.getLastChild().setActive(true); | ||||||
|  |  | ||||||
|  |         node.folder = true; | ||||||
|  |         node.renderTitle(); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         infoService.throwError("Unrecognized target: " + target); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     clearSelectedNodes(); // to unmark previously active node | ||||||
|  |  | ||||||
|  |     infoService.showMessage("Created!"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function sortAlphabetically(noteId) { | ||||||
|  |     await server.put('notes/' + noteId + '/sort'); | ||||||
|  |  | ||||||
|  |     await reload(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function showTree() { | ||||||
|  |     const tree = await loadTree(); | ||||||
|  |  | ||||||
|  |     initFancyTree(tree); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | messagingService.subscribeToMessages(syncData => { | ||||||
|  |     if (syncData.some(sync => sync.entityName === 'branches') | ||||||
|  |         || syncData.some(sync => sync.entityName === 'notes')) { | ||||||
|  |  | ||||||
|  |         console.log(utils.now(), "Reloading tree because of background changes"); | ||||||
|  |  | ||||||
|  |         reload(); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | utils.bindShortcut('ctrl+o', () => { | ||||||
|  |     const node = getCurrentNode(); | ||||||
|  |     const parentNoteId = node.data.parentNoteId; | ||||||
|  |     const isProtected = treeUtils.getParentProtectedStatus(node); | ||||||
|  |  | ||||||
|  |     createNote(node, parentNoteId, 'after', isProtected); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | utils.bindShortcut('ctrl+p', () => { | ||||||
|  |     const node = getCurrentNode(); | ||||||
|  |  | ||||||
|  |     createNote(node, node.data.noteId, 'into', node.data.isProtected); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | utils.bindShortcut('ctrl+del', () => { | ||||||
|  |     const node = getCurrentNode(); | ||||||
|  |  | ||||||
|  |     treeChangesService.deleteNodes([node]); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | utils.bindShortcut('ctrl+.', scrollToCurrentNote); | ||||||
|  |  | ||||||
|  | $(window).bind('hashchange', function() { | ||||||
|  |     const notePath = getNotePathFromAddress(); | ||||||
|  |  | ||||||
|  |     if (getCurrentNotePath() !== notePath) { | ||||||
|  |         console.log("Switching to " + notePath + " because of hash change"); | ||||||
|  |  | ||||||
|  |         activateNode(notePath); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | utils.bindShortcut('alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument | ||||||
|  |  | ||||||
|  | $createTopLevelNoteButton.click(createNewTopLevelNote); | ||||||
|  | $collapseTreeButton.click(collapseTree); | ||||||
|  | $scrollToCurrentNoteButton.click(scrollToCurrentNote); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     reload, | ||||||
|  |     collapseTree, | ||||||
|  |     scrollToCurrentNote, | ||||||
|  |     setBranchBackgroundBasedOnProtectedStatus, | ||||||
|  |     setProtected, | ||||||
|  |     expandToNote, | ||||||
|  |     activateNode, | ||||||
|  |     getCurrentNode, | ||||||
|  |     getCurrentNotePath, | ||||||
|  |     setCurrentNotePathToHash, | ||||||
|  |     setNoteTitle, | ||||||
|  |     setPrefix, | ||||||
|  |     createNewTopLevelNote, | ||||||
|  |     createNote, | ||||||
|  |     getSelectedNodes, | ||||||
|  |     clearSelectedNodes, | ||||||
|  |     sortAlphabetically, | ||||||
|  |     showTree | ||||||
|  | }; | ||||||
							
								
								
									
										112
									
								
								src/public/javascripts/services/tree_builder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/public/javascripts/services/tree_builder.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | |||||||
|  | import noteDetailService from "./note_detail.js"; | ||||||
|  | import utils from "./utils.js"; | ||||||
|  | import Branch from "../entities/branch.js"; | ||||||
|  | import server from "./server.js"; | ||||||
|  | import treeCache from "./tree_cache.js"; | ||||||
|  | import messagingService from "./messaging.js"; | ||||||
|  |  | ||||||
|  | async function prepareTree(noteRows, branchRows) { | ||||||
|  |     utils.assertArguments(noteRows); | ||||||
|  |  | ||||||
|  |     treeCache.load(noteRows, branchRows); | ||||||
|  |  | ||||||
|  |     return await prepareRealBranch(await treeCache.getNote('root')); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function prepareBranch(note) { | ||||||
|  |     if (note.type === 'search') { | ||||||
|  |         return await prepareSearchBranch(note); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         return await prepareRealBranch(note); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function prepareRealBranch(parentNote) { | ||||||
|  |     utils.assertArguments(parentNote); | ||||||
|  |  | ||||||
|  |     const childBranches = await parentNote.getChildBranches(); | ||||||
|  |  | ||||||
|  |     if (!childBranches) { | ||||||
|  |         messagingService.logError(`No children for ${parentNote}. This shouldn't happen.`); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const noteList = []; | ||||||
|  |  | ||||||
|  |     for (const branch of childBranches) { | ||||||
|  |         const note = await branch.getNote(); | ||||||
|  |         const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title; | ||||||
|  |  | ||||||
|  |         const node = { | ||||||
|  |             noteId: note.noteId, | ||||||
|  |             parentNoteId: branch.parentNoteId, | ||||||
|  |             branchId: branch.branchId, | ||||||
|  |             isProtected: note.isProtected, | ||||||
|  |             title: utils.escapeHtml(title), | ||||||
|  |             extraClasses: await getExtraClasses(note), | ||||||
|  |             refKey: note.noteId, | ||||||
|  |             expanded: note.type !== 'search' && branch.isExpanded | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         const hasChildren = (await note.getChildNotes()).length > 0; | ||||||
|  |  | ||||||
|  |         if (hasChildren || note.type === 'search') { | ||||||
|  |             node.folder = true; | ||||||
|  |  | ||||||
|  |             if (node.expanded && note.type !== 'search') { | ||||||
|  |                 node.children = await prepareRealBranch(note); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 node.lazy = true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         noteList.push(node); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return noteList; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function prepareSearchBranch(note) { | ||||||
|  |     const fullNote = await noteDetailService.loadNote(note.noteId); | ||||||
|  |     const noteIds = await server.get('search/' + encodeURIComponent(fullNote.jsonContent.searchString)); | ||||||
|  |  | ||||||
|  |     for (const noteId of noteIds) { | ||||||
|  |         const branch = new Branch(treeCache, { | ||||||
|  |             branchId: "virt" + utils.randomString(10), | ||||||
|  |             noteId: noteId, | ||||||
|  |             parentNoteId: note.noteId, | ||||||
|  |             prefix: '', | ||||||
|  |             virtual: true | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         treeCache.addBranch(branch); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return await prepareRealBranch(fullNote); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getExtraClasses(note) { | ||||||
|  |     utils.assertArguments(note); | ||||||
|  |  | ||||||
|  |     const extraClasses = []; | ||||||
|  |  | ||||||
|  |     if (note.isProtected) { | ||||||
|  |         extraClasses.push("protected"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if ((await note.getParentNotes()).length > 1) { | ||||||
|  |         extraClasses.push("multiple-parents"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     extraClasses.push(note.type); | ||||||
|  |  | ||||||
|  |     return extraClasses.join(" "); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     prepareTree, | ||||||
|  |     prepareBranch, | ||||||
|  |     getExtraClasses | ||||||
|  | } | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user