mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Compare commits
	
		
			36 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 25ce2e4253 | ||
|  | c27f573eed | ||
|  | 1d99c4e80b | ||
|  | dc7c64a94d | ||
|  | dc6a530d8c | ||
|  | a674a12706 | ||
|  | c29f1af48f | ||
|  | a8d72c46e4 | ||
|  | ec36fbd83e | ||
|  | aedb05cbab | ||
|  | bb16840a72 | ||
|  | 7b5d44a329 | ||
|  | a53a65be1f | ||
|  | 44af431a93 | ||
|  | caa11b8f7e | ||
|  | 41cce4dcb9 | ||
|  | 858072cc10 | ||
|  | 855c5e0e67 | ||
|  | f0cc3d0bcd | ||
|  | 7672f22ce0 | ||
|  | ef37a52a06 | ||
|  | 2318d615bb | ||
|  | bc14c3d665 | ||
|  | b89ea9a684 | ||
|  | 496767a52b | ||
|  | 9139c597e5 | ||
|  | 942132c01d | ||
|  | 1862acd1ff | ||
|  | ce7e18d0b0 | ||
|  | 65280d5ba3 | ||
|  | 7e3d424e23 | ||
|  | bff04c121a | ||
|  | e8903e82a1 | ||
|  | 0cfd95d9b8 | ||
|  | 1aa5349628 | ||
|  | 4e21d12202 | 
							
								
								
									
										3
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							| @@ -2,6 +2,5 @@ | ||||
| <project version="4"> | ||||
|   <component name="VcsDirectoryMappings"> | ||||
|     <mapping directory="" vcs="Git" /> | ||||
|     <mapping directory="$PROJECT_DIR$/../.." vcs="Git" /> | ||||
|   </component> | ||||
| </project> | ||||
| </project> | ||||
|   | ||||
| @@ -10,7 +10,7 @@ fi | ||||
|  | ||||
| cd dist | ||||
| wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz | ||||
| tar xvfJ node-v${NODE_VERSION}-linux-x64.tar.xz | ||||
| tar xfJ node-v${NODE_VERSION}-linux-x64.tar.xz | ||||
| rm node-v${NODE_VERSION}-linux-x64.tar.xz | ||||
| cd .. | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| npm run webpack | ||||
| n exec 12 npm run webpack | ||||
|  | ||||
| DIR=$1 | ||||
|  | ||||
| @@ -27,7 +27,7 @@ cp -r electron.js $DIR/ | ||||
| cp webpack-* $DIR/ | ||||
|  | ||||
| # run in subshell (so we return to original dir) | ||||
| (cd $DIR && npm install --only=prod) | ||||
| (cd $DIR && n exec 12 npm install --only=prod) | ||||
|  | ||||
| # cleanup of useless files in dependencies | ||||
| rm -r $DIR/node_modules/image-q/demo | ||||
|   | ||||
| @@ -55,47 +55,20 @@ echo "Creating release in GitHub" | ||||
| EXTRA= | ||||
|  | ||||
| if [[ $TAG == *"beta"* ]]; then | ||||
|   EXTRA=--pre-release | ||||
|   EXTRA=--prerelease | ||||
| fi | ||||
|  | ||||
| github-release release \ | ||||
|     --tag $TAG \ | ||||
|     --name "$TAG release" $EXTRA | ||||
| echo "$GITHUB_CLI_AUTH_TOKEN" | gh auth login --with-token | ||||
|  | ||||
| echo "Uploading debian x64 package" | ||||
|  | ||||
| github-release upload \ | ||||
|     --tag $TAG \ | ||||
|     --name "$DEBIAN_X64_BUILD" \ | ||||
|     --file "dist/$DEBIAN_X64_BUILD" | ||||
|  | ||||
| echo "Uploading linux x64 build" | ||||
|  | ||||
| github-release upload \ | ||||
|     --tag $TAG \ | ||||
|     --name "$LINUX_X64_BUILD" \ | ||||
|     --file "dist/$LINUX_X64_BUILD" | ||||
|  | ||||
| echo "Uploading windows x64 build" | ||||
|  | ||||
| github-release upload \ | ||||
|     --tag $TAG \ | ||||
|     --name "$WINDOWS_X64_BUILD" \ | ||||
|     --file "dist/$WINDOWS_X64_BUILD" | ||||
|  | ||||
| echo "Uploading mac x64 build" | ||||
|  | ||||
| github-release upload \ | ||||
|     --tag $TAG \ | ||||
|     --name "$MAC_X64_BUILD" \ | ||||
|     --file "dist/$MAC_X64_BUILD" | ||||
|  | ||||
| echo "Uploading linux x64 server build" | ||||
|  | ||||
| github-release upload \ | ||||
|     --tag $TAG \ | ||||
|     --name "$SERVER_BUILD" \ | ||||
|     --file "dist/$SERVER_BUILD" | ||||
| gh release create "$TAG" \ | ||||
|     --title "$TAG release" \ | ||||
|     --notes "" \ | ||||
|     $EXTRA \ | ||||
|     "dist/$DEBIAN_X64_BUILD" \ | ||||
|     "dist/$LINUX_X64_BUILD" \ | ||||
|     "dist/$WINDOWS_X64_BUILD" \ | ||||
|     "dist/$MAC_X64_BUILD" \ | ||||
|     "dist/$SERVER_BUILD" | ||||
|  | ||||
| echo "Building docker image" | ||||
|  | ||||
| @@ -105,4 +78,4 @@ echo "Pushing docker image to dockerhub" | ||||
|  | ||||
| bin/push-docker-image.sh $VERSION | ||||
|  | ||||
| echo "Release finished!" | ||||
| echo "Release finished!" | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								db/demo.zip
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db/demo.zip
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "version": "0.46.4-beta", | ||||
|   "version": "0.46.7", | ||||
|   "lockfileVersion": 1, | ||||
|   "requires": true, | ||||
|   "dependencies": { | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   "name": "trilium", | ||||
|   "productName": "Trilium Notes", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.46.5", | ||||
|   "version": "0.46.9", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "bin": { | ||||
| @@ -14,7 +14,7 @@ | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "start-server": "cross-env TRILIUM_ENV=dev node ./src/www", | ||||
|     "start-electron": "cross-env TRILIUM_ENV=dev electron .", | ||||
|     "start-electron": "cross-env TRILIUM_ENV=dev electron --inspect=5858 .", | ||||
|     "build-backend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js", | ||||
|     "build-frontend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/collapsible_widget.js", | ||||
|     "build-docs": "npm run build-backend-docs && npm run build-frontend-docs", | ||||
| @@ -51,10 +51,11 @@ | ||||
|     "is-animated": "^2.0.1", | ||||
|     "is-svg": "4.2.1", | ||||
|     "jimp": "0.16.1", | ||||
|     "joplin-turndown-plugin-gfm": "1.0.12", | ||||
|     "jsdom": "16.5.0", | ||||
|     "mime-types": "2.1.29", | ||||
|     "multer": "1.4.2", | ||||
|     "node-abi": "2.21.0", | ||||
|     "node-abi": "2.26.0", | ||||
|     "open": "7.4.2", | ||||
|     "portscanner": "2.2.0", | ||||
|     "rand-token": "1.0.1", | ||||
| @@ -70,7 +71,6 @@ | ||||
|     "striptags": "3.1.1", | ||||
|     "tmp": "^0.2.1", | ||||
|     "turndown": "7.0.0", | ||||
|     "joplin-turndown-plugin-gfm": "1.0.12", | ||||
|     "unescape": "1.0.1", | ||||
|     "ws": "7.4.4", | ||||
|     "yauzl": "2.10.0", | ||||
|   | ||||
| @@ -87,14 +87,16 @@ describe("Lexer expression", () => { | ||||
|             .toEqual(["#label", "*=*", "text"]); | ||||
|     }); | ||||
|  | ||||
|     it("simple label operator with in quotes and without", () => { | ||||
|     it("simple label operator with in quotes", () => { | ||||
|         expect(lex("#label*=*'text'").expressionTokens) | ||||
|             .toEqual([ | ||||
|                 {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, | ||||
|                 {token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8}, | ||||
|                 {token: "text", inQuotes: true, startIndex: 10, endIndex: 13} | ||||
|             ]); | ||||
|     }); | ||||
|  | ||||
|     it("simple label operator with param without quotes", () => { | ||||
|         expect(lex("#label*=*text").expressionTokens) | ||||
|             .toEqual([ | ||||
|                 {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, | ||||
| @@ -103,6 +105,16 @@ describe("Lexer expression", () => { | ||||
|             ]); | ||||
|     }); | ||||
|  | ||||
|     it("simple label operator with empty string param", () => { | ||||
|         expect(lex("#label = ''").expressionTokens) | ||||
|             .toEqual([ | ||||
|                 {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, | ||||
|                 {token: "=", inQuotes: false, startIndex: 7, endIndex: 7}, | ||||
|                 // weird case for empty strings which ends up with endIndex < startIndex :-( | ||||
|                 {token: "", inQuotes: true, startIndex: 10, endIndex: 9} | ||||
|             ]); | ||||
|     }); | ||||
|  | ||||
|     it("note. prefix also separates fulltext from expression", () => { | ||||
|         expect(lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map(t => t.token)) | ||||
|             .toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]); | ||||
|   | ||||
| @@ -19,6 +19,13 @@ function tokens(toks, cur = 0) { | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function assertIsArchived(exp) { | ||||
|     expect(exp.constructor.name).toEqual("PropertyComparisonExp"); | ||||
|     expect(exp.propertyName).toEqual("isArchived"); | ||||
|     expect(exp.operator).toEqual("="); | ||||
|     expect(exp.comparedValue).toEqual("false"); | ||||
| } | ||||
|  | ||||
| describe("Parser", () => { | ||||
|     it("fulltext parser without content", () => { | ||||
|         const rootExp = parse({ | ||||
| @@ -29,8 +36,9 @@ describe("Parser", () => { | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp"); | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("NoteCacheFlatTextExp"); | ||||
|         expect(rootExp.subExpressions[1].tokens).toEqual(["hello", "hi"]); | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpressions[0].constructor.name).toEqual("NoteCacheFlatTextExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpressions[0].tokens).toEqual(["hello", "hi"]); | ||||
|     }); | ||||
|  | ||||
|     it("fulltext parser with content", () => { | ||||
| @@ -40,9 +48,12 @@ describe("Parser", () => { | ||||
|             searchContext: new SearchContext({includeNoteContent: true}) | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("OrExp"); | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         const subs = rootExp.subExpressions; | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); | ||||
|  | ||||
|         const subs = rootExp.subExpressions[1].subExpressions; | ||||
|  | ||||
|         expect(subs[0].constructor.name).toEqual("NoteCacheFlatTextExp"); | ||||
|         expect(subs[0].tokens).toEqual(["hello", "hi"]); | ||||
| @@ -61,10 +72,12 @@ describe("Parser", () => { | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(rootExp.attributeType).toEqual("label"); | ||||
|         expect(rootExp.attributeName).toEqual("mylabel"); | ||||
|         expect(rootExp.comparator).toBeTruthy(); | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(rootExp.subExpressions[1].attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[1].attributeName).toEqual("mylabel"); | ||||
|         expect(rootExp.subExpressions[1].comparator).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     it("simple attribute negation", () => { | ||||
| @@ -74,10 +87,12 @@ describe("Parser", () => { | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("NotExp"); | ||||
|         expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(rootExp.subExpression.attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpression.attributeName).toEqual("mylabel"); | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("NotExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.attributeName).toEqual("mylabel"); | ||||
|  | ||||
|         rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
| @@ -85,10 +100,12 @@ describe("Parser", () => { | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("NotExp"); | ||||
|         expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(rootExp.subExpression.attributeType).toEqual("relation"); | ||||
|         expect(rootExp.subExpression.attributeName).toEqual("myrelation"); | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("NotExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.attributeType).toEqual("relation"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.attributeName).toEqual("myrelation"); | ||||
|     }); | ||||
|  | ||||
|     it("simple label AND", () => { | ||||
| @@ -99,7 +116,10 @@ describe("Parser", () => { | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions; | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
| @@ -116,7 +136,10 @@ describe("Parser", () => { | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions; | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
| @@ -132,8 +155,11 @@ describe("Parser", () => { | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("OrExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions; | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
| @@ -155,8 +181,9 @@ describe("Parser", () => { | ||||
|         expect(firstSub.constructor.name).toEqual("PropertyComparisonExp"); | ||||
|         expect(firstSub.propertyName).toEqual('isArchived'); | ||||
|  | ||||
|         expect(secondSub.constructor.name).toEqual("NoteCacheFlatTextExp"); | ||||
|         expect(secondSub.tokens).toEqual(["hello"]); | ||||
|         expect(secondSub.constructor.name).toEqual("OrExp"); | ||||
|         expect(secondSub.subExpressions[0].constructor.name).toEqual("NoteCacheFlatTextExp"); | ||||
|         expect(secondSub.subExpressions[0].tokens).toEqual(["hello"]); | ||||
|  | ||||
|         expect(thirdSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(thirdSub.attributeName).toEqual("mylabel"); | ||||
| @@ -169,8 +196,11 @@ describe("Parser", () => { | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("OrExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions; | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
| @@ -232,10 +262,12 @@ describe("Invalid expressions", () => { | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(rootExp.attributeType).toEqual("label"); | ||||
|         expect(rootExp.attributeName).toEqual("first"); | ||||
|         expect(rootExp.comparator).toBeTruthy(); | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(rootExp.subExpressions[1].attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[1].attributeName).toEqual("first"); | ||||
|         expect(rootExp.subExpressions[1].comparator).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     it("searching by relation without note property", () => { | ||||
|   | ||||
| @@ -562,8 +562,8 @@ describe("Search", () => { | ||||
|         expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Austria"); | ||||
|         expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy"); | ||||
|  | ||||
|         searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 0', searchContext); | ||||
|         expect(searchResults.length).toEqual(0); | ||||
|         searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1', searchContext); | ||||
|         expect(searchResults.length).toEqual(1); | ||||
|  | ||||
|         searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', searchContext); | ||||
|         expect(searchResults.length).toEqual(4); | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| const {note} = require('./note_cache_mocking.js'); | ||||
| const ValueExtractor = require('../../src/services/search/value_extractor.js'); | ||||
| const noteCache = require('../../src/services/note_cache/note_cache.js'); | ||||
| const SearchContext = require("../../src/services/search/search_context.js"); | ||||
|  | ||||
| const dsc = new SearchContext(); | ||||
|  | ||||
| describe("Value extractor", () => { | ||||
|     beforeEach(() => { | ||||
| @@ -10,7 +13,7 @@ describe("Value extractor", () => { | ||||
|     it("simple title extraction", async () => { | ||||
|         const europe = note("Europe").note; | ||||
|  | ||||
|         const valueExtractor = new ValueExtractor(["note", "title"]); | ||||
|         const valueExtractor = new ValueExtractor(dsc, ["note", "title"]); | ||||
|  | ||||
|         expect(valueExtractor.validate()).toBeFalsy(); | ||||
|         expect(valueExtractor.extract(europe)).toEqual("Europe"); | ||||
| @@ -21,12 +24,12 @@ describe("Value extractor", () => { | ||||
|             .label("Capital", "Vienna") | ||||
|             .note; | ||||
|  | ||||
|         let valueExtractor = new ValueExtractor(["note", "labels", "capital"]); | ||||
|         let valueExtractor = new ValueExtractor(dsc, ["note", "labels", "capital"]); | ||||
|  | ||||
|         expect(valueExtractor.validate()).toBeFalsy(); | ||||
|         expect(valueExtractor.extract(austria)).toEqual("Vienna"); | ||||
|  | ||||
|         valueExtractor = new ValueExtractor(["#capital"]); | ||||
|         valueExtractor = new ValueExtractor(dsc, ["#capital"]); | ||||
|  | ||||
|         expect(valueExtractor.validate()).toBeFalsy(); | ||||
|         expect(valueExtractor.extract(austria)).toEqual("Vienna"); | ||||
| @@ -38,12 +41,12 @@ describe("Value extractor", () => { | ||||
|             .child(note("Austria") | ||||
|                 .child(vienna)); | ||||
|  | ||||
|         let valueExtractor = new ValueExtractor(["note", "children", "children", "title"]); | ||||
|         let valueExtractor = new ValueExtractor(dsc, ["note", "children", "children", "title"]); | ||||
|  | ||||
|         expect(valueExtractor.validate()).toBeFalsy(); | ||||
|         expect(valueExtractor.extract(europe.note)).toEqual("Vienna"); | ||||
|  | ||||
|         valueExtractor = new ValueExtractor(["note", "parents", "parents", "title"]); | ||||
|         valueExtractor = new ValueExtractor(dsc, ["note", "parents", "parents", "title"]); | ||||
|  | ||||
|         expect(valueExtractor.validate()).toBeFalsy(); | ||||
|         expect(valueExtractor.extract(vienna.note)).toEqual("Europe"); | ||||
| @@ -56,12 +59,12 @@ describe("Value extractor", () => { | ||||
|                 .relation('neighbor', czechRepublic.note) | ||||
|                 .relation('neighbor', slovakia.note); | ||||
|  | ||||
|         let valueExtractor = new ValueExtractor(["note", "relations", "neighbor", "labels", "capital"]); | ||||
|         let valueExtractor = new ValueExtractor(dsc, ["note", "relations", "neighbor", "labels", "capital"]); | ||||
|  | ||||
|         expect(valueExtractor.validate()).toBeFalsy(); | ||||
|         expect(valueExtractor.extract(austria.note)).toEqual("Prague"); | ||||
|  | ||||
|         valueExtractor = new ValueExtractor(["~neighbor", "labels", "capital"]); | ||||
|         valueExtractor = new ValueExtractor(dsc, ["~neighbor", "labels", "capital"]); | ||||
|  | ||||
|         expect(valueExtractor.validate()).toBeFalsy(); | ||||
|         expect(valueExtractor.extract(austria.note)).toEqual("Prague"); | ||||
| @@ -70,17 +73,17 @@ describe("Value extractor", () => { | ||||
|  | ||||
| describe("Invalid value extractor property path", () => { | ||||
|     it('each path must start with "note" (or label/relation)', | ||||
|         () => expect(new ValueExtractor(["neighbor"]).validate()).toBeTruthy()); | ||||
|         () => expect(new ValueExtractor(dsc, ["neighbor"]).validate()).toBeTruthy()); | ||||
|  | ||||
|     it("extra path element after terminal label", | ||||
|         () => expect(new ValueExtractor(["~neighbor", "labels", "capital", "noteId"]).validate()).toBeTruthy()); | ||||
|         () => expect(new ValueExtractor(dsc, ["~neighbor", "labels", "capital", "noteId"]).validate()).toBeTruthy()); | ||||
|  | ||||
|     it("extra path element after terminal title", | ||||
|         () => expect(new ValueExtractor(["note", "title", "isProtected"]).validate()).toBeTruthy()); | ||||
|         () => expect(new ValueExtractor(dsc, ["note", "title", "isProtected"]).validate()).toBeTruthy()); | ||||
|  | ||||
|     it("relation name and note property is missing", | ||||
|         () => expect(new ValueExtractor(["note", "relations"]).validate()).toBeTruthy()); | ||||
|         () => expect(new ValueExtractor(dsc, ["note", "relations"]).validate()).toBeTruthy()); | ||||
|  | ||||
|     it("relation is specified but target note property is not specified", | ||||
|         () => expect(new ValueExtractor(["note", "relations", "myrel"]).validate()).toBeTruthy()); | ||||
|         () => expect(new ValueExtractor(dsc, ["note", "relations", "myrel"]).validate()).toBeTruthy()); | ||||
| }); | ||||
|   | ||||
| @@ -49,7 +49,12 @@ class Note extends Entity { | ||||
|             this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable(); | ||||
|  | ||||
|             if (this.isContentAvailable) { | ||||
|                 this.title = protectedSessionService.decryptString(this.title); | ||||
|                 try { | ||||
|                     this.title = protectedSessionService.decryptString(this.title); | ||||
|                 } | ||||
|                 catch (e) { | ||||
|                     throw new Error(`Could not decrypt title of note ${this.noteId}: ${e.message} ${e.stack}`) | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 this.title = "[protected]"; | ||||
| @@ -156,14 +161,14 @@ class Note extends Entity { | ||||
|  | ||||
|         sql.upsert("note_contents", "noteId", pojo); | ||||
|  | ||||
|         const hash = utils.hash(this.noteId + "|" + content.toString()); | ||||
|         const hash = utils.hash(this.noteId + "|" + pojo.content.toString()); | ||||
|  | ||||
|         entityChangesService.addEntityChange({ | ||||
|             entityName: 'note_contents', | ||||
|             entityId: this.noteId, | ||||
|             hash: hash, | ||||
|             isErased: false, | ||||
|             utcDateChanged: this.getUtcDateChanged() | ||||
|             utcDateChanged: pojo.utcDateModified | ||||
|         }, null); | ||||
|     } | ||||
|  | ||||
| @@ -365,7 +370,6 @@ class Note extends Entity { | ||||
|                 WHERE attributes.isDeleted = 0 | ||||
|                   AND attributes.type = 'relation' | ||||
|                   AND attributes.name = 'template' | ||||
|                   AND (treeWithAttrs.level = 0 OR attributes.isInheritable = 1) | ||||
|                 ) | ||||
|             SELECT attributes.* FROM attributes JOIN treeWithAttrs ON attributes.noteId = treeWithAttrs.noteId | ||||
|             WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR treeWithAttrs.level = 0) | ||||
|   | ||||
| @@ -359,7 +359,7 @@ class NoteShort { | ||||
|         const workspaceIconClass = this.getWorkspaceIconClass(); | ||||
|  | ||||
|         if (iconClassLabels.length > 0) { | ||||
|             return iconClassLabels.map(l => l.value).join(' '); | ||||
|             return iconClassLabels[0].value; | ||||
|         } | ||||
|         else if (workspaceIconClass) { | ||||
|             return workspaceIconClass; | ||||
|   | ||||
| @@ -83,6 +83,9 @@ export default class MobileLayout { | ||||
|                     .child(new NoteTitleWidget()) | ||||
|                     .child(new CloseDetailButtonWidget())) | ||||
|                 .child(new NoteDetailWidget() | ||||
|                     .css('padding', '5px 20px 10px 0'))); | ||||
|                     .css('padding', '5px 20px 10px 0') | ||||
|                     .css('overflow', 'auto') | ||||
|                     .css('height', '100%') | ||||
|                 )); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import utils from "./utils.js"; | ||||
| import options from './options.js'; | ||||
| import server from "./server.js"; | ||||
|  | ||||
| const PROTECTED_SESSION_ID_KEY = 'protectedSessionId'; | ||||
|  | ||||
| @@ -23,11 +24,11 @@ function resetSessionCookie() { | ||||
|     utils.setSessionCookie(PROTECTED_SESSION_ID_KEY, null); | ||||
| } | ||||
|  | ||||
| function resetProtectedSession() { | ||||
| async function resetProtectedSession() { | ||||
|     resetSessionCookie(); | ||||
|  | ||||
|     // most secure solution - guarantees nothing remained in memory | ||||
|     // since this expires because user doesn't use the app, it shouldn't be disruptive | ||||
|     await server.post("logout/protected"); | ||||
|  | ||||
|     utils.reloadApp(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -36,7 +36,7 @@ export default class NoteTitleWidget extends TabAwareWidget { | ||||
|  | ||||
|             protectedSessionHolder.touchProtectedSessionIfNecessary(this.note); | ||||
|  | ||||
|             await server.put(`notes/${this.noteId}/change-title`, {title}); | ||||
|             await server.put(`notes/${this.noteId}/change-title`, {title}, this.componentId); | ||||
|         }); | ||||
|  | ||||
|         appContext.addBeforeUnloadListener(this); | ||||
|   | ||||
| @@ -176,6 +176,15 @@ const TPL = ` | ||||
|                       title="Images which are shown in the parent text note will not be displayed in the tree"></span> | ||||
|             </label> | ||||
|         </div> | ||||
|         <div class="form-check"> | ||||
|             <label class="form-check-label"> | ||||
|                 <input class="form-check-input auto-collapse-note-tree" type="checkbox" value=""> | ||||
|                  | ||||
|                 Automatically collapse notes | ||||
|                 <span class="bx bx-info-circle"  | ||||
|                       title="Notes will be collapsed after period of inactivity to declutter the tree."></span> | ||||
|             </label> | ||||
|         </div> | ||||
|      | ||||
|         <br/> | ||||
|      | ||||
| @@ -235,6 +244,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|         this.$treeSettingsPopup = this.$widget.find('.tree-settings-popup'); | ||||
|         this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find('.hide-archived-notes'); | ||||
|         this.$hideIncludedImages = this.$treeSettingsPopup.find('.hide-included-images'); | ||||
|         this.$autoCollapseNoteTree = this.$treeSettingsPopup.find('.auto-collapse-note-tree'); | ||||
|  | ||||
|         this.$treeSettingsButton = this.$widget.find('.tree-settings-button'); | ||||
|         this.$treeSettingsButton.on("click", e => { | ||||
| @@ -245,6 +255,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|  | ||||
|             this.$hideArchivedNotesCheckbox.prop("checked", this.hideArchivedNotes); | ||||
|             this.$hideIncludedImages.prop("checked", this.hideIncludedImages); | ||||
|             this.$autoCollapseNoteTree.prop("checked", this.autoCollapseNoteTree); | ||||
|  | ||||
|             let top = this.$treeSettingsButton[0].offsetTop; | ||||
|             let left = this.$treeSettingsButton[0].offsetLeft; | ||||
| @@ -272,6 +283,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|         this.$saveTreeSettingsButton.on('click', async () => { | ||||
|             await this.setHideArchivedNotes(this.$hideArchivedNotesCheckbox.prop("checked")); | ||||
|             await this.setHideIncludedImages(this.$hideIncludedImages.prop("checked")); | ||||
|             await this.setAutoCollapseNoteTree(this.$autoCollapseNoteTree.prop("checked")); | ||||
|  | ||||
|             this.$treeSettingsPopup.hide(); | ||||
|  | ||||
| @@ -327,6 +339,14 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|         await options.save("hideIncludedImages_" + this.treeName, val.toString()); | ||||
|     } | ||||
|  | ||||
|     get autoCollapseNoteTree() { | ||||
|         return options.is("autoCollapseNoteTree"); | ||||
|     } | ||||
|  | ||||
|     async setAutoCollapseNoteTree(val) { | ||||
|         await options.save("autoCollapseNoteTree", val.toString()); | ||||
|     } | ||||
|  | ||||
|     initFancyTree() { | ||||
|         const treeData = [this.prepareRootNode()]; | ||||
|  | ||||
| @@ -362,8 +382,6 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|                     } | ||||
|                     else { | ||||
|                         node.setActive(); | ||||
|  | ||||
|                         this.clearSelectedNodes(); | ||||
|                     } | ||||
|  | ||||
|                     return false; | ||||
| @@ -373,6 +391,8 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|                 // click event won't propagate so let's close context menu manually | ||||
|                 contextMenu.hide(); | ||||
|  | ||||
|                 this.clearSelectedNodes(); | ||||
|  | ||||
|                 const notePath = treeService.getNotePath(data.node); | ||||
|  | ||||
|                 const activeTabContext = appContext.tabManager.getActiveTabContext(); | ||||
| @@ -797,8 +817,10 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|  | ||||
|             const node = await this.expandToNote(activeContext.notePath); | ||||
|  | ||||
|             await node.makeVisible({scrollIntoView: true}); | ||||
|             node.setActive(true, {noEvents: true, noFocus: false}); | ||||
|             if (node) { | ||||
|                 await node.makeVisible({scrollIntoView: true}); | ||||
|                 node.setActive(true, {noEvents: true, noFocus: false}); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -851,7 +873,11 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|                             // these are real notes with real notePath, user can display them in a detail | ||||
|                             // but they don't have a node in the tree | ||||
|  | ||||
|                             ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`); | ||||
|                             const childNote = await treeCache.getNote(childNoteId); | ||||
|  | ||||
|                             if (!childNote || childNote.type !== 'image') { | ||||
|                                 ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`); | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         return; | ||||
| @@ -955,6 +981,10 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|         } | ||||
|  | ||||
|         this.autoCollapseTimeoutId = setTimeout(() => { | ||||
|             if (!this.autoCollapseNoteTree) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             /* | ||||
|              * We're collapsing notes after period of inactivity to "cleanup" the tree - users rarely | ||||
|              * collapse the notes and the tree becomes unusuably large. | ||||
| @@ -1114,11 +1144,12 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|             } | ||||
|  | ||||
|             if (node) { | ||||
|                 node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused}); | ||||
|  | ||||
|                 if (activeNodeFocused) { | ||||
|                     node.setFocus(true); | ||||
|                     // needed by Firefox: https://github.com/zadam/trilium/issues/1865 | ||||
|                     this.tree.$container.focus(); | ||||
|                 } | ||||
|  | ||||
|                 await node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused}); | ||||
|             } | ||||
|             else { | ||||
|                 // this is used when original note has been deleted and we want to move the focus to the note above/below | ||||
|   | ||||
| @@ -5,7 +5,7 @@ const TPL = ` | ||||
|     <td colspan="2"> | ||||
|         <span class="bx bx-trash"></span> | ||||
|      | ||||
|         Delete matched note | ||||
|         Delete matched notes | ||||
|     </td> | ||||
|     <td class="button-column"> | ||||
|         <span class="bx bx-x icon-action action-conf-del"></span> | ||||
|   | ||||
| @@ -14,6 +14,10 @@ const TPL = ` | ||||
|         height: 35px; | ||||
|     } | ||||
|      | ||||
|     .standard-top-widget > div { | ||||
|         flex-shrink: 0; /* fixes https://github.com/zadam/trilium/issues/1745 */ | ||||
|     } | ||||
|      | ||||
|     .standard-top-widget button.noborder { | ||||
|         padding: 1px 5px 1px 5px; | ||||
|         font-size: 90%; | ||||
|   | ||||
| @@ -5,24 +5,47 @@ import linkService from "../../services/link.js"; | ||||
| import noteContentRenderer from "../../services/note_content_renderer.js"; | ||||
|  | ||||
| export default class AbstractTextTypeWidget extends TypeWidget { | ||||
|     doRender() { | ||||
|         this.$widget.on("dblclick", "img", e => { | ||||
|             const $img = $(e.target); | ||||
|             const src = $img.prop("src"); | ||||
|     setupImageOpening(singleClickOpens) { | ||||
|         this.$widget.on("dblclick", "img", e => this.openImageInCurrentTab($(e.target))); | ||||
|  | ||||
|             const match = src.match(/\/api\/images\/([A-Za-z0-9]+)\//); | ||||
|  | ||||
|             if (match) { | ||||
|                 const noteId = match[1]; | ||||
|  | ||||
|                 appContext.tabManager.getActiveTabContext().setNote(noteId); | ||||
|         this.$widget.on("click", "img", e => { | ||||
|             if ((e.which === 1 && e.ctrlKey) || e.which === 2) { | ||||
|                 this.openImageInNewTab($(e.target)); | ||||
|             } | ||||
|             else { | ||||
|                 window.open(src, '_blank'); | ||||
|             else if (e.which === 1 && singleClickOpens) { | ||||
|                 this.openImageInCurrentTab($(e.target)); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     openImageInCurrentTab($img) { | ||||
|         const imgSrc = $img.prop("src"); | ||||
|         const noteId = this.getNoteIdFromImage(imgSrc); | ||||
|  | ||||
|         if (noteId) { | ||||
|             appContext.tabManager.getActiveTabContext().setNote(noteId); | ||||
|         } else { | ||||
|             window.open(imgSrc, '_blank'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     openImageInNewTab($img) { | ||||
|         const imgSrc = $img.prop("src"); | ||||
|         const noteId = this.getNoteIdFromImage(imgSrc); | ||||
|  | ||||
|         if (noteId) { | ||||
|             appContext.tabManager.openTabWithNoteWithHoisting(noteId); | ||||
|         } else { | ||||
|             window.open(imgSrc, '_blank'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     getNoteIdFromImage(imgSrc) { | ||||
|         const match = imgSrc.match(/\/api\/images\/([A-Za-z0-9]+)\//); | ||||
|  | ||||
|         return match ? match[1] : null; | ||||
|     } | ||||
|  | ||||
|     async loadIncludedNote(noteId, $el) { | ||||
|         const note = await treeCache.getNote(noteId); | ||||
|  | ||||
|   | ||||
| @@ -84,6 +84,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|  | ||||
|         keyboardActionService.setupActionsForElement('text-detail', this.$widget, this); | ||||
|  | ||||
|         this.setupImageOpening(false); | ||||
|  | ||||
|         super.doRender(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -5,6 +5,7 @@ const TPL = ` | ||||
|     <style> | ||||
|     .note-detail-read-only-code { | ||||
|         position: relative; | ||||
|         min-height: 50px; | ||||
|     } | ||||
|      | ||||
|     .note-detail-read-only-code-content { | ||||
|   | ||||
| @@ -25,6 +25,7 @@ const TPL = ` | ||||
|         padding-top: 10px; | ||||
|         font-family: var(--detail-text-font-family); | ||||
|         position: relative; | ||||
|         min-height: 50px; | ||||
|     } | ||||
|          | ||||
|     .note-detail-readonly-text p:first-child, .note-detail-readonly-text::before { | ||||
| @@ -33,6 +34,7 @@ const TPL = ` | ||||
|      | ||||
|     .note-detail-readonly-text img { | ||||
|         max-width: 100%; | ||||
|         cursor: pointer; | ||||
|     } | ||||
|      | ||||
|     .edit-text-note-container { | ||||
| @@ -65,6 +67,8 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget { | ||||
|             this.triggerEvent('textPreviewDisabled', {tabContext: this.tabContext}); | ||||
|         }); | ||||
|  | ||||
|         this.setupImageOpening(true); | ||||
|  | ||||
|         super.doRender(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -107,7 +107,11 @@ function processContent(images, note, content) { | ||||
|             const filename = path.basename(src); | ||||
|  | ||||
|             if (!dataUrl || !dataUrl.startsWith("data:image")) { | ||||
|                 log.info("Image could not be recognized as data URL:", dataUrl.substr(0, Math.min(100, dataUrl.length))); | ||||
|                 const excerpt = dataUrl | ||||
|                     ? dataUrl.substr(0, Math.min(100, dataUrl.length)) | ||||
|                     : "null"; | ||||
|  | ||||
|                 log.info("Image could not be recognized as data URL: " + excerpt); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -78,6 +78,12 @@ function loginToProtectedSession(req) { | ||||
|     }; | ||||
| } | ||||
|  | ||||
| function logoutFromProtectedSession() { | ||||
|     protectedSessionService.resetDataKey(); | ||||
|  | ||||
|     eventService.emit(eventService.LEAVE_PROTECTED_SESSION); | ||||
| } | ||||
|  | ||||
| function token(req) { | ||||
|     const username = req.body.username; | ||||
|     const password = req.body.password; | ||||
| @@ -101,5 +107,6 @@ function token(req) { | ||||
| module.exports = { | ||||
|     loginSync, | ||||
|     loginToProtectedSession, | ||||
|     logoutFromProtectedSession, | ||||
|     token | ||||
| }; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ const noteCacheService = require('../../services/note_cache/note_cache_service') | ||||
| const protectedSessionService = require('../../services/protected_session'); | ||||
| const noteRevisionService = require('../../services/note_revisions'); | ||||
| const utils = require('../../services/utils'); | ||||
| const sql = require('../../services/sql'); | ||||
| const path = require('path'); | ||||
|  | ||||
| function getNoteRevisions(req) { | ||||
|   | ||||
| @@ -41,7 +41,8 @@ const ALLOWED_OPTIONS = new Set([ | ||||
|     'attributeListExpanded', | ||||
|     'promotedAttributesExpanded', | ||||
|     'similarNotesExpanded', | ||||
|     'headingStyle' | ||||
|     'headingStyle', | ||||
|     'autoCollapseNoteTree' | ||||
| ]); | ||||
|  | ||||
| function getOptions() { | ||||
|   | ||||
| @@ -200,9 +200,7 @@ function queueSector(req) { | ||||
|     const entityName = utils.sanitizeSqlIdentifier(req.params.entityName); | ||||
|     const sector = utils.sanitizeSqlIdentifier(req.params.sector); | ||||
|  | ||||
|     const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; | ||||
|  | ||||
|     entityChangesService.addEntityChangesForSector(entityName, entityPrimaryKey, sector); | ||||
|     entityChangesService.addEntityChangesForSector(entityName, sector); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   | ||||
| @@ -270,6 +270,8 @@ function register(app) { | ||||
|     route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); | ||||
|     // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username) | ||||
|     apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession); | ||||
|     apiRoute(POST, '/api/logout/protected', loginApiRoute.logoutFromProtectedSession); | ||||
|  | ||||
|     route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler); | ||||
|  | ||||
|     // in case of local electron, local calls are allowed unauthenticated, for server they need auth | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| module.exports = { buildDate:"2021-03-14T22:56:27+01:00", buildRevision: "6c8d20288df302f3a415bd1bdcace98bf29d4bf6" }; | ||||
| module.exports = { buildDate:"2021-04-22T20:50:22+02:00", buildRevision: "c27f573eed8905fc1d958adf5bdee0efc3aff593" }; | ||||
|   | ||||
| @@ -53,22 +53,14 @@ function moveEntityChangeToTop(entityName, entityId) { | ||||
|     addEntityChange(entityName, entityId, hash, null, isSynced); | ||||
| } | ||||
|  | ||||
| function addEntityChangesForSector(entityName, entityPrimaryKey, sector) { | ||||
| function addEntityChangesForSector(entityName, sector) { | ||||
|     const startTime = Date.now(); | ||||
|     const repository = require('./repository'); | ||||
|  | ||||
|     const entityChanges = sql.getRows(`SELECT * FROM entity_changes WHERE entityName = ? AND SUBSTR(entityId, 1, 1) = ?`, [entityName, sector]); | ||||
|  | ||||
|     sql.transactional(() => { | ||||
|         const entityIds = sql.getColumn(`SELECT ${entityPrimaryKey} FROM ${entityName} WHERE SUBSTR(${entityPrimaryKey}, 1, 1) = ?`, [sector]); | ||||
|  | ||||
|         for (const entityId of entityIds) { | ||||
|             // retrieving entity one by one to avoid memory issues with note_contents | ||||
|             const entity = repository.getEntity(`SELECT * FROM ${entityName} WHERE ${entityPrimaryKey} = ?`, [entityId]); | ||||
|  | ||||
|             if (entityName === 'options' && !entity.isSynced) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             insertEntityChange(entityName, entityId, entity.generateHash(), false, entity.getUtcDateChanged(), 'content-check', true); | ||||
|         for (const ec of entityChanges) { | ||||
|             insertEntityChange(entityName, ec.entityId, ec.hash, ec.isErased, ec.utcDateChanged, ec.sourceId, ec.isSynced); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ const log = require('./log'); | ||||
|  | ||||
| const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED"; | ||||
| const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION"; | ||||
| const LEAVE_PROTECTED_SESSION = "LEAVE_PROTECTED_SESSION"; | ||||
| const ENTITY_CREATED = "ENTITY_CREATED"; | ||||
| const ENTITY_CHANGED = "ENTITY_CHANGED"; | ||||
| const ENTITY_DELETED = "ENTITY_DELETED"; | ||||
| @@ -47,6 +48,7 @@ module.exports = { | ||||
|     // event types: | ||||
|     NOTE_TITLE_CHANGED, | ||||
|     ENTER_PROTECTED_SESSION, | ||||
|     LEAVE_PROTECTED_SESSION, | ||||
|     ENTITY_CREATED, | ||||
|     ENTITY_CHANGED, | ||||
|     ENTITY_DELETED, | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const protectedSessionService = require('../../protected_session'); | ||||
| const log = require('../../log'); | ||||
|  | ||||
| class Note { | ||||
|     constructor(noteCache, row) { | ||||
| @@ -405,7 +406,7 @@ class Note { | ||||
|             return 0; | ||||
|         } | ||||
|  | ||||
|         let minDistance = 999_999; | ||||
|         let minDistance = 999999; | ||||
|  | ||||
|         for (const parent of this.parents) { | ||||
|             minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1); | ||||
| @@ -416,9 +417,14 @@ class Note { | ||||
|  | ||||
|     decrypt() { | ||||
|         if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) { | ||||
|             this.title = protectedSessionService.decryptString(this.title); | ||||
|             try { | ||||
|                 this.title = protectedSessionService.decryptString(this.title); | ||||
|  | ||||
|             this.isDecrypted = true; | ||||
|                 this.isDecrypted = true; | ||||
|             } | ||||
|             catch (e) { | ||||
|                 log.error(`Could not decrypt note ${this.noteId}: ${e.message} ${e.stack}`); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -177,6 +177,10 @@ eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => { | ||||
|     } | ||||
| }); | ||||
|  | ||||
| eventService.subscribe(eventService.LEAVE_PROTECTED_SESSION, () => { | ||||
|     load(); | ||||
| }); | ||||
|  | ||||
| module.exports = { | ||||
|     load | ||||
| }; | ||||
|   | ||||
| @@ -233,7 +233,7 @@ async function findSimilarNotes(noteId) { | ||||
|  | ||||
|     const baseNote = noteCache.notes[noteId]; | ||||
|  | ||||
|     if (!baseNote) { | ||||
|     if (!baseNote || !baseNote.utcDateCreated) { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -86,6 +86,7 @@ const defaultOptions = [ | ||||
|     { name: 'similarNotesExpanded', value: 'true', isSynced: true }, | ||||
|     { name: 'debugModeEnabled', value: 'false', isSynced: false }, | ||||
|     { name: 'headingStyle', value: 'markdown', isSynced: true }, | ||||
|     { name: 'autoCollapseNoteTree', value: 'true', isSynced: true }, | ||||
| ]; | ||||
|  | ||||
| function initStartupOptions() { | ||||
|   | ||||
| @@ -5,7 +5,7 @@ const log = require('./log'); | ||||
| const dataEncryptionService = require('./data_encryption'); | ||||
| const cls = require('./cls'); | ||||
|  | ||||
| const dataKeyMap = {}; | ||||
| let dataKeyMap = {}; | ||||
|  | ||||
| function setDataKey(decryptedDataKey) { | ||||
|     const protectedSessionId = utils.randomSecureToken(32); | ||||
| @@ -29,6 +29,10 @@ function getDataKey() { | ||||
|     return dataKeyMap[protectedSessionId]; | ||||
| } | ||||
|  | ||||
| function resetDataKey() { | ||||
|     dataKeyMap = {}; | ||||
| } | ||||
|  | ||||
| function isProtectedSessionAvailable() { | ||||
|     const protectedSessionId = getProtectedSessionId(); | ||||
|  | ||||
| @@ -71,6 +75,7 @@ function decryptString(cipherText) { | ||||
| module.exports = { | ||||
|     setDataKey, | ||||
|     getDataKey, | ||||
|     resetDataKey, | ||||
|     isProtectedSessionAvailable, | ||||
|     encrypt, | ||||
|     decrypt, | ||||
|   | ||||
| @@ -21,8 +21,8 @@ function lex(str) { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function finishWord(endIndex) { | ||||
|         if (currentWord === '') { | ||||
|     function finishWord(endIndex, createAlsoForEmptyWords = false) { | ||||
|         if (currentWord === '' && !createAlsoForEmptyWords) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @@ -71,7 +71,7 @@ function lex(str) { | ||||
|                 } | ||||
|             } | ||||
|             else if (quotes === chr) { | ||||
|                 finishWord(i - 1); | ||||
|                 finishWord(i - 1, true); | ||||
|  | ||||
|                 quotes = false; | ||||
|             } | ||||
|   | ||||
| @@ -245,9 +245,7 @@ async function checkContentHash(syncContext) { | ||||
|     const failedChecks = contentHashService.checkContentHashes(resp.entityHashes); | ||||
|  | ||||
|     for (const {entityName, sector} of failedChecks) { | ||||
|         const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; | ||||
|  | ||||
|         entityChangesService.addEntityChangesForSector(entityName, entityPrimaryKey, sector); | ||||
|         entityChangesService.addEntityChangesForSector(entityName, sector); | ||||
|  | ||||
|         await syncRequest(syncContext, 'POST', `/api/sync/queue-sector/${entityName}/${sector}`); | ||||
|     } | ||||
|   | ||||
| @@ -26,9 +26,7 @@ function updateEntity(entityChange, entity, sourceId) { | ||||
|         ? updateNoteReordering(entityChange, entity, sourceId) | ||||
|         : updateNormalEntity(entityChange, entity, sourceId); | ||||
|  | ||||
|     // currently making exception for protected notes and note revisions because here | ||||
|     // the title and content are not available decrypted as listeners would expect | ||||
|     if (updated && !entity.isProtected && !entityChange.isErased) { | ||||
|     if (updated && !entityChange.isErased) { | ||||
|         eventService.emit(eventService.ENTITY_SYNCED, { | ||||
|             entityName: entityChange.entityName, | ||||
|             entity | ||||
| @@ -44,7 +42,7 @@ function updateNormalEntity(remoteEntityChange, entity, sourceId) { | ||||
|  | ||||
|     if (localEntityChange && !localEntityChange.isErased && remoteEntityChange.isErased) { | ||||
|         sql.transactional(() => { | ||||
|             const primaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; | ||||
|             const primaryKey = entityConstructor.getEntityFromEntityName(remoteEntityChange.entityName).primaryKeyName; | ||||
|  | ||||
|             sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId); | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const sql = require('./sql'); | ||||
| const log = require('./log'); | ||||
| const repository = require('./repository'); | ||||
| const Branch = require('../entities/branch'); | ||||
| const entityChangesService = require('./entity_changes.js'); | ||||
| @@ -139,7 +140,12 @@ function sortNotesByTitle(parentNoteId, foldersFirst = false, reverse = false) { | ||||
|             sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", | ||||
|                 [position, note.branchId]); | ||||
|  | ||||
|             noteCache.branches[note.branchId].notePosition = position; | ||||
|             if (note.branchId in noteCache.branches) { | ||||
|                 noteCache.branches[note.branchId].notePosition = position; | ||||
|             } | ||||
|             else { | ||||
|                 log.info(`Branch "${note.branchId}" was not found in note cache.`); | ||||
|             } | ||||
|  | ||||
|             position += 10; | ||||
|         } | ||||
|   | ||||
| @@ -7,6 +7,9 @@ | ||||
|       <sourceFolder url="file://$MODULE_DIR$/src/public" isTestSource="false" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/dist" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/src/public/app-dist" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/libraries" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/docs" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/bin/better-sqlite3" /> | ||||
|     </content> | ||||
|     <orderEntry type="inheritedJdk" /> | ||||
|     <orderEntry type="sourceFolder" forTests="false" /> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user