mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Compare commits
	
		
			46 Commits
		
	
	
		
			v0.46.4-be
			...
			v0.46.9
		
	
	| 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 | ||
|  | fdce218e88 | ||
|  | 6c8d20288d | ||
|  | 88d04772c4 | ||
|  | 9fd26a9b9f | ||
|  | 98f02c3c9a | ||
|  | 584fea1992 | ||
|  | af50a1ec52 | ||
|  | 4e76d1fa85 | ||
|  | 03a11e6f77 | ||
|  | e1a16b4a9f | 
							
								
								
									
										1
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							| @@ -2,6 +2,5 @@ | |||||||
| <project version="4"> | <project version="4"> | ||||||
|   <component name="VcsDirectoryMappings"> |   <component name="VcsDirectoryMappings"> | ||||||
|     <mapping directory="" vcs="Git" /> |     <mapping directory="" vcs="Git" /> | ||||||
|     <mapping directory="$PROJECT_DIR$/../.." vcs="Git" /> |  | ||||||
|   </component> |   </component> | ||||||
| </project> | </project> | ||||||
| @@ -10,7 +10,7 @@ fi | |||||||
|  |  | ||||||
| cd dist | cd dist | ||||||
| wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz | 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 | rm node-v${NODE_VERSION}-linux-x64.tar.xz | ||||||
| cd .. | cd .. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then | |||||||
|     exit 1 |     exit 1 | ||||||
| fi | fi | ||||||
|  |  | ||||||
| npm run webpack | n exec 12 npm run webpack | ||||||
|  |  | ||||||
| DIR=$1 | DIR=$1 | ||||||
|  |  | ||||||
| @@ -27,7 +27,7 @@ cp -r electron.js $DIR/ | |||||||
| cp webpack-* $DIR/ | cp webpack-* $DIR/ | ||||||
|  |  | ||||||
| # run in subshell (so we return to original 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 | # cleanup of useless files in dependencies | ||||||
| rm -r $DIR/node_modules/image-q/demo | rm -r $DIR/node_modules/image-q/demo | ||||||
|   | |||||||
| @@ -55,47 +55,20 @@ echo "Creating release in GitHub" | |||||||
| EXTRA= | EXTRA= | ||||||
|  |  | ||||||
| if [[ $TAG == *"beta"* ]]; then | if [[ $TAG == *"beta"* ]]; then | ||||||
|   EXTRA=--pre-release |   EXTRA=--prerelease | ||||||
| fi | fi | ||||||
|  |  | ||||||
| github-release release \ | echo "$GITHUB_CLI_AUTH_TOKEN" | gh auth login --with-token | ||||||
|     --tag $TAG \ |  | ||||||
|     --name "$TAG release" $EXTRA |  | ||||||
|  |  | ||||||
| echo "Uploading debian x64 package" | gh release create "$TAG" \ | ||||||
|  |     --title "$TAG release" \ | ||||||
| github-release upload \ |     --notes "" \ | ||||||
|     --tag $TAG \ |     $EXTRA \ | ||||||
|     --name "$DEBIAN_X64_BUILD" \ |     "dist/$DEBIAN_X64_BUILD" \ | ||||||
|     --file "dist/$DEBIAN_X64_BUILD" |     "dist/$LINUX_X64_BUILD" \ | ||||||
|  |     "dist/$WINDOWS_X64_BUILD" \ | ||||||
| echo "Uploading linux x64 build" |     "dist/$MAC_X64_BUILD" \ | ||||||
|  |     "dist/$SERVER_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" |  | ||||||
|  |  | ||||||
| echo "Building docker image" | echo "Building docker image" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										
											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", |   "name": "trilium", | ||||||
|   "version": "0.46.3-beta", |   "version": "0.46.7", | ||||||
|   "lockfileVersion": 1, |   "lockfileVersion": 1, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "productName": "Trilium Notes", |   "productName": "Trilium Notes", | ||||||
|   "description": "Trilium Notes", |   "description": "Trilium Notes", | ||||||
|   "version": "0.46.4-beta", |   "version": "0.46.9", | ||||||
|   "license": "AGPL-3.0-only", |   "license": "AGPL-3.0-only", | ||||||
|   "main": "electron.js", |   "main": "electron.js", | ||||||
|   "bin": { |   "bin": { | ||||||
| @@ -14,7 +14,7 @@ | |||||||
|   }, |   }, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start-server": "cross-env TRILIUM_ENV=dev node ./src/www", |     "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-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-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", |     "build-docs": "npm run build-backend-docs && npm run build-frontend-docs", | ||||||
| @@ -51,10 +51,11 @@ | |||||||
|     "is-animated": "^2.0.1", |     "is-animated": "^2.0.1", | ||||||
|     "is-svg": "4.2.1", |     "is-svg": "4.2.1", | ||||||
|     "jimp": "0.16.1", |     "jimp": "0.16.1", | ||||||
|  |     "joplin-turndown-plugin-gfm": "1.0.12", | ||||||
|     "jsdom": "16.5.0", |     "jsdom": "16.5.0", | ||||||
|     "mime-types": "2.1.29", |     "mime-types": "2.1.29", | ||||||
|     "multer": "1.4.2", |     "multer": "1.4.2", | ||||||
|     "node-abi": "2.21.0", |     "node-abi": "2.26.0", | ||||||
|     "open": "7.4.2", |     "open": "7.4.2", | ||||||
|     "portscanner": "2.2.0", |     "portscanner": "2.2.0", | ||||||
|     "rand-token": "1.0.1", |     "rand-token": "1.0.1", | ||||||
| @@ -70,7 +71,6 @@ | |||||||
|     "striptags": "3.1.1", |     "striptags": "3.1.1", | ||||||
|     "tmp": "^0.2.1", |     "tmp": "^0.2.1", | ||||||
|     "turndown": "7.0.0", |     "turndown": "7.0.0", | ||||||
|     "joplin-turndown-plugin-gfm": "1.0.12", |  | ||||||
|     "unescape": "1.0.1", |     "unescape": "1.0.1", | ||||||
|     "ws": "7.4.4", |     "ws": "7.4.4", | ||||||
|     "yauzl": "2.10.0", |     "yauzl": "2.10.0", | ||||||
|   | |||||||
| @@ -87,14 +87,16 @@ describe("Lexer expression", () => { | |||||||
|             .toEqual(["#label", "*=*", "text"]); |             .toEqual(["#label", "*=*", "text"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("simple label operator with in quotes and without", () => { |     it("simple label operator with in quotes", () => { | ||||||
|         expect(lex("#label*=*'text'").expressionTokens) |         expect(lex("#label*=*'text'").expressionTokens) | ||||||
|             .toEqual([ |             .toEqual([ | ||||||
|                 {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, |                 {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, | ||||||
|                 {token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8}, |                 {token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8}, | ||||||
|                 {token: "text", inQuotes: true, startIndex: 10, endIndex: 13} |                 {token: "text", inQuotes: true, startIndex: 10, endIndex: 13} | ||||||
|             ]); |             ]); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it("simple label operator with param without quotes", () => { | ||||||
|         expect(lex("#label*=*text").expressionTokens) |         expect(lex("#label*=*text").expressionTokens) | ||||||
|             .toEqual([ |             .toEqual([ | ||||||
|                 {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, |                 {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", () => { |     it("note. prefix also separates fulltext from expression", () => { | ||||||
|         expect(lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map(t => t.token)) |         expect(lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map(t => t.token)) | ||||||
|             .toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]); |             .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", () => { | describe("Parser", () => { | ||||||
|     it("fulltext parser without content", () => { |     it("fulltext parser without content", () => { | ||||||
|         const rootExp = parse({ |         const rootExp = parse({ | ||||||
| @@ -29,8 +36,9 @@ describe("Parser", () => { | |||||||
|  |  | ||||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); |         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||||
|         expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp"); |         expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp"); | ||||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("NoteCacheFlatTextExp"); |         expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); | ||||||
|         expect(rootExp.subExpressions[1].tokens).toEqual(["hello", "hi"]); |         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", () => { |     it("fulltext parser with content", () => { | ||||||
| @@ -40,9 +48,12 @@ describe("Parser", () => { | |||||||
|             searchContext: new SearchContext({includeNoteContent: true}) |             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].constructor.name).toEqual("NoteCacheFlatTextExp"); | ||||||
|         expect(subs[0].tokens).toEqual(["hello", "hi"]); |         expect(subs[0].tokens).toEqual(["hello", "hi"]); | ||||||
| @@ -61,10 +72,12 @@ describe("Parser", () => { | |||||||
|             searchContext: new SearchContext() |             searchContext: new SearchContext() | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         expect(rootExp.constructor.name).toEqual("LabelComparisonExp"); |         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||||
|         expect(rootExp.attributeType).toEqual("label"); |         assertIsArchived(rootExp.subExpressions[0]); | ||||||
|         expect(rootExp.attributeName).toEqual("mylabel"); |         expect(rootExp.subExpressions[1].constructor.name).toEqual("LabelComparisonExp"); | ||||||
|         expect(rootExp.comparator).toBeTruthy(); |         expect(rootExp.subExpressions[1].attributeType).toEqual("label"); | ||||||
|  |         expect(rootExp.subExpressions[1].attributeName).toEqual("mylabel"); | ||||||
|  |         expect(rootExp.subExpressions[1].comparator).toBeTruthy(); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("simple attribute negation", () => { |     it("simple attribute negation", () => { | ||||||
| @@ -74,10 +87,12 @@ describe("Parser", () => { | |||||||
|             searchContext: new SearchContext() |             searchContext: new SearchContext() | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         expect(rootExp.constructor.name).toEqual("NotExp"); |         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||||
|         expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp"); |         assertIsArchived(rootExp.subExpressions[0]); | ||||||
|         expect(rootExp.subExpression.attributeType).toEqual("label"); |         expect(rootExp.subExpressions[1].constructor.name).toEqual("NotExp"); | ||||||
|         expect(rootExp.subExpression.attributeName).toEqual("mylabel"); |         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({ |         rootExp = parse({ | ||||||
|             fulltextTokens: [], |             fulltextTokens: [], | ||||||
| @@ -85,10 +100,12 @@ describe("Parser", () => { | |||||||
|             searchContext: new SearchContext() |             searchContext: new SearchContext() | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         expect(rootExp.constructor.name).toEqual("NotExp"); |         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||||
|         expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp"); |         assertIsArchived(rootExp.subExpressions[0]); | ||||||
|         expect(rootExp.subExpression.attributeType).toEqual("relation"); |         expect(rootExp.subExpressions[1].constructor.name).toEqual("NotExp"); | ||||||
|         expect(rootExp.subExpression.attributeName).toEqual("myrelation"); |         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", () => { |     it("simple label AND", () => { | ||||||
| @@ -99,7 +116,10 @@ describe("Parser", () => { | |||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); |         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.constructor.name).toEqual("LabelComparisonExp"); | ||||||
|         expect(firstSub.attributeName).toEqual("first"); |         expect(firstSub.attributeName).toEqual("first"); | ||||||
| @@ -116,7 +136,10 @@ describe("Parser", () => { | |||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); |         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.constructor.name).toEqual("LabelComparisonExp"); | ||||||
|         expect(firstSub.attributeName).toEqual("first"); |         expect(firstSub.attributeName).toEqual("first"); | ||||||
| @@ -132,8 +155,11 @@ describe("Parser", () => { | |||||||
|             searchContext: new SearchContext() |             searchContext: new SearchContext() | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         expect(rootExp.constructor.name).toEqual("OrExp"); |         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||||
|         const [firstSub, secondSub] = rootExp.subExpressions; |         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.constructor.name).toEqual("LabelComparisonExp"); | ||||||
|         expect(firstSub.attributeName).toEqual("first"); |         expect(firstSub.attributeName).toEqual("first"); | ||||||
| @@ -155,8 +181,9 @@ describe("Parser", () => { | |||||||
|         expect(firstSub.constructor.name).toEqual("PropertyComparisonExp"); |         expect(firstSub.constructor.name).toEqual("PropertyComparisonExp"); | ||||||
|         expect(firstSub.propertyName).toEqual('isArchived'); |         expect(firstSub.propertyName).toEqual('isArchived'); | ||||||
|  |  | ||||||
|         expect(secondSub.constructor.name).toEqual("NoteCacheFlatTextExp"); |         expect(secondSub.constructor.name).toEqual("OrExp"); | ||||||
|         expect(secondSub.tokens).toEqual(["hello"]); |         expect(secondSub.subExpressions[0].constructor.name).toEqual("NoteCacheFlatTextExp"); | ||||||
|  |         expect(secondSub.subExpressions[0].tokens).toEqual(["hello"]); | ||||||
|  |  | ||||||
|         expect(thirdSub.constructor.name).toEqual("LabelComparisonExp"); |         expect(thirdSub.constructor.name).toEqual("LabelComparisonExp"); | ||||||
|         expect(thirdSub.attributeName).toEqual("mylabel"); |         expect(thirdSub.attributeName).toEqual("mylabel"); | ||||||
| @@ -169,8 +196,11 @@ describe("Parser", () => { | |||||||
|             searchContext: new SearchContext() |             searchContext: new SearchContext() | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         expect(rootExp.constructor.name).toEqual("OrExp"); |         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||||
|         const [firstSub, secondSub] = rootExp.subExpressions; |         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.constructor.name).toEqual("LabelComparisonExp"); | ||||||
|         expect(firstSub.attributeName).toEqual("first"); |         expect(firstSub.attributeName).toEqual("first"); | ||||||
| @@ -232,10 +262,12 @@ describe("Invalid expressions", () => { | |||||||
|             searchContext: new SearchContext() |             searchContext: new SearchContext() | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         expect(rootExp.constructor.name).toEqual("LabelComparisonExp"); |         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||||
|         expect(rootExp.attributeType).toEqual("label"); |         assertIsArchived(rootExp.subExpressions[0]); | ||||||
|         expect(rootExp.attributeName).toEqual("first"); |         expect(rootExp.subExpressions[1].constructor.name).toEqual("LabelComparisonExp"); | ||||||
|         expect(rootExp.comparator).toBeTruthy(); |         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", () => { |     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[0].noteId].title).toEqual("Austria"); | ||||||
|         expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy"); |         expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy"); | ||||||
|  |  | ||||||
|         searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 0', searchContext); |         searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1', searchContext); | ||||||
|         expect(searchResults.length).toEqual(0); |         expect(searchResults.length).toEqual(1); | ||||||
|  |  | ||||||
|         searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', searchContext); |         searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', searchContext); | ||||||
|         expect(searchResults.length).toEqual(4); |         expect(searchResults.length).toEqual(4); | ||||||
|   | |||||||
| @@ -1,6 +1,9 @@ | |||||||
| const {note} = require('./note_cache_mocking.js'); | const {note} = require('./note_cache_mocking.js'); | ||||||
| const ValueExtractor = require('../../src/services/search/value_extractor.js'); | const ValueExtractor = require('../../src/services/search/value_extractor.js'); | ||||||
| const noteCache = require('../../src/services/note_cache/note_cache.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", () => { | describe("Value extractor", () => { | ||||||
|     beforeEach(() => { |     beforeEach(() => { | ||||||
| @@ -10,7 +13,7 @@ describe("Value extractor", () => { | |||||||
|     it("simple title extraction", async () => { |     it("simple title extraction", async () => { | ||||||
|         const europe = note("Europe").note; |         const europe = note("Europe").note; | ||||||
|  |  | ||||||
|         const valueExtractor = new ValueExtractor(["note", "title"]); |         const valueExtractor = new ValueExtractor(dsc, ["note", "title"]); | ||||||
|  |  | ||||||
|         expect(valueExtractor.validate()).toBeFalsy(); |         expect(valueExtractor.validate()).toBeFalsy(); | ||||||
|         expect(valueExtractor.extract(europe)).toEqual("Europe"); |         expect(valueExtractor.extract(europe)).toEqual("Europe"); | ||||||
| @@ -21,12 +24,12 @@ describe("Value extractor", () => { | |||||||
|             .label("Capital", "Vienna") |             .label("Capital", "Vienna") | ||||||
|             .note; |             .note; | ||||||
|  |  | ||||||
|         let valueExtractor = new ValueExtractor(["note", "labels", "capital"]); |         let valueExtractor = new ValueExtractor(dsc, ["note", "labels", "capital"]); | ||||||
|  |  | ||||||
|         expect(valueExtractor.validate()).toBeFalsy(); |         expect(valueExtractor.validate()).toBeFalsy(); | ||||||
|         expect(valueExtractor.extract(austria)).toEqual("Vienna"); |         expect(valueExtractor.extract(austria)).toEqual("Vienna"); | ||||||
|  |  | ||||||
|         valueExtractor = new ValueExtractor(["#capital"]); |         valueExtractor = new ValueExtractor(dsc, ["#capital"]); | ||||||
|  |  | ||||||
|         expect(valueExtractor.validate()).toBeFalsy(); |         expect(valueExtractor.validate()).toBeFalsy(); | ||||||
|         expect(valueExtractor.extract(austria)).toEqual("Vienna"); |         expect(valueExtractor.extract(austria)).toEqual("Vienna"); | ||||||
| @@ -38,12 +41,12 @@ describe("Value extractor", () => { | |||||||
|             .child(note("Austria") |             .child(note("Austria") | ||||||
|                 .child(vienna)); |                 .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.validate()).toBeFalsy(); | ||||||
|         expect(valueExtractor.extract(europe.note)).toEqual("Vienna"); |         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.validate()).toBeFalsy(); | ||||||
|         expect(valueExtractor.extract(vienna.note)).toEqual("Europe"); |         expect(valueExtractor.extract(vienna.note)).toEqual("Europe"); | ||||||
| @@ -56,12 +59,12 @@ describe("Value extractor", () => { | |||||||
|                 .relation('neighbor', czechRepublic.note) |                 .relation('neighbor', czechRepublic.note) | ||||||
|                 .relation('neighbor', slovakia.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.validate()).toBeFalsy(); | ||||||
|         expect(valueExtractor.extract(austria.note)).toEqual("Prague"); |         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.validate()).toBeFalsy(); | ||||||
|         expect(valueExtractor.extract(austria.note)).toEqual("Prague"); |         expect(valueExtractor.extract(austria.note)).toEqual("Prague"); | ||||||
| @@ -70,17 +73,17 @@ describe("Value extractor", () => { | |||||||
|  |  | ||||||
| describe("Invalid value extractor property path", () => { | describe("Invalid value extractor property path", () => { | ||||||
|     it('each path must start with "note" (or label/relation)', |     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", |     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", |     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", |     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", |     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,8 +49,13 @@ class Note extends Entity { | |||||||
|             this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable(); |             this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable(); | ||||||
|  |  | ||||||
|             if (this.isContentAvailable) { |             if (this.isContentAvailable) { | ||||||
|  |                 try { | ||||||
|                     this.title = protectedSessionService.decryptString(this.title); |                     this.title = protectedSessionService.decryptString(this.title); | ||||||
|                 } |                 } | ||||||
|  |                 catch (e) { | ||||||
|  |                     throw new Error(`Could not decrypt title of note ${this.noteId}: ${e.message} ${e.stack}`) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|             else { |             else { | ||||||
|                 this.title = "[protected]"; |                 this.title = "[protected]"; | ||||||
|             } |             } | ||||||
| @@ -156,14 +161,14 @@ class Note extends Entity { | |||||||
|  |  | ||||||
|         sql.upsert("note_contents", "noteId", pojo); |         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({ |         entityChangesService.addEntityChange({ | ||||||
|             entityName: 'note_contents', |             entityName: 'note_contents', | ||||||
|             entityId: this.noteId, |             entityId: this.noteId, | ||||||
|             hash: hash, |             hash: hash, | ||||||
|             isErased: false, |             isErased: false, | ||||||
|             utcDateChanged: this.getUtcDateChanged() |             utcDateChanged: pojo.utcDateModified | ||||||
|         }, null); |         }, null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -365,7 +370,6 @@ class Note extends Entity { | |||||||
|                 WHERE attributes.isDeleted = 0 |                 WHERE attributes.isDeleted = 0 | ||||||
|                   AND attributes.type = 'relation' |                   AND attributes.type = 'relation' | ||||||
|                   AND attributes.name = 'template' |                   AND attributes.name = 'template' | ||||||
|                   AND (treeWithAttrs.level = 0 OR attributes.isInheritable = 1) |  | ||||||
|                 ) |                 ) | ||||||
|             SELECT attributes.* FROM attributes JOIN treeWithAttrs ON attributes.noteId = treeWithAttrs.noteId |             SELECT attributes.* FROM attributes JOIN treeWithAttrs ON attributes.noteId = treeWithAttrs.noteId | ||||||
|             WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR treeWithAttrs.level = 0) |             WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR treeWithAttrs.level = 0) | ||||||
|   | |||||||
| @@ -359,7 +359,7 @@ class NoteShort { | |||||||
|         const workspaceIconClass = this.getWorkspaceIconClass(); |         const workspaceIconClass = this.getWorkspaceIconClass(); | ||||||
|  |  | ||||||
|         if (iconClassLabels.length > 0) { |         if (iconClassLabels.length > 0) { | ||||||
|             return iconClassLabels.map(l => l.value).join(' '); |             return iconClassLabels[0].value; | ||||||
|         } |         } | ||||||
|         else if (workspaceIconClass) { |         else if (workspaceIconClass) { | ||||||
|             return workspaceIconClass; |             return workspaceIconClass; | ||||||
|   | |||||||
| @@ -83,6 +83,9 @@ export default class MobileLayout { | |||||||
|                     .child(new NoteTitleWidget()) |                     .child(new NoteTitleWidget()) | ||||||
|                     .child(new CloseDetailButtonWidget())) |                     .child(new CloseDetailButtonWidget())) | ||||||
|                 .child(new NoteDetailWidget() |                 .child(new NoteDetailWidget() | ||||||
|                     .css('padding', '5px 20px 10px 0'))); |                     .css('padding', '5px 20px 10px 0') | ||||||
|  |                     .css('overflow', 'auto') | ||||||
|  |                     .css('height', '100%') | ||||||
|  |                 )); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -79,6 +79,15 @@ async function renderAttributes(attributes, renderIsInheritable) { | |||||||
|     return $container; |     return $container; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | const HIDDEN_ATTRIBUTES = [ | ||||||
|  |     'originalFileName', | ||||||
|  |     'template', | ||||||
|  |     'cssClass', | ||||||
|  |     'iconClass', | ||||||
|  |     'pageSize', | ||||||
|  |     'viewType' | ||||||
|  | ]; | ||||||
|  |  | ||||||
| async function renderNormalAttributes(note) { | async function renderNormalAttributes(note) { | ||||||
|     const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes(); |     const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes(); | ||||||
|     let attrs = note.getAttributes(); |     let attrs = note.getAttributes(); | ||||||
| @@ -90,6 +99,7 @@ async function renderNormalAttributes(note) { | |||||||
|         attrs = attrs.filter( |         attrs = attrs.filter( | ||||||
|             attr => !attr.isDefinition() |             attr => !attr.isDefinition() | ||||||
|                  && !attr.isAutoLink |                  && !attr.isAutoLink | ||||||
|  |                  && !HIDDEN_ATTRIBUTES.includes(attr.name) | ||||||
|                  && attr.noteId === note.noteId |                  && attr.noteId === note.noteId | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -74,7 +74,11 @@ export default class Entrypoints extends Component { | |||||||
|  |  | ||||||
|         await ws.waitForMaxKnownEntityChangeId(); |         await ws.waitForMaxKnownEntityChangeId(); | ||||||
|  |  | ||||||
|         await appContext.tabManager.openTabWithNote(note.noteId, true); |         const hoistedNoteId = appContext.tabManager.getActiveTabContext() | ||||||
|  |             ? appContext.tabManager.getActiveTabContext().hoistedNoteId | ||||||
|  |             : 'root'; | ||||||
|  |  | ||||||
|  |         await appContext.tabManager.openTabWithNote(note.noteId, true, null, hoistedNoteId); | ||||||
|  |  | ||||||
|         appContext.triggerEvent('focusAndSelectTitle'); |         appContext.triggerEvent('focusAndSelectTitle'); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import utils from "./utils.js"; | import utils from "./utils.js"; | ||||||
| import options from './options.js'; | import options from './options.js'; | ||||||
|  | import server from "./server.js"; | ||||||
|  |  | ||||||
| const PROTECTED_SESSION_ID_KEY = 'protectedSessionId'; | const PROTECTED_SESSION_ID_KEY = 'protectedSessionId'; | ||||||
|  |  | ||||||
| @@ -23,11 +24,11 @@ function resetSessionCookie() { | |||||||
|     utils.setSessionCookie(PROTECTED_SESSION_ID_KEY, null); |     utils.setSessionCookie(PROTECTED_SESSION_ID_KEY, null); | ||||||
| } | } | ||||||
|  |  | ||||||
| function resetProtectedSession() { | async function resetProtectedSession() { | ||||||
|     resetSessionCookie(); |     resetSessionCookie(); | ||||||
|  |  | ||||||
|     // most secure solution - guarantees nothing remained in memory |     await server.post("logout/protected"); | ||||||
|     // since this expires because user doesn't use the app, it shouldn't be disruptive |  | ||||||
|     utils.reloadApp(); |     utils.reloadApp(); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -194,9 +194,18 @@ const ATTR_HELP = { | |||||||
|         "appTheme": "marks CSS notes which are full Trilium themes and are thus available in Trilium options.", |         "appTheme": "marks CSS notes which are full Trilium themes and are thus available in Trilium options.", | ||||||
|         "cssClass": "value of this label is then added as CSS class to the node representing given note in the tree. This can be useful for advanced theming. Can be used in template notes.", |         "cssClass": "value of this label is then added as CSS class to the node representing given note in the tree. This can be useful for advanced theming. Can be used in template notes.", | ||||||
|         "iconClass": "value of this label is added as a CSS class to the icon on the tree which can help visually distinguish the notes in the tree. Example might be bx bx-home - icons are taken from boxicons. Can be used in template notes.", |         "iconClass": "value of this label is added as a CSS class to the icon on the tree which can help visually distinguish the notes in the tree. Example might be bx bx-home - icons are taken from boxicons. Can be used in template notes.", | ||||||
|         "bookZoomLevel": 'applies only to book note and sets the "zoom level" (how many notes fit on 1 row)', |         "pageSize": "number of items per page in note listing", | ||||||
|         "customRequestHandler": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>', |         "customRequestHandler": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>', | ||||||
|         "customResourceProvider": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>' |         "customResourceProvider": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>', | ||||||
|  |         "widget": "marks this note as a custom widget which will be added to the Trilium component tree", | ||||||
|  |         "workspace": "marks this note as a workspace which allows easy hoisting", | ||||||
|  |         "workspaceIconClass": "defines box icon CSS class which will be used in tab when hoisted to this note", | ||||||
|  |         "workspaceTabBackgroundColor": "CSS color used in the note tab when hoisted to this note", | ||||||
|  |         "searchHome": "new search notes will be created as children of this note", | ||||||
|  |         "hoistedSearchHome": "new search notes will be created as children of this note when hoisted to some ancestor of this note", | ||||||
|  |         "inbox": "default inbox location for new notes", | ||||||
|  |         "hoistedInbox": "default inbox location for new notes when hoisted to some ancestor of this note", | ||||||
|  |         "sqlConsoleHome": "default location of SQL console notes", | ||||||
|     }, |     }, | ||||||
|     "relation": { |     "relation": { | ||||||
|         "runOnNoteCreation": "executes when note is created on backend", |         "runOnNoteCreation": "executes when note is created on backend", | ||||||
|   | |||||||
| @@ -278,7 +278,7 @@ export default class NoteDetailWidget extends TabAwareWidget { | |||||||
|  |  | ||||||
|             const label = attrs.find(attr => |             const label = attrs.find(attr => | ||||||
|                 attr.type === 'label' |                 attr.type === 'label' | ||||||
|                 && ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'bookZoomLevel', 'displayRelations'].includes(attr.name) |                 && ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'displayRelations'].includes(attr.name) | ||||||
|                 && attr.isAffecting(this.note)); |                 && attr.isAffecting(this.note)); | ||||||
|  |  | ||||||
|             const relation = attrs.find(attr => |             const relation = attrs.find(attr => | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ export default class NoteTitleWidget extends TabAwareWidget { | |||||||
|  |  | ||||||
|             protectedSessionHolder.touchProtectedSessionIfNecessary(this.note); |             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); |         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> |                       title="Images which are shown in the parent text note will not be displayed in the tree"></span> | ||||||
|             </label> |             </label> | ||||||
|         </div> |         </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/> |         <br/> | ||||||
|      |      | ||||||
| @@ -235,6 +244,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | |||||||
|         this.$treeSettingsPopup = this.$widget.find('.tree-settings-popup'); |         this.$treeSettingsPopup = this.$widget.find('.tree-settings-popup'); | ||||||
|         this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find('.hide-archived-notes'); |         this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find('.hide-archived-notes'); | ||||||
|         this.$hideIncludedImages = this.$treeSettingsPopup.find('.hide-included-images'); |         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 = this.$widget.find('.tree-settings-button'); | ||||||
|         this.$treeSettingsButton.on("click", e => { |         this.$treeSettingsButton.on("click", e => { | ||||||
| @@ -245,6 +255,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | |||||||
|  |  | ||||||
|             this.$hideArchivedNotesCheckbox.prop("checked", this.hideArchivedNotes); |             this.$hideArchivedNotesCheckbox.prop("checked", this.hideArchivedNotes); | ||||||
|             this.$hideIncludedImages.prop("checked", this.hideIncludedImages); |             this.$hideIncludedImages.prop("checked", this.hideIncludedImages); | ||||||
|  |             this.$autoCollapseNoteTree.prop("checked", this.autoCollapseNoteTree); | ||||||
|  |  | ||||||
|             let top = this.$treeSettingsButton[0].offsetTop; |             let top = this.$treeSettingsButton[0].offsetTop; | ||||||
|             let left = this.$treeSettingsButton[0].offsetLeft; |             let left = this.$treeSettingsButton[0].offsetLeft; | ||||||
| @@ -272,6 +283,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | |||||||
|         this.$saveTreeSettingsButton.on('click', async () => { |         this.$saveTreeSettingsButton.on('click', async () => { | ||||||
|             await this.setHideArchivedNotes(this.$hideArchivedNotesCheckbox.prop("checked")); |             await this.setHideArchivedNotes(this.$hideArchivedNotesCheckbox.prop("checked")); | ||||||
|             await this.setHideIncludedImages(this.$hideIncludedImages.prop("checked")); |             await this.setHideIncludedImages(this.$hideIncludedImages.prop("checked")); | ||||||
|  |             await this.setAutoCollapseNoteTree(this.$autoCollapseNoteTree.prop("checked")); | ||||||
|  |  | ||||||
|             this.$treeSettingsPopup.hide(); |             this.$treeSettingsPopup.hide(); | ||||||
|  |  | ||||||
| @@ -327,6 +339,14 @@ export default class NoteTreeWidget extends TabAwareWidget { | |||||||
|         await options.save("hideIncludedImages_" + this.treeName, val.toString()); |         await options.save("hideIncludedImages_" + this.treeName, val.toString()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     get autoCollapseNoteTree() { | ||||||
|  |         return options.is("autoCollapseNoteTree"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async setAutoCollapseNoteTree(val) { | ||||||
|  |         await options.save("autoCollapseNoteTree", val.toString()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     initFancyTree() { |     initFancyTree() { | ||||||
|         const treeData = [this.prepareRootNode()]; |         const treeData = [this.prepareRootNode()]; | ||||||
|  |  | ||||||
| @@ -362,8 +382,6 @@ export default class NoteTreeWidget extends TabAwareWidget { | |||||||
|                     } |                     } | ||||||
|                     else { |                     else { | ||||||
|                         node.setActive(); |                         node.setActive(); | ||||||
|  |  | ||||||
|                         this.clearSelectedNodes(); |  | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     return false; |                     return false; | ||||||
| @@ -373,6 +391,8 @@ export default class NoteTreeWidget extends TabAwareWidget { | |||||||
|                 // click event won't propagate so let's close context menu manually |                 // click event won't propagate so let's close context menu manually | ||||||
|                 contextMenu.hide(); |                 contextMenu.hide(); | ||||||
|  |  | ||||||
|  |                 this.clearSelectedNodes(); | ||||||
|  |  | ||||||
|                 const notePath = treeService.getNotePath(data.node); |                 const notePath = treeService.getNotePath(data.node); | ||||||
|  |  | ||||||
|                 const activeTabContext = appContext.tabManager.getActiveTabContext(); |                 const activeTabContext = appContext.tabManager.getActiveTabContext(); | ||||||
| @@ -797,10 +817,12 @@ export default class NoteTreeWidget extends TabAwareWidget { | |||||||
|  |  | ||||||
|             const node = await this.expandToNote(activeContext.notePath); |             const node = await this.expandToNote(activeContext.notePath); | ||||||
|  |  | ||||||
|  |             if (node) { | ||||||
|                 await node.makeVisible({scrollIntoView: true}); |                 await node.makeVisible({scrollIntoView: true}); | ||||||
|                 node.setActive(true, {noEvents: true, noFocus: false}); |                 node.setActive(true, {noEvents: true, noFocus: false}); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** @return {FancytreeNode} */ |     /** @return {FancytreeNode} */ | ||||||
|     async getNodeFromPath(notePath, expand = false, logErrors = true) { |     async getNodeFromPath(notePath, expand = false, logErrors = true) { | ||||||
| @@ -851,8 +873,12 @@ export default class NoteTreeWidget extends TabAwareWidget { | |||||||
|                             // these are real notes with real notePath, user can display them in a detail |                             // these are real notes with real notePath, user can display them in a detail | ||||||
|                             // but they don't have a node in the tree |                             // but they don't have a node in the tree | ||||||
|  |  | ||||||
|  |                             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}`); |                                 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; |                         return; | ||||||
|                     } |                     } | ||||||
| @@ -955,6 +981,10 @@ export default class NoteTreeWidget extends TabAwareWidget { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         this.autoCollapseTimeoutId = setTimeout(() => { |         this.autoCollapseTimeoutId = setTimeout(() => { | ||||||
|  |             if (!this.autoCollapseNoteTree) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             /* |             /* | ||||||
|              * We're collapsing notes after period of inactivity to "cleanup" the tree - users rarely |              * We're collapsing notes after period of inactivity to "cleanup" the tree - users rarely | ||||||
|              * collapse the notes and the tree becomes unusuably large. |              * collapse the notes and the tree becomes unusuably large. | ||||||
| @@ -1114,11 +1144,12 @@ export default class NoteTreeWidget extends TabAwareWidget { | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (node) { |             if (node) { | ||||||
|                 node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused}); |  | ||||||
|  |  | ||||||
|                 if (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 { |             else { | ||||||
|                 // this is used when original note has been deleted and we want to move the focus to the note above/below |                 // 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"> |     <td colspan="2"> | ||||||
|         <span class="bx bx-trash"></span> |         <span class="bx bx-trash"></span> | ||||||
|      |      | ||||||
|         Delete matched note |         Delete matched notes | ||||||
|     </td> |     </td> | ||||||
|     <td class="button-column"> |     <td class="button-column"> | ||||||
|         <span class="bx bx-x icon-action action-conf-del"></span> |         <span class="bx bx-x icon-action action-conf-del"></span> | ||||||
|   | |||||||
| @@ -14,6 +14,10 @@ const TPL = ` | |||||||
|         height: 35px; |         height: 35px; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     .standard-top-widget > div { | ||||||
|  |         flex-shrink: 0; /* fixes https://github.com/zadam/trilium/issues/1745 */ | ||||||
|  |     } | ||||||
|  |      | ||||||
|     .standard-top-widget button.noborder { |     .standard-top-widget button.noborder { | ||||||
|         padding: 1px 5px 1px 5px; |         padding: 1px 5px 1px 5px; | ||||||
|         font-size: 90%; |         font-size: 90%; | ||||||
|   | |||||||
| @@ -5,24 +5,47 @@ import linkService from "../../services/link.js"; | |||||||
| import noteContentRenderer from "../../services/note_content_renderer.js"; | import noteContentRenderer from "../../services/note_content_renderer.js"; | ||||||
|  |  | ||||||
| export default class AbstractTextTypeWidget extends TypeWidget { | export default class AbstractTextTypeWidget extends TypeWidget { | ||||||
|     doRender() { |     setupImageOpening(singleClickOpens) { | ||||||
|         this.$widget.on("dblclick", "img", e => { |         this.$widget.on("dblclick", "img", e => this.openImageInCurrentTab($(e.target))); | ||||||
|             const $img = $(e.target); |  | ||||||
|             const src = $img.prop("src"); |  | ||||||
|  |  | ||||||
|             const match = src.match(/\/api\/images\/([A-Za-z0-9]+)\//); |         this.$widget.on("click", "img", e => { | ||||||
|  |             if ((e.which === 1 && e.ctrlKey) || e.which === 2) { | ||||||
|             if (match) { |                 this.openImageInNewTab($(e.target)); | ||||||
|                 const noteId = match[1]; |  | ||||||
|  |  | ||||||
|                 appContext.tabManager.getActiveTabContext().setNote(noteId); |  | ||||||
|             } |             } | ||||||
|             else { |             else if (e.which === 1 && singleClickOpens) { | ||||||
|                 window.open(src, '_blank'); |                 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) { |     async loadIncludedNote(noteId, $el) { | ||||||
|         const note = await treeCache.getNote(noteId); |         const note = await treeCache.getNote(noteId); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -84,6 +84,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | |||||||
|  |  | ||||||
|         keyboardActionService.setupActionsForElement('text-detail', this.$widget, this); |         keyboardActionService.setupActionsForElement('text-detail', this.$widget, this); | ||||||
|  |  | ||||||
|  |         this.setupImageOpening(false); | ||||||
|  |  | ||||||
|         super.doRender(); |         super.doRender(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ const TPL = ` | |||||||
|     <style> |     <style> | ||||||
|     .note-detail-read-only-code { |     .note-detail-read-only-code { | ||||||
|         position: relative; |         position: relative; | ||||||
|  |         min-height: 50px; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     .note-detail-read-only-code-content { |     .note-detail-read-only-code-content { | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ const TPL = ` | |||||||
|         padding-top: 10px; |         padding-top: 10px; | ||||||
|         font-family: var(--detail-text-font-family); |         font-family: var(--detail-text-font-family); | ||||||
|         position: relative; |         position: relative; | ||||||
|  |         min-height: 50px; | ||||||
|     } |     } | ||||||
|          |          | ||||||
|     .note-detail-readonly-text p:first-child, .note-detail-readonly-text::before { |     .note-detail-readonly-text p:first-child, .note-detail-readonly-text::before { | ||||||
| @@ -33,6 +34,7 @@ const TPL = ` | |||||||
|      |      | ||||||
|     .note-detail-readonly-text img { |     .note-detail-readonly-text img { | ||||||
|         max-width: 100%; |         max-width: 100%; | ||||||
|  |         cursor: pointer; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     .edit-text-note-container { |     .edit-text-note-container { | ||||||
| @@ -65,6 +67,8 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget { | |||||||
|             this.triggerEvent('textPreviewDisabled', {tabContext: this.tabContext}); |             this.triggerEvent('textPreviewDisabled', {tabContext: this.tabContext}); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  |         this.setupImageOpening(true); | ||||||
|  |  | ||||||
|         super.doRender(); |         super.doRender(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -106,8 +106,12 @@ function processContent(images, note, content) { | |||||||
|         for (const {src, dataUrl, imageId} of images) { |         for (const {src, dataUrl, imageId} of images) { | ||||||
|             const filename = path.basename(src); |             const filename = path.basename(src); | ||||||
|  |  | ||||||
|             if (!dataUrl.startsWith("data:image")) { |             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; |                 continue; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,8 +9,27 @@ const cls = require('../../services/cls'); | |||||||
| const repository = require('../../services/repository'); | const repository = require('../../services/repository'); | ||||||
|  |  | ||||||
| function getInboxNote(req) { | function getInboxNote(req) { | ||||||
|     return attributeService.getNoteWithLabel('inbox') |     const hoistedNote = getHoistedNote(); | ||||||
|  |  | ||||||
|  |     let inbox; | ||||||
|  |  | ||||||
|  |     if (hoistedNote) { | ||||||
|  |         ([inbox] = hoistedNote.getDescendantNotesWithLabel('hoistedInbox')); | ||||||
|  |  | ||||||
|  |         if (!inbox) { | ||||||
|  |             ([inbox] = hoistedNote.getDescendantNotesWithLabel('inbox')); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!inbox) { | ||||||
|  |             inbox = hoistedNote; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         inbox = attributeService.getNoteWithLabel('inbox') | ||||||
|             || dateNoteService.getDateNote(req.params.date); |             || dateNoteService.getDateNote(req.params.date); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return inbox; | ||||||
| } | } | ||||||
|  |  | ||||||
| function getDateNote(req) { | function getDateNote(req) { | ||||||
| @@ -66,27 +85,18 @@ function createSearchNote(req) { | |||||||
|     const searchString = params.searchString || ""; |     const searchString = params.searchString || ""; | ||||||
|     let ancestorNoteId = params.ancestorNoteId; |     let ancestorNoteId = params.ancestorNoteId; | ||||||
|  |  | ||||||
|     const hoistedNote = cls.getHoistedNoteId() && cls.getHoistedNoteId() !== 'root' |     const hoistedNote = getHoistedNote(); | ||||||
|         ? repository.getNote(cls.getHoistedNoteId()) |  | ||||||
|         : null; |  | ||||||
|  |  | ||||||
|     let searchHome; |     let searchHome; | ||||||
|  |  | ||||||
|     if (hoistedNote) { |     if (hoistedNote) { | ||||||
|         ([searchHome] = hoistedNote.getDescendantNotesWithLabel('hoistedSearchHome')); |         ([searchHome] = hoistedNote.getDescendantNotesWithLabel('hoistedSearchHome')); | ||||||
|  |  | ||||||
|  |         if (!searchHome) { | ||||||
|  |             ([searchHome] = hoistedNote.getDescendantNotesWithLabel('searchHome')); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!searchHome) { |         if (!searchHome) { | ||||||
|         const today = dateUtils.localNowDate(); |  | ||||||
|  |  | ||||||
|         searchHome = attributeService.getNoteWithLabel('searchHome') |  | ||||||
|                   || dateNoteService.getDateNote(today); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (hoistedNote) { |  | ||||||
|  |  | ||||||
|         if (!hoistedNote.getDescendantNoteIds().includes(searchHome.noteId)) { |  | ||||||
|             // otherwise the note would be saved outside of the hoisted context which is weird |  | ||||||
|             searchHome = hoistedNote; |             searchHome = hoistedNote; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -94,6 +104,12 @@ function createSearchNote(req) { | |||||||
|             ancestorNoteId = hoistedNote.noteId; |             ancestorNoteId = hoistedNote.noteId; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |     else { | ||||||
|  |         const today = dateUtils.localNowDate(); | ||||||
|  |  | ||||||
|  |         searchHome = attributeService.getNoteWithLabel('searchHome') | ||||||
|  |                   || dateNoteService.getDateNote(today); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const {note} = noteService.createNewNote({ |     const {note} = noteService.createNewNote({ | ||||||
|         parentNoteId: searchHome.noteId, |         parentNoteId: searchHome.noteId, | ||||||
| @@ -112,6 +128,12 @@ function createSearchNote(req) { | |||||||
|     return note; |     return note; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function getHoistedNote() { | ||||||
|  |     return cls.getHoistedNoteId() && cls.getHoistedNoteId() !== 'root' | ||||||
|  |         ? repository.getNote(cls.getHoistedNoteId()) | ||||||
|  |         : null; | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     getInboxNote, |     getInboxNote, | ||||||
|     getDateNote, |     getDateNote, | ||||||
|   | |||||||
| @@ -78,6 +78,12 @@ function loginToProtectedSession(req) { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function logoutFromProtectedSession() { | ||||||
|  |     protectedSessionService.resetDataKey(); | ||||||
|  |  | ||||||
|  |     eventService.emit(eventService.LEAVE_PROTECTED_SESSION); | ||||||
|  | } | ||||||
|  |  | ||||||
| function token(req) { | function token(req) { | ||||||
|     const username = req.body.username; |     const username = req.body.username; | ||||||
|     const password = req.body.password; |     const password = req.body.password; | ||||||
| @@ -101,5 +107,6 @@ function token(req) { | |||||||
| module.exports = { | module.exports = { | ||||||
|     loginSync, |     loginSync, | ||||||
|     loginToProtectedSession, |     loginToProtectedSession, | ||||||
|  |     logoutFromProtectedSession, | ||||||
|     token |     token | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ const noteCacheService = require('../../services/note_cache/note_cache_service') | |||||||
| const protectedSessionService = require('../../services/protected_session'); | const protectedSessionService = require('../../services/protected_session'); | ||||||
| const noteRevisionService = require('../../services/note_revisions'); | const noteRevisionService = require('../../services/note_revisions'); | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
|  | const sql = require('../../services/sql'); | ||||||
| const path = require('path'); | const path = require('path'); | ||||||
|  |  | ||||||
| function getNoteRevisions(req) { | function getNoteRevisions(req) { | ||||||
|   | |||||||
| @@ -41,7 +41,8 @@ const ALLOWED_OPTIONS = new Set([ | |||||||
|     'attributeListExpanded', |     'attributeListExpanded', | ||||||
|     'promotedAttributesExpanded', |     'promotedAttributesExpanded', | ||||||
|     'similarNotesExpanded', |     'similarNotesExpanded', | ||||||
|     'headingStyle' |     'headingStyle', | ||||||
|  |     'autoCollapseNoteTree' | ||||||
| ]); | ]); | ||||||
|  |  | ||||||
| function getOptions() { | function getOptions() { | ||||||
|   | |||||||
| @@ -200,9 +200,7 @@ function queueSector(req) { | |||||||
|     const entityName = utils.sanitizeSqlIdentifier(req.params.entityName); |     const entityName = utils.sanitizeSqlIdentifier(req.params.entityName); | ||||||
|     const sector = utils.sanitizeSqlIdentifier(req.params.sector); |     const sector = utils.sanitizeSqlIdentifier(req.params.sector); | ||||||
|  |  | ||||||
|     const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; |     entityChangesService.addEntityChangesForSector(entityName, sector); | ||||||
|  |  | ||||||
|     entityChangesService.addEntityChangesForSector(entityName, entityPrimaryKey, sector); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   | |||||||
| @@ -270,6 +270,8 @@ function register(app) { | |||||||
|     route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); |     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) |     // 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/login/protected', loginApiRoute.loginToProtectedSession); | ||||||
|  |     apiRoute(POST, '/api/logout/protected', loginApiRoute.logoutFromProtectedSession); | ||||||
|  |  | ||||||
|     route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler); |     route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler); | ||||||
|  |  | ||||||
|     // in case of local electron, local calls are allowed unauthenticated, for server they need auth |     // in case of local electron, local calls are allowed unauthenticated, for server they need auth | ||||||
|   | |||||||
| @@ -27,7 +27,6 @@ const BUILTIN_ATTRIBUTES = [ | |||||||
|     { type: 'label', name: 'run', isDangerous: true }, |     { type: 'label', name: 'run', isDangerous: true }, | ||||||
|     { type: 'label', name: 'customRequestHandler', isDangerous: true }, |     { type: 'label', name: 'customRequestHandler', isDangerous: true }, | ||||||
|     { type: 'label', name: 'customResourceProvider', isDangerous: true }, |     { type: 'label', name: 'customResourceProvider', isDangerous: true }, | ||||||
|     { type: 'label', name: 'bookZoomLevel', isDangerous: false }, |  | ||||||
|     { type: 'label', name: 'widget', isDangerous: true }, |     { type: 'label', name: 'widget', isDangerous: true }, | ||||||
|     { type: 'label', name: 'noteInfoWidgetDisabled' }, |     { type: 'label', name: 'noteInfoWidgetDisabled' }, | ||||||
|     { type: 'label', name: 'linkMapWidgetDisabled' }, |     { type: 'label', name: 'linkMapWidgetDisabled' }, | ||||||
| @@ -38,9 +37,12 @@ const BUILTIN_ATTRIBUTES = [ | |||||||
|     { type: 'label', name: 'workspaceIconClass' }, |     { type: 'label', name: 'workspaceIconClass' }, | ||||||
|     { type: 'label', name: 'workspaceTabBackgroundColor' }, |     { type: 'label', name: 'workspaceTabBackgroundColor' }, | ||||||
|     { type: 'label', name: 'searchHome' }, |     { type: 'label', name: 'searchHome' }, | ||||||
|  |     { type: 'label', name: 'hoistedInbox' }, | ||||||
|     { type: 'label', name: 'hoistedSearchHome' }, |     { type: 'label', name: 'hoistedSearchHome' }, | ||||||
|     { type: 'label', name: 'sqlConsoleHome' }, |     { type: 'label', name: 'sqlConsoleHome' }, | ||||||
|     { type: 'label', name: 'datePattern' }, |     { type: 'label', name: 'datePattern' }, | ||||||
|  |     { type: 'label', name: 'pageSize' }, | ||||||
|  |     { type: 'label', name: 'viewType' }, | ||||||
|  |  | ||||||
|     // relation names |     // relation names | ||||||
|     { type: 'relation', name: 'runOnNoteCreation', isDangerous: true }, |     { type: 'relation', name: 'runOnNoteCreation', isDangerous: true }, | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| module.exports = { buildDate:"2021-03-10T23:35:12+01:00", buildRevision: "6f901e6852c33ba0dae6c70efb9f65e5b0028995" }; | module.exports = { buildDate:"2021-04-22T20:50:22+02:00", buildRevision: "c27f573eed8905fc1d958adf5bdee0efc3aff593" }; | ||||||
|   | |||||||
| @@ -44,10 +44,14 @@ function isEntityEventsDisabled() { | |||||||
|     return !!namespace.get('disableEntityEvents'); |     return !!namespace.get('disableEntityEvents'); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function clearEntityChanges() { | ||||||
|  |     namespace.set('entityChanges', []); | ||||||
|  | } | ||||||
|  |  | ||||||
| function getAndClearEntityChanges() { | function getAndClearEntityChanges() { | ||||||
|     const entityChanges = namespace.get('entityChanges') || []; |     const entityChanges = namespace.get('entityChanges') || []; | ||||||
|  |  | ||||||
|     namespace.set('entityChanges', []); |     clearEntityChanges(); | ||||||
|  |  | ||||||
|     return entityChanges; |     return entityChanges; | ||||||
| } | } | ||||||
| @@ -92,6 +96,7 @@ module.exports = { | |||||||
|     disableEntityEvents, |     disableEntityEvents, | ||||||
|     isEntityEventsDisabled, |     isEntityEventsDisabled, | ||||||
|     reset, |     reset, | ||||||
|  |     clearEntityChanges, | ||||||
|     getAndClearEntityChanges, |     getAndClearEntityChanges, | ||||||
|     addEntityChange, |     addEntityChange, | ||||||
|     getEntityFromCache, |     getEntityFromCache, | ||||||
|   | |||||||
| @@ -53,22 +53,14 @@ function moveEntityChangeToTop(entityName, entityId) { | |||||||
|     addEntityChange(entityName, entityId, hash, null, isSynced); |     addEntityChange(entityName, entityId, hash, null, isSynced); | ||||||
| } | } | ||||||
|  |  | ||||||
| function addEntityChangesForSector(entityName, entityPrimaryKey, sector) { | function addEntityChangesForSector(entityName, sector) { | ||||||
|     const startTime = Date.now(); |     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(() => { |     sql.transactional(() => { | ||||||
|         const entityIds = sql.getColumn(`SELECT ${entityPrimaryKey} FROM ${entityName} WHERE SUBSTR(${entityPrimaryKey}, 1, 1) = ?`, [sector]); |         for (const ec of entityChanges) { | ||||||
|  |             insertEntityChange(entityName, ec.entityId, ec.hash, ec.isErased, ec.utcDateChanged, ec.sourceId, ec.isSynced); | ||||||
|         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); |  | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ const log = require('./log'); | |||||||
|  |  | ||||||
| const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED"; | const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED"; | ||||||
| const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION"; | const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION"; | ||||||
|  | const LEAVE_PROTECTED_SESSION = "LEAVE_PROTECTED_SESSION"; | ||||||
| const ENTITY_CREATED = "ENTITY_CREATED"; | const ENTITY_CREATED = "ENTITY_CREATED"; | ||||||
| const ENTITY_CHANGED = "ENTITY_CHANGED"; | const ENTITY_CHANGED = "ENTITY_CHANGED"; | ||||||
| const ENTITY_DELETED = "ENTITY_DELETED"; | const ENTITY_DELETED = "ENTITY_DELETED"; | ||||||
| @@ -47,6 +48,7 @@ module.exports = { | |||||||
|     // event types: |     // event types: | ||||||
|     NOTE_TITLE_CHANGED, |     NOTE_TITLE_CHANGED, | ||||||
|     ENTER_PROTECTED_SESSION, |     ENTER_PROTECTED_SESSION, | ||||||
|  |     LEAVE_PROTECTED_SESSION, | ||||||
|     ENTITY_CREATED, |     ENTITY_CREATED, | ||||||
|     ENTITY_CHANGED, |     ENTITY_CHANGED, | ||||||
|     ENTITY_DELETED, |     ENTITY_DELETED, | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const protectedSessionService = require('../../protected_session'); | const protectedSessionService = require('../../protected_session'); | ||||||
|  | const log = require('../../log'); | ||||||
|  |  | ||||||
| class Note { | class Note { | ||||||
|     constructor(noteCache, row) { |     constructor(noteCache, row) { | ||||||
| @@ -405,7 +406,7 @@ class Note { | |||||||
|             return 0; |             return 0; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         let minDistance = 999_999; |         let minDistance = 999999; | ||||||
|  |  | ||||||
|         for (const parent of this.parents) { |         for (const parent of this.parents) { | ||||||
|             minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1); |             minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1); | ||||||
| @@ -416,10 +417,15 @@ class Note { | |||||||
|  |  | ||||||
|     decrypt() { |     decrypt() { | ||||||
|         if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) { |         if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) { | ||||||
|  |             try { | ||||||
|                 this.title = protectedSessionService.decryptString(this.title); |                 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}`); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // for logging etc |     // for logging etc | ||||||
|   | |||||||
| @@ -177,6 +177,10 @@ eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => { | |||||||
|     } |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | eventService.subscribe(eventService.LEAVE_PROTECTED_SESSION, () => { | ||||||
|  |     load(); | ||||||
|  | }); | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     load |     load | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -23,7 +23,6 @@ const IGNORED_ATTR_NAMES = [ | |||||||
|     "archived", |     "archived", | ||||||
|     "hidepromotedattributes", |     "hidepromotedattributes", | ||||||
|     "keyboardshortcut", |     "keyboardshortcut", | ||||||
|     "bookzoomlevel", |  | ||||||
|     "noteinfowidgetdisabled", |     "noteinfowidgetdisabled", | ||||||
|     "linkmapwidgetdisabled", |     "linkmapwidgetdisabled", | ||||||
|     "noterevisionswidgetdisabled", |     "noterevisionswidgetdisabled", | ||||||
| @@ -234,7 +233,7 @@ async function findSimilarNotes(noteId) { | |||||||
|  |  | ||||||
|     const baseNote = noteCache.notes[noteId]; |     const baseNote = noteCache.notes[noteId]; | ||||||
|  |  | ||||||
|     if (!baseNote) { |     if (!baseNote || !baseNote.utcDateCreated) { | ||||||
|         return []; |         return []; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -86,6 +86,7 @@ const defaultOptions = [ | |||||||
|     { name: 'similarNotesExpanded', value: 'true', isSynced: true }, |     { name: 'similarNotesExpanded', value: 'true', isSynced: true }, | ||||||
|     { name: 'debugModeEnabled', value: 'false', isSynced: false }, |     { name: 'debugModeEnabled', value: 'false', isSynced: false }, | ||||||
|     { name: 'headingStyle', value: 'markdown', isSynced: true }, |     { name: 'headingStyle', value: 'markdown', isSynced: true }, | ||||||
|  |     { name: 'autoCollapseNoteTree', value: 'true', isSynced: true }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| function initStartupOptions() { | function initStartupOptions() { | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ const log = require('./log'); | |||||||
| const dataEncryptionService = require('./data_encryption'); | const dataEncryptionService = require('./data_encryption'); | ||||||
| const cls = require('./cls'); | const cls = require('./cls'); | ||||||
|  |  | ||||||
| const dataKeyMap = {}; | let dataKeyMap = {}; | ||||||
|  |  | ||||||
| function setDataKey(decryptedDataKey) { | function setDataKey(decryptedDataKey) { | ||||||
|     const protectedSessionId = utils.randomSecureToken(32); |     const protectedSessionId = utils.randomSecureToken(32); | ||||||
| @@ -29,6 +29,10 @@ function getDataKey() { | |||||||
|     return dataKeyMap[protectedSessionId]; |     return dataKeyMap[protectedSessionId]; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function resetDataKey() { | ||||||
|  |     dataKeyMap = {}; | ||||||
|  | } | ||||||
|  |  | ||||||
| function isProtectedSessionAvailable() { | function isProtectedSessionAvailable() { | ||||||
|     const protectedSessionId = getProtectedSessionId(); |     const protectedSessionId = getProtectedSessionId(); | ||||||
|  |  | ||||||
| @@ -71,6 +75,7 @@ function decryptString(cipherText) { | |||||||
| module.exports = { | module.exports = { | ||||||
|     setDataKey, |     setDataKey, | ||||||
|     getDataKey, |     getDataKey, | ||||||
|  |     resetDataKey, | ||||||
|     isProtectedSessionAvailable, |     isProtectedSessionAvailable, | ||||||
|     encrypt, |     encrypt, | ||||||
|     decrypt, |     decrypt, | ||||||
|   | |||||||
| @@ -84,7 +84,15 @@ function exec(opts) { | |||||||
|                 }); |                 }); | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             request.end(opts.body); |             let payload; | ||||||
|  |  | ||||||
|  |             if (opts.body) { | ||||||
|  |                 payload = typeof opts.body === 'object' | ||||||
|  |                     ? JSON.stringify(opts.body) | ||||||
|  |                     : opts.body; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             request.end(payload); | ||||||
|         } |         } | ||||||
|         catch (e) { |         catch (e) { | ||||||
|             reject(generateError(opts, e.message)); |             reject(generateError(opts, e.message)); | ||||||
|   | |||||||
| @@ -21,8 +21,8 @@ function lex(str) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function finishWord(endIndex) { |     function finishWord(endIndex, createAlsoForEmptyWords = false) { | ||||||
|         if (currentWord === '') { |         if (currentWord === '' && !createAlsoForEmptyWords) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -71,7 +71,7 @@ function lex(str) { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             else if (quotes === chr) { |             else if (quotes === chr) { | ||||||
|                 finishWord(i - 1); |                 finishWord(i - 1, true); | ||||||
|  |  | ||||||
|                 quotes = false; |                 quotes = false; | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| const log = require('./log'); | const log = require('./log'); | ||||||
| const Database = require('better-sqlite3'); | const Database = require('better-sqlite3'); | ||||||
| const dataDir = require('./data_dir'); | const dataDir = require('./data_dir'); | ||||||
|  | const cls = require('./cls'); | ||||||
|  |  | ||||||
| const dbConnection = new Database(dataDir.DOCUMENT_PATH); | const dbConnection = new Database(dataDir.DOCUMENT_PATH); | ||||||
| dbConnection.pragma('journal_mode = WAL'); | dbConnection.pragma('journal_mode = WAL'); | ||||||
| @@ -229,6 +230,7 @@ function wrap(query, func) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function transactional(func) { | function transactional(func) { | ||||||
|  |     try { | ||||||
|         const ret = dbConnection.transaction(func).deferred(); |         const ret = dbConnection.transaction(func).deferred(); | ||||||
|  |  | ||||||
|         if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released) |         if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released) | ||||||
| @@ -236,6 +238,12 @@ function transactional(func) { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         return ret; |         return ret; | ||||||
|  |     } | ||||||
|  |     catch (e) { | ||||||
|  |         cls.clearEntityChanges(); | ||||||
|  |  | ||||||
|  |         throw e; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| function fillNoteIdList(noteIds, truncate = true) { | function fillNoteIdList(noteIds, truncate = true) { | ||||||
|   | |||||||
| @@ -245,9 +245,7 @@ async function checkContentHash(syncContext) { | |||||||
|     const failedChecks = contentHashService.checkContentHashes(resp.entityHashes); |     const failedChecks = contentHashService.checkContentHashes(resp.entityHashes); | ||||||
|  |  | ||||||
|     for (const {entityName, sector} of failedChecks) { |     for (const {entityName, sector} of failedChecks) { | ||||||
|         const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; |         entityChangesService.addEntityChangesForSector(entityName, sector); | ||||||
|  |  | ||||||
|         entityChangesService.addEntityChangesForSector(entityName, entityPrimaryKey, sector); |  | ||||||
|  |  | ||||||
|         await syncRequest(syncContext, 'POST', `/api/sync/queue-sector/${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) |         ? updateNoteReordering(entityChange, entity, sourceId) | ||||||
|         : updateNormalEntity(entityChange, entity, sourceId); |         : updateNormalEntity(entityChange, entity, sourceId); | ||||||
|  |  | ||||||
|     // currently making exception for protected notes and note revisions because here |     if (updated && !entityChange.isErased) { | ||||||
|     // the title and content are not available decrypted as listeners would expect |  | ||||||
|     if (updated && !entity.isProtected && !entityChange.isErased) { |  | ||||||
|         eventService.emit(eventService.ENTITY_SYNCED, { |         eventService.emit(eventService.ENTITY_SYNCED, { | ||||||
|             entityName: entityChange.entityName, |             entityName: entityChange.entityName, | ||||||
|             entity |             entity | ||||||
| @@ -44,7 +42,7 @@ function updateNormalEntity(remoteEntityChange, entity, sourceId) { | |||||||
|  |  | ||||||
|     if (localEntityChange && !localEntityChange.isErased && remoteEntityChange.isErased) { |     if (localEntityChange && !localEntityChange.isErased && remoteEntityChange.isErased) { | ||||||
|         sql.transactional(() => { |         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); |             sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
|  | const log = require('./log'); | ||||||
| const repository = require('./repository'); | const repository = require('./repository'); | ||||||
| const Branch = require('../entities/branch'); | const Branch = require('../entities/branch'); | ||||||
| const entityChangesService = require('./entity_changes.js'); | 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 = ?", |             sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", | ||||||
|                 [position, note.branchId]); |                 [position, note.branchId]); | ||||||
|  |  | ||||||
|  |             if (note.branchId in noteCache.branches) { | ||||||
|                 noteCache.branches[note.branchId].notePosition = position; |                 noteCache.branches[note.branchId].notePosition = position; | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 log.info(`Branch "${note.branchId}" was not found in note cache.`); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             position += 10; |             position += 10; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -106,8 +106,6 @@ function sendPing(client, entityChanges = []) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const stats = require('./sync').stats; |  | ||||||
|  |  | ||||||
|     sendMessage(client, { |     sendMessage(client, { | ||||||
|         type: 'sync', |         type: 'sync', | ||||||
|         data: entityChanges |         data: entityChanges | ||||||
| @@ -118,9 +116,7 @@ function sendTransactionSyncsToAllClients() { | |||||||
|     if (webSocketServer) { |     if (webSocketServer) { | ||||||
|         const entityChanges = cls.getAndClearEntityChanges(); |         const entityChanges = cls.getAndClearEntityChanges(); | ||||||
|  |  | ||||||
|         webSocketServer.clients.forEach(function each(client) { |         webSocketServer.clients.forEach(client => sendPing(client, entityChanges)); | ||||||
|            sendPing(client, entityChanges); |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,6 +7,9 @@ | |||||||
|       <sourceFolder url="file://$MODULE_DIR$/src/public" isTestSource="false" /> |       <sourceFolder url="file://$MODULE_DIR$/src/public" isTestSource="false" /> | ||||||
|       <excludeFolder url="file://$MODULE_DIR$/dist" /> |       <excludeFolder url="file://$MODULE_DIR$/dist" /> | ||||||
|       <excludeFolder url="file://$MODULE_DIR$/src/public/app-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> |     </content> | ||||||
|     <orderEntry type="inheritedJdk" /> |     <orderEntry type="inheritedJdk" /> | ||||||
|     <orderEntry type="sourceFolder" forTests="false" /> |     <orderEntry type="sourceFolder" forTests="false" /> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user