mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 19:49:01 +01:00 
			
		
		
		
	Merge branch 'next53'
# Conflicts: # src/services/builtin_attributes.js
This commit is contained in:
		
						commit
						8e23c15763
					
				
							
								
								
									
										2
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							| @ -3,7 +3,7 @@ | ||||
|   <component name="JavaScriptSettings"> | ||||
|     <option name="languageLevel" value="ES6" /> | ||||
|   </component> | ||||
|   <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK"> | ||||
|   <component name="ProjectRootManager" version="2" languageLevel="JDK_18" default="true" project-jdk-name="openjdk-18" project-jdk-type="JavaSDK"> | ||||
|     <output url="file://$PROJECT_DIR$/out" /> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										2
									
								
								db/migrations/0196__rename_bulk_actions.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								db/migrations/0196__rename_bulk_actions.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| UPDATE attributes SET value = replace(value, 'setLabelValue', 'updateLabelValue') WHERE name = 'action' AND type = 'label'; | ||||
| UPDATE attributes SET value = replace(value, 'setRelationTarget', 'updateRelationTarget') WHERE name = 'action' AND type = 'label'; | ||||
							
								
								
									
										197
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										197
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,12 +1,12 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "version": "0.52.1-beta", | ||||
|   "version": "0.52.0-beta", | ||||
|   "lockfileVersion": 2, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "trilium", | ||||
|       "version": "0.52.1-beta", | ||||
|       "version": "0.52.0-beta", | ||||
|       "hasInstallScript": true, | ||||
|       "license": "AGPL-3.0-only", | ||||
|       "dependencies": { | ||||
| @ -21,7 +21,7 @@ | ||||
|         "commonmark": "0.30.0", | ||||
|         "cookie-parser": "1.4.6", | ||||
|         "csurf": "1.11.0", | ||||
|         "dayjs": "1.11.3", | ||||
|         "dayjs": "1.11.2", | ||||
|         "ejs": "3.1.8", | ||||
|         "electron-debug": "3.2.0", | ||||
|         "electron-dl": "3.3.1", | ||||
| @ -44,8 +44,8 @@ | ||||
|         "joplin-turndown-plugin-gfm": "1.0.12", | ||||
|         "jsdom": "19.0.0", | ||||
|         "mime-types": "2.1.35", | ||||
|         "multer": "1.4.4", | ||||
|         "node-abi": "3.21.0", | ||||
|         "multer": "1.4.5-lts.1", | ||||
|         "node-abi": "3.22.0", | ||||
|         "normalize-strings": "1.1.1", | ||||
|         "open": "8.4.0", | ||||
|         "portscanner": "2.2.0", | ||||
| @ -2161,38 +2161,16 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/busboy": { | ||||
|       "version": "0.2.14", | ||||
|       "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", | ||||
|       "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", | ||||
|       "version": "1.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", | ||||
|       "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", | ||||
|       "dependencies": { | ||||
|         "dicer": "0.2.5", | ||||
|         "readable-stream": "1.1.x" | ||||
|         "streamsearch": "^1.1.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.8.0" | ||||
|         "node": ">=10.16.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/busboy/node_modules/isarray": { | ||||
|       "version": "0.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", | ||||
|       "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" | ||||
|     }, | ||||
|     "node_modules/busboy/node_modules/readable-stream": { | ||||
|       "version": "1.1.14", | ||||
|       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", | ||||
|       "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", | ||||
|       "dependencies": { | ||||
|         "core-util-is": "~1.0.0", | ||||
|         "inherits": "~2.0.1", | ||||
|         "isarray": "0.0.1", | ||||
|         "string_decoder": "~0.10.x" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/busboy/node_modules/string_decoder": { | ||||
|       "version": "0.10.31", | ||||
|       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", | ||||
|       "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" | ||||
|     }, | ||||
|     "node_modules/bytes": { | ||||
|       "version": "3.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", | ||||
| @ -3115,9 +3093,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/dayjs": { | ||||
|       "version": "1.11.3", | ||||
|       "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz", | ||||
|       "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==" | ||||
|       "version": "1.11.2", | ||||
|       "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", | ||||
|       "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" | ||||
|     }, | ||||
|     "node_modules/debug": { | ||||
|       "version": "4.3.4", | ||||
| @ -3267,39 +3245,6 @@ | ||||
|       "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "node_modules/dicer": { | ||||
|       "version": "0.2.5", | ||||
|       "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", | ||||
|       "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", | ||||
|       "dependencies": { | ||||
|         "readable-stream": "1.1.x", | ||||
|         "streamsearch": "0.1.2" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.8.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/dicer/node_modules/isarray": { | ||||
|       "version": "0.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", | ||||
|       "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" | ||||
|     }, | ||||
|     "node_modules/dicer/node_modules/readable-stream": { | ||||
|       "version": "1.1.14", | ||||
|       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", | ||||
|       "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", | ||||
|       "dependencies": { | ||||
|         "core-util-is": "~1.0.0", | ||||
|         "inherits": "~2.0.1", | ||||
|         "isarray": "0.0.1", | ||||
|         "string_decoder": "~0.10.x" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/dicer/node_modules/string_decoder": { | ||||
|       "version": "0.10.31", | ||||
|       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", | ||||
|       "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" | ||||
|     }, | ||||
|     "node_modules/dir-compare": { | ||||
|       "version": "2.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", | ||||
| @ -7359,21 +7304,20 @@ | ||||
|       "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" | ||||
|     }, | ||||
|     "node_modules/multer": { | ||||
|       "version": "1.4.4", | ||||
|       "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", | ||||
|       "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", | ||||
|       "version": "1.4.5-lts.1", | ||||
|       "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", | ||||
|       "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", | ||||
|       "dependencies": { | ||||
|         "append-field": "^1.0.0", | ||||
|         "busboy": "^0.2.11", | ||||
|         "busboy": "^1.0.0", | ||||
|         "concat-stream": "^1.5.2", | ||||
|         "mkdirp": "^0.5.4", | ||||
|         "object-assign": "^4.1.1", | ||||
|         "on-finished": "^2.3.0", | ||||
|         "type-is": "^1.6.4", | ||||
|         "xtend": "^4.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 0.10.0" | ||||
|         "node": ">= 6.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/nanoid": { | ||||
| @ -7407,9 +7351,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/node-abi": { | ||||
|       "version": "3.21.0", | ||||
|       "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.21.0.tgz", | ||||
|       "integrity": "sha512-0ChvtQmmNYzXju0fjG0Vfg72q2D8FxUhluvV9uqivtXsKblSekJE2juxfg+9HoSgqPMqCmVEC/GHHtGzi4xYTg==", | ||||
|       "version": "3.22.0", | ||||
|       "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.22.0.tgz", | ||||
|       "integrity": "sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==", | ||||
|       "dependencies": { | ||||
|         "semver": "^7.3.5" | ||||
|       }, | ||||
| @ -9463,11 +9407,11 @@ | ||||
|       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" | ||||
|     }, | ||||
|     "node_modules/streamsearch": { | ||||
|       "version": "0.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", | ||||
|       "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", | ||||
|       "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", | ||||
|       "engines": { | ||||
|         "node": ">=0.8.0" | ||||
|         "node": ">=10.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/string_decoder": { | ||||
| @ -12550,35 +12494,11 @@ | ||||
|       } | ||||
|     }, | ||||
|     "busboy": { | ||||
|       "version": "0.2.14", | ||||
|       "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", | ||||
|       "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", | ||||
|       "version": "1.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", | ||||
|       "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", | ||||
|       "requires": { | ||||
|         "dicer": "0.2.5", | ||||
|         "readable-stream": "1.1.x" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "isarray": { | ||||
|           "version": "0.0.1", | ||||
|           "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", | ||||
|           "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" | ||||
|         }, | ||||
|         "readable-stream": { | ||||
|           "version": "1.1.14", | ||||
|           "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", | ||||
|           "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", | ||||
|           "requires": { | ||||
|             "core-util-is": "~1.0.0", | ||||
|             "inherits": "~2.0.1", | ||||
|             "isarray": "0.0.1", | ||||
|             "string_decoder": "~0.10.x" | ||||
|           } | ||||
|         }, | ||||
|         "string_decoder": { | ||||
|           "version": "0.10.31", | ||||
|           "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", | ||||
|           "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" | ||||
|         } | ||||
|         "streamsearch": "^1.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "bytes": { | ||||
| @ -13271,9 +13191,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "dayjs": { | ||||
|       "version": "1.11.3", | ||||
|       "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz", | ||||
|       "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==" | ||||
|       "version": "1.11.2", | ||||
|       "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", | ||||
|       "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" | ||||
|     }, | ||||
|     "debug": { | ||||
|       "version": "4.3.4", | ||||
| @ -13383,38 +13303,6 @@ | ||||
|       "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "dicer": { | ||||
|       "version": "0.2.5", | ||||
|       "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", | ||||
|       "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", | ||||
|       "requires": { | ||||
|         "readable-stream": "1.1.x", | ||||
|         "streamsearch": "0.1.2" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "isarray": { | ||||
|           "version": "0.0.1", | ||||
|           "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", | ||||
|           "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" | ||||
|         }, | ||||
|         "readable-stream": { | ||||
|           "version": "1.1.14", | ||||
|           "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", | ||||
|           "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", | ||||
|           "requires": { | ||||
|             "core-util-is": "~1.0.0", | ||||
|             "inherits": "~2.0.1", | ||||
|             "isarray": "0.0.1", | ||||
|             "string_decoder": "~0.10.x" | ||||
|           } | ||||
|         }, | ||||
|         "string_decoder": { | ||||
|           "version": "0.10.31", | ||||
|           "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", | ||||
|           "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "dir-compare": { | ||||
|       "version": "2.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", | ||||
| @ -16530,16 +16418,15 @@ | ||||
|       "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" | ||||
|     }, | ||||
|     "multer": { | ||||
|       "version": "1.4.4", | ||||
|       "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", | ||||
|       "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", | ||||
|       "version": "1.4.5-lts.1", | ||||
|       "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", | ||||
|       "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", | ||||
|       "requires": { | ||||
|         "append-field": "^1.0.0", | ||||
|         "busboy": "^0.2.11", | ||||
|         "busboy": "^1.0.0", | ||||
|         "concat-stream": "^1.5.2", | ||||
|         "mkdirp": "^0.5.4", | ||||
|         "object-assign": "^4.1.1", | ||||
|         "on-finished": "^2.3.0", | ||||
|         "type-is": "^1.6.4", | ||||
|         "xtend": "^4.0.0" | ||||
|       } | ||||
| @ -16566,9 +16453,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node-abi": { | ||||
|       "version": "3.21.0", | ||||
|       "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.21.0.tgz", | ||||
|       "integrity": "sha512-0ChvtQmmNYzXju0fjG0Vfg72q2D8FxUhluvV9uqivtXsKblSekJE2juxfg+9HoSgqPMqCmVEC/GHHtGzi4xYTg==", | ||||
|       "version": "3.22.0", | ||||
|       "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.22.0.tgz", | ||||
|       "integrity": "sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==", | ||||
|       "requires": { | ||||
|         "semver": "^7.3.5" | ||||
|       } | ||||
| @ -18169,9 +18056,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "streamsearch": { | ||||
|       "version": "0.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", | ||||
|       "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", | ||||
|       "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" | ||||
|     }, | ||||
|     "string_decoder": { | ||||
|       "version": "1.1.1", | ||||
|  | ||||
| @ -59,8 +59,8 @@ | ||||
|     "joplin-turndown-plugin-gfm": "1.0.12", | ||||
|     "jsdom": "19.0.0", | ||||
|     "mime-types": "2.1.35", | ||||
|     "multer": "1.4.4", | ||||
|     "node-abi": "3.21.0", | ||||
|     "multer": "1.4.5-lts.1", | ||||
|     "node-abi": "3.22.0", | ||||
|     "normalize-strings": "1.1.1", | ||||
|     "open": "8.4.0", | ||||
|     "portscanner": "2.2.0", | ||||
|  | ||||
| @ -52,13 +52,13 @@ if (utils.isElectron()) { | ||||
|                     title: suggestion, | ||||
|                     command: "replaceMisspelling", | ||||
|                     spellingSuggestion: suggestion, | ||||
|                     uiIcon: "empty" | ||||
|                     uiIcon: "bx bx-empty" | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             items.push({ | ||||
|                 title: `Add "${params.misspelledWord}" to dictionary`, | ||||
|                 uiIcon: "plus", | ||||
|                 uiIcon: "bx bx-plus", | ||||
|                 handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) | ||||
|             }); | ||||
| 
 | ||||
| @ -69,7 +69,7 @@ if (utils.isElectron()) { | ||||
|             items.push({ | ||||
|                 enabled: editFlags.canCut && hasText, | ||||
|                 title: `Cut <kbd>${platformModifier}+X`, | ||||
|                 uiIcon: "cut", | ||||
|                 uiIcon: "bx bx-cut", | ||||
|                 handler: () => webContents.cut() | ||||
|             }); | ||||
|         } | ||||
| @ -78,7 +78,7 @@ if (utils.isElectron()) { | ||||
|             items.push({ | ||||
|                 enabled: editFlags.canCopy && hasText, | ||||
|                 title: `Copy <kbd>${platformModifier}+C`, | ||||
|                 uiIcon: "copy", | ||||
|                 uiIcon: "bx bx-copy", | ||||
|                 handler: () => webContents.copy() | ||||
|             }); | ||||
|         } | ||||
| @ -86,7 +86,7 @@ if (utils.isElectron()) { | ||||
|         if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') { | ||||
|             items.push({ | ||||
|                 title: `Copy link`, | ||||
|                 uiIcon: "copy", | ||||
|                 uiIcon: "bx bx-copy", | ||||
|                 handler: () => { | ||||
|                     electron.clipboard.write({ | ||||
|                         bookmark: params.linkText, | ||||
| @ -100,7 +100,7 @@ if (utils.isElectron()) { | ||||
|             items.push({ | ||||
|                 enabled: editFlags.canPaste, | ||||
|                 title: `Paste <kbd>${platformModifier}+V`, | ||||
|                 uiIcon: "paste", | ||||
|                 uiIcon: "bx bx-paste", | ||||
|                 handler: () => webContents.paste() | ||||
|             }); | ||||
|         } | ||||
| @ -109,7 +109,7 @@ if (utils.isElectron()) { | ||||
|             items.push({ | ||||
|                 enabled: editFlags.canPaste, | ||||
|                 title: `Paste as plain text <kbd>${platformModifier}+Shift+V`, | ||||
|                 uiIcon: "paste", | ||||
|                 uiIcon: "bx bx-paste", | ||||
|                 handler: () => webContents.pasteAndMatchStyle() | ||||
|             }); | ||||
|         } | ||||
| @ -122,7 +122,7 @@ if (utils.isElectron()) { | ||||
|             items.push({ | ||||
|                 enabled: editFlags.canPaste, | ||||
|                 title: `Search for "${shortenedSelection}" with DuckDuckGo`, | ||||
|                 uiIcon: "search-alt", | ||||
|                 uiIcon: "bx bx-search-alt", | ||||
|                 handler: () => electron.shell.openExternal(`https://duckduckgo.com/?q=${encodeURIComponent(params.selectionText)}`) | ||||
|             }); | ||||
|         } | ||||
|  | ||||
							
								
								
									
										48
									
								
								src/public/app/dialogs/bulk_assign_attributes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/public/app/dialogs/bulk_assign_attributes.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| import utils from "../services/utils.js"; | ||||
| import bulkActionService from "../services/bulk_action.js"; | ||||
| import froca from "../services/froca.js"; | ||||
| 
 | ||||
| const $dialog = $("#bulk-assign-attributes-dialog"); | ||||
| const $availableActionList = $("#bulk-available-action-list"); | ||||
| const $existingActionList = $("#bulk-existing-action-list"); | ||||
| 
 | ||||
| $dialog.on('click', '[data-action-add]', async event => { | ||||
|     const actionName = $(event.target).attr('data-action-add'); | ||||
| 
 | ||||
|     await bulkActionService.addAction('bulkaction', actionName); | ||||
| 
 | ||||
|     await refresh(); | ||||
| }); | ||||
| 
 | ||||
| for (const actionGroup of bulkActionService.ACTION_GROUPS) { | ||||
|     const $actionGroupList = $("<td>"); | ||||
|     const $actionGroup = $("<tr>") | ||||
|         .append($("<td>").text(actionGroup.title + ": ")) | ||||
|         .append($actionGroupList); | ||||
| 
 | ||||
|     for (const action of actionGroup.actions) { | ||||
|         $actionGroupList.append( | ||||
|             $('<button class="btn btn-sm">') | ||||
|                 .attr('data-action-add', action.actionName) | ||||
|                 .text(action.actionTitle) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     $availableActionList.append($actionGroup); | ||||
| } | ||||
| 
 | ||||
| async function refresh() { | ||||
|     const bulkActionNote = await froca.getNote('bulkaction'); | ||||
| 
 | ||||
|     const actions = bulkActionService.parseActions(bulkActionNote); | ||||
| 
 | ||||
|     $existingActionList | ||||
|         .empty() | ||||
|         .append(...actions.map(action => action.render())); | ||||
| } | ||||
| 
 | ||||
| export async function showDialog(nodes) { | ||||
|     await refresh(); | ||||
| 
 | ||||
|     utils.openDialog($dialog); | ||||
| } | ||||
							
								
								
									
										94
									
								
								src/public/app/dialogs/note_type_chooser.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/public/app/dialogs/note_type_chooser.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,94 @@ | ||||
| import noteTypesService from "../services/note_types.js"; | ||||
| 
 | ||||
| const $dialog = $("#note-type-chooser-dialog"); | ||||
| const $noteTypeDropdown = $("#note-type-dropdown"); | ||||
| const $noteTypeDropdownTrigger = $("#note-type-dropdown-trigger"); | ||||
| $noteTypeDropdownTrigger.dropdown(); | ||||
| 
 | ||||
| let resolve; | ||||
| let $originalFocused; // element focused before the dialog was opened, so we can return to it afterwards
 | ||||
| let $originalDialog; | ||||
| 
 | ||||
| export async function chooseNoteType() { | ||||
|     $originalFocused = $(':focus'); | ||||
| 
 | ||||
|     const noteTypes = await noteTypesService.getNoteTypeItems(); | ||||
| 
 | ||||
|     $noteTypeDropdown.empty(); | ||||
| 
 | ||||
|     for (const noteType of noteTypes) { | ||||
|         if (noteType.title === '----') { | ||||
|             $noteTypeDropdown.append($('<h6 class="dropdown-header">').append("Templates:")); | ||||
|         } | ||||
|         else { | ||||
|             $noteTypeDropdown.append( | ||||
|                 $('<a class="dropdown-item" tabindex="0">') | ||||
|                     .attr("data-note-type", noteType.type) | ||||
|                     .attr("data-template-note-id", noteType.templateNoteId) | ||||
|                     .append($("<span>").addClass(noteType.uiIcon)) | ||||
|                     .append(" " + noteType.title) | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     $noteTypeDropdownTrigger.dropdown('show'); | ||||
| 
 | ||||
|     $originalDialog = glob.activeDialog; | ||||
|     glob.activeDialog = $dialog; | ||||
|     $dialog.modal(); | ||||
| 
 | ||||
|     $noteTypeDropdown.find(".dropdown-item:first").focus(); | ||||
| 
 | ||||
|     return new Promise((res, rej) => { resolve = res; }); | ||||
| } | ||||
| 
 | ||||
| $dialog.on("hidden.bs.modal", () => { | ||||
|     if (resolve) { | ||||
|         resolve({success: false}); | ||||
|     } | ||||
| 
 | ||||
|     if ($originalFocused) { | ||||
|         $originalFocused.trigger('focus'); | ||||
|         $originalFocused = null; | ||||
|     } | ||||
| 
 | ||||
|     glob.activeDialog = $originalDialog; | ||||
| }); | ||||
| 
 | ||||
| function doResolve(e) { | ||||
|     const $item = $(e.target).closest(".dropdown-item"); | ||||
|     const noteType = $item.attr("data-note-type"); | ||||
|     const templateNoteId = $item.attr("data-template-note-id"); | ||||
| 
 | ||||
|     resolve({ | ||||
|         success: true, | ||||
|         noteType, | ||||
|         templateNoteId | ||||
|     }); | ||||
|     resolve = null; | ||||
| 
 | ||||
|     $dialog.modal("hide"); | ||||
| } | ||||
| 
 | ||||
| $noteTypeDropdown.on('click', '.dropdown-item', e => doResolve(e)); | ||||
| 
 | ||||
| $noteTypeDropdown.on('focus', '.dropdown-item', e => { | ||||
|     $noteTypeDropdown.find('.dropdown-item').each((i, el) => { | ||||
|         $(el).toggleClass('active', el === e.target); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| $noteTypeDropdown.on('keydown', '.dropdown-item', e => { | ||||
|     if (e.key === 'Enter') { | ||||
|         doResolve(e); | ||||
|         e.preventDefault(); | ||||
|         return false; | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| $noteTypeDropdown.parent().on('hide.bs.dropdown', e => { | ||||
|     // prevent closing dropdown by clicking outside
 | ||||
|     if (e.clickEvent) { | ||||
|         e.preventDefault(); | ||||
|     } | ||||
| }); | ||||
| @ -9,8 +9,9 @@ let parentNoteId = null; | ||||
| $form.on('submit', async () => { | ||||
|     const sortBy = $form.find("input[name='sort-by']:checked").val(); | ||||
|     const sortDirection = $form.find("input[name='sort-direction']:checked").val(); | ||||
|     const foldersFirst = $form.find("input[name='sort-folders-first']").is(":checked"); | ||||
| 
 | ||||
|     await server.put(`notes/${parentNoteId}/sort-children`, {sortBy, sortDirection}); | ||||
|     await server.put(`notes/${parentNoteId}/sort-children`, {sortBy, sortDirection, foldersFirst}); | ||||
| 
 | ||||
|     utils.closeActiveDialog(); | ||||
| }); | ||||
|  | ||||
| @ -17,7 +17,8 @@ const NOTE_TYPE_ICONS = { | ||||
|     "book": "bx bx-book", | ||||
|     "note-map": "bx bx-map-alt", | ||||
|     "mermaid": "bx bx-selection", | ||||
|     "canvas": "bx bx-pen" | ||||
|     "canvas": "bx bx-pen", | ||||
|     "web-view": "bx bx-globe-alt" | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -49,6 +49,7 @@ import NoteWrapperWidget from "../widgets/note_wrapper.js"; | ||||
| import BacklinksWidget from "../widgets/backlinks.js"; | ||||
| import SharedInfoWidget from "../widgets/shared_info.js"; | ||||
| import FindWidget from "../widgets/find.js"; | ||||
| import TocWidget from "../widgets/toc.js"; | ||||
| 
 | ||||
| export default class DesktopLayout { | ||||
|     constructor(customWidgets) { | ||||
| @ -169,6 +170,7 @@ export default class DesktopLayout { | ||||
|                         .child(...this.customWidgets.get('center-pane')) | ||||
|                     ) | ||||
|                     .child(new RightPaneContainer() | ||||
|                         .child(new TocWidget()) | ||||
|                         .child(...this.customWidgets.get('right-pane')) | ||||
|                     ) | ||||
|                 ) | ||||
|  | ||||
							
								
								
									
										92
									
								
								src/public/app/services/bulk_action.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/public/app/services/bulk_action.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,92 @@ | ||||
| import server from "./server.js"; | ||||
| import ws from "./ws.js"; | ||||
| import MoveNoteBulkAction from "../widgets/bulk_actions/note/move_note.js"; | ||||
| import DeleteNoteBulkAction from "../widgets/bulk_actions/note/delete_note.js"; | ||||
| import DeleteNoteRevisionsBulkAction from "../widgets/bulk_actions/note/delete_note_revisions.js"; | ||||
| import DeleteLabelBulkAction from "../widgets/bulk_actions/label/delete_label.js"; | ||||
| import DeleteRelationBulkAction from "../widgets/bulk_actions/relation/delete_relation.js"; | ||||
| import RenameLabelBulkAction from "../widgets/bulk_actions/label/rename_label.js"; | ||||
| import RenameRelationBulkAction from "../widgets/bulk_actions/relation/rename_relation.js"; | ||||
| import UpdateLabelValueBulkAction from "../widgets/bulk_actions/label/update_label_value.js"; | ||||
| import UpdateRelationTargetBulkAction from "../widgets/bulk_actions/relation/update_relation_target.js"; | ||||
| import ExecuteScriptBulkAction from "../widgets/bulk_actions/execute_script.js"; | ||||
| import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js"; | ||||
| import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js"; | ||||
| 
 | ||||
| const ACTION_GROUPS = [ | ||||
|     { | ||||
|         title: 'Labels', | ||||
|         actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction] | ||||
|     }, | ||||
|     { | ||||
|         title: 'Relations', | ||||
|         actions: [AddRelationBulkAction, UpdateRelationTargetBulkAction, RenameRelationBulkAction, DeleteRelationBulkAction] | ||||
|     }, | ||||
|     { | ||||
|         title: 'Notes', | ||||
|         actions: [DeleteNoteBulkAction, DeleteNoteRevisionsBulkAction, MoveNoteBulkAction], | ||||
|     }, | ||||
|     { | ||||
|         title: 'Other', | ||||
|         actions: [ExecuteScriptBulkAction] | ||||
|     } | ||||
| ]; | ||||
| 
 | ||||
| const ACTION_CLASSES = [ | ||||
|     MoveNoteBulkAction, | ||||
|     DeleteNoteBulkAction, | ||||
|     DeleteNoteRevisionsBulkAction, | ||||
|     DeleteLabelBulkAction, | ||||
|     DeleteRelationBulkAction, | ||||
|     RenameLabelBulkAction, | ||||
|     RenameRelationBulkAction, | ||||
|     AddLabelBulkAction, | ||||
|     AddRelationBulkAction, | ||||
|     UpdateLabelValueBulkAction, | ||||
|     UpdateRelationTargetBulkAction, | ||||
|     ExecuteScriptBulkAction | ||||
| ]; | ||||
| 
 | ||||
| async function addAction(noteId, actionName) { | ||||
|     await server.post(`notes/${noteId}/attributes`, { | ||||
|         type: 'label', | ||||
|         name: 'action', | ||||
|         value: JSON.stringify({ | ||||
|             name: actionName | ||||
|         }) | ||||
|     }); | ||||
| 
 | ||||
|     await ws.waitForMaxKnownEntityChangeId(); | ||||
| } | ||||
| 
 | ||||
| function parseActions(note) { | ||||
|     const actionLabels = note.getLabels('action'); | ||||
| 
 | ||||
|     return actionLabels.map(actionAttr => { | ||||
|         let actionDef; | ||||
| 
 | ||||
|         try { | ||||
|             actionDef = JSON.parse(actionAttr.value); | ||||
|         } catch (e) { | ||||
|             logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         const ActionClass = ACTION_CLASSES.find(actionClass => actionClass.actionName === actionDef.name); | ||||
| 
 | ||||
|         if (!ActionClass) { | ||||
|             logError(`No action class for '${actionDef.name}' found.`); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         return new ActionClass(actionAttr, actionDef); | ||||
|     }) | ||||
|         .filter(action => !!action); | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|     addAction, | ||||
|     parseActions, | ||||
|     ACTION_CLASSES, | ||||
|     ACTION_GROUPS | ||||
| }; | ||||
| @ -82,7 +82,7 @@ class ContextMenu { | ||||
|                 const $icon = $("<span>"); | ||||
| 
 | ||||
|                 if (item.uiIcon) { | ||||
|                     $icon.addClass("bx bx-" + item.uiIcon); | ||||
|                     $icon.addClass(item.uiIcon); | ||||
|                 } else { | ||||
|                     $icon.append(" "); | ||||
|                 } | ||||
|  | ||||
| @ -6,9 +6,9 @@ function openContextMenu(notePath, e) { | ||||
|         x: e.pageX, | ||||
|         y: e.pageY, | ||||
|         items: [ | ||||
|             {title: "Open note in a new tab", command: "openNoteInNewTab", uiIcon: "empty"}, | ||||
|             {title: "Open note in a new split", command: "openNoteInNewSplit", uiIcon: "dock-right"}, | ||||
|             {title: "Open note in a new window", command: "openNoteInNewWindow", uiIcon: "window-open"} | ||||
|             {title: "Open note in a new tab", command: "openNoteInNewTab", uiIcon: "bx bx-empty"}, | ||||
|             {title: "Open note in a new split", command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right"}, | ||||
|             {title: "Open note in a new window", command: "openNoteInNewWindow", uiIcon: "bx bx-window-open"} | ||||
|         ], | ||||
|         selectMenuItemHandler: ({command}) => { | ||||
|             if (command === 'openNoteInNewTab') { | ||||
|  | ||||
| @ -140,7 +140,9 @@ function initNoteAutocomplete($el, options) { | ||||
|         appendTo: document.querySelector('body'), | ||||
|         hint: false, | ||||
|         autoselect: true, | ||||
|         openOnFocus: true, | ||||
|         // openOnFocus has to be false, otherwise re-focus (after return from note type chooser dialog) forces
 | ||||
|         // re-querying of the autocomplete source which then changes currently selected suggestion
 | ||||
|         openOnFocus: false, | ||||
|         minLength: 0, | ||||
|         tabAutocomplete: false | ||||
|     }, [ | ||||
| @ -170,9 +172,18 @@ function initNoteAutocomplete($el, options) { | ||||
|         } | ||||
| 
 | ||||
|         if (suggestion.action === 'create-note') { | ||||
|             const noteTypeChooserDialog = await import('../dialogs/note_type_chooser.js'); | ||||
|             const {success, noteType, templateNoteId} = await noteTypeChooserDialog.chooseNoteType(); | ||||
| 
 | ||||
|             if (!success) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const {note} = await noteCreateService.createNote(suggestion.parentNoteId, { | ||||
|                 title: suggestion.noteTitle, | ||||
|                 activate: false | ||||
|                 activate: false, | ||||
|                 type: noteType, | ||||
|                 templateNoteId: templateNoteId | ||||
|             }); | ||||
| 
 | ||||
|             suggestion.notePath = treeService.getSomeNotePath(note); | ||||
| @ -261,7 +272,6 @@ function init() { | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|     autocompleteSource, | ||||
|     autocompleteSourceForCKEditor, | ||||
|     initNoteAutocomplete, | ||||
|     showRecentNotes, | ||||
|  | ||||
| @ -43,7 +43,8 @@ async function createNote(parentNotePath, options = {}) { | ||||
|         content: options.content || "", | ||||
|         isProtected: options.isProtected, | ||||
|         type: options.type, | ||||
|         mime: options.mime | ||||
|         mime: options.mime, | ||||
|         templateNoteId: options.templateNoteId | ||||
|     }); | ||||
| 
 | ||||
|     if (options.saveSelection) { | ||||
| @ -74,6 +75,20 @@ async function createNote(parentNotePath, options = {}) { | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| async function createNoteWithTypePrompt(parentNotePath, options = {}) { | ||||
|     const noteTypeChooserDialog = await import('../dialogs/note_type_chooser.js'); | ||||
|     const {success, noteType, templateNoteId} = await noteTypeChooserDialog.chooseNoteType(); | ||||
| 
 | ||||
|     if (!success) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     options.type = noteType; | ||||
|     options.templateNoteId = templateNoteId; | ||||
| 
 | ||||
|     return await createNote(parentNotePath, options); | ||||
| } | ||||
| 
 | ||||
| /* If first element is heading, parse it out and use it as a new heading. */ | ||||
| function parseSelectedHtml(selectedHtml) { | ||||
|     const dom = $.parseHTML(selectedHtml); | ||||
| @ -105,5 +120,6 @@ async function duplicateSubtree(noteId, parentNotePath) { | ||||
| 
 | ||||
| export default { | ||||
|     createNote, | ||||
|     createNoteWithTypePrompt, | ||||
|     duplicateSubtree | ||||
| }; | ||||
|  | ||||
							
								
								
									
										40
									
								
								src/public/app/services/note_types.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/public/app/services/note_types.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| import server from "./server.js"; | ||||
| import froca from "./froca.js"; | ||||
| 
 | ||||
| async function getNoteTypeItems(command) { | ||||
|     const items = [ | ||||
|         { title: "Text", command: command, type: "text", uiIcon: "bx bx-note" }, | ||||
|         { title: "Code", command: command, type: "code", uiIcon: "bx bx-code" }, | ||||
|         { title: "Saved Search", command: command, type: "search", uiIcon: "bx bx-file-find" }, | ||||
|         { title: "Relation Map", command: command, type: "relation-map", uiIcon: "bx bx-map-alt" }, | ||||
|         { title: "Note Map", command: command, type: "note-map", uiIcon: "bx bx-map-alt" }, | ||||
|         { title: "Render Note", command: command, type: "render", uiIcon: "bx bx-extension" }, | ||||
|         { title: "Book", command: command, type: "book", uiIcon: "bx bx-book" }, | ||||
|         { title: "Mermaid Diagram", command: command, type: "mermaid", uiIcon: "bx bx-selection" }, | ||||
|         { title: "Canvas", command: command, type: "canvas", uiIcon: "bx bx-pen" }, | ||||
|         { title: "Web View", command: command, type: "iframe", uiIcon: "bx bx-globe-alt" }, | ||||
|     ]; | ||||
| 
 | ||||
|     const templateNoteIds = await server.get("search-templates"); | ||||
|     const templateNotes = await froca.getNotes(templateNoteIds); | ||||
| 
 | ||||
|     if (items.length > 0) { | ||||
|         items.push({ title: "----" }); | ||||
| 
 | ||||
|         for (const templateNote of templateNotes) { | ||||
|             items.push({ | ||||
|                 title: templateNote.title, | ||||
|                 uiIcon: templateNote.getIcon(), | ||||
|                 command: command, | ||||
|                 type: templateNote.type, | ||||
|                 templateNoteId: templateNote.noteId | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return items; | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|     getNoteTypeItems | ||||
| } | ||||
| @ -4,6 +4,7 @@ import clipboard from './clipboard.js'; | ||||
| import noteCreateService from "./note_create.js"; | ||||
| import contextMenu from "./context_menu.js"; | ||||
| import appContext from "./app_context.js"; | ||||
| import noteTypesService from "./note_types.js"; | ||||
| 
 | ||||
| class TreeContextMenu { | ||||
|     /** | ||||
| @ -24,20 +25,6 @@ class TreeContextMenu { | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     getNoteTypeItems(command) { | ||||
|         return [ | ||||
|             { title: "Text", command: command, type: "text", uiIcon: "note" }, | ||||
|             { title: "Code", command: command, type: "code", uiIcon: "code" }, | ||||
|             { title: "Saved search", command: command, type: "search", uiIcon: "file-find" }, | ||||
|             { title: "Relation Map", command: command, type: "relation-map", uiIcon: "map-alt" }, | ||||
|             { title: "Note Map", command: command, type: "note-map", uiIcon: "map-alt" }, | ||||
|             { title: "Render HTML note", command: command, type: "render", uiIcon: "extension" }, | ||||
|             { title: "Book", command: command, type: "book", uiIcon: "book" }, | ||||
|             { title: "Mermaid diagram", command: command, type: "mermaid", uiIcon: "selection" }, | ||||
|             { title: "Canvas", command: command, type: "canvas", uiIcon: "pen" }, | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     async getMenuItems() { | ||||
|         const note = await froca.getNote(this.node.data.noteId); | ||||
|         const branch = froca.getBranch(this.node.data.branchId); | ||||
| @ -57,58 +44,59 @@ class TreeContextMenu { | ||||
|         const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch; | ||||
| 
 | ||||
|         return [ | ||||
|             { title: 'Open in a new tab <kbd>Ctrl+Click</kbd>', command: "openInTab", uiIcon: "empty", enabled: noSelectedNotes }, | ||||
|             { title: 'Open in a new split', command: "openNoteInSplit", uiIcon: "dock-right", enabled: noSelectedNotes }, | ||||
|             { title: 'Insert note after <kbd data-command="createNoteAfter"></kbd>', command: "insertNoteAfter", uiIcon: "plus", | ||||
|                 items: insertNoteAfterEnabled ? this.getNoteTypeItems("insertNoteAfter") : null, | ||||
|             { title: 'Open in a new tab <kbd>Ctrl+Click</kbd>', command: "openInTab", uiIcon: "bx bx-empty", enabled: noSelectedNotes }, | ||||
|             { title: 'Open in a new split', command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes }, | ||||
|             { title: 'Insert note after <kbd data-command="createNoteAfter"></kbd>', command: "insertNoteAfter", uiIcon: "bx bx-plus", | ||||
|                 items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null, | ||||
|                 enabled: insertNoteAfterEnabled && noSelectedNotes }, | ||||
|             { title: 'Insert child note <kbd data-command="createNoteInto"></kbd>', command: "insertChildNote", uiIcon: "plus", | ||||
|                 items: notSearch ? this.getNoteTypeItems("insertChildNote") : null, | ||||
|             { title: 'Insert child note <kbd data-command="createNoteInto"></kbd>', command: "insertChildNote", uiIcon: "bx bx-plus", | ||||
|                 items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null, | ||||
|                 enabled: notSearch && noSelectedNotes }, | ||||
|             { title: 'Delete <kbd data-command="deleteNotes"></kbd>', command: "deleteNotes", uiIcon: "trash", | ||||
|             { title: 'Delete <kbd data-command="deleteNotes"></kbd>', command: "deleteNotes", uiIcon: "bx bx-trash", | ||||
|                 enabled: isNotRoot && !isHoisted && parentNotSearch }, | ||||
|             { title: "----" }, | ||||
|             { title: 'Search in subtree <kbd data-command="searchInSubtree"></kbd>', command: "searchInSubtree", uiIcon: "search", | ||||
|             { title: 'Search in subtree <kbd data-command="searchInSubtree"></kbd>', command: "searchInSubtree", uiIcon: "bx bx-search", | ||||
|                 enabled: notSearch && noSelectedNotes }, | ||||
|             isHoisted ? null : { title: 'Hoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "empty", enabled: noSelectedNotes && notSearch }, | ||||
|             !isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "door-open" }, | ||||
|             { title: 'Edit branch prefix <kbd data-command="editBranchPrefix"></kbd>', command: "editBranchPrefix", uiIcon: "empty", | ||||
|             isHoisted ? null : { title: 'Hoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch }, | ||||
|             !isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" }, | ||||
|             { title: 'Edit branch prefix <kbd data-command="editBranchPrefix"></kbd>', command: "editBranchPrefix", uiIcon: "bx bx-empty", | ||||
|                 enabled: isNotRoot && parentNotSearch && noSelectedNotes}, | ||||
|             { title: "Advanced", uiIcon: "empty", enabled: true, items: [ | ||||
|                     { title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "expand", enabled: noSelectedNotes }, | ||||
|                     { title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "collapse", enabled: noSelectedNotes }, | ||||
|                     { title: "Force note sync", command: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes }, | ||||
|                     { title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "empty", enabled: noSelectedNotes && notSearch }, | ||||
|                     { title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "history", enabled: noSelectedNotes } | ||||
|             { title: "Advanced", uiIcon: "bx bx-empty", enabled: true, items: [ | ||||
|                     { title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes }, | ||||
|                     { title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes }, | ||||
|                     { title: "Force note sync", command: "forceNoteSync", uiIcon: "bx bx-refresh", enabled: noSelectedNotes }, | ||||
|                     { title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch }, | ||||
|                     { title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes } | ||||
|                 ] }, | ||||
|             { title: "----" }, | ||||
|             { title: "Protect subtree", command: "protectSubtree", uiIcon: "check-shield", enabled: noSelectedNotes }, | ||||
|             { title: "Unprotect subtree", command: "unprotectSubtree", uiIcon: "shield", enabled: noSelectedNotes }, | ||||
|             { title: "Protect subtree", command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes }, | ||||
|             { title: "Unprotect subtree", command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes }, | ||||
|             { title: "----" }, | ||||
|             { title: 'Copy / clone <kbd data-command="copyNotesToClipboard"></kbd>', command: "copyNotesToClipboard", uiIcon: "copy", | ||||
|             { title: 'Copy / clone <kbd data-command="copyNotesToClipboard"></kbd>', command: "copyNotesToClipboard", uiIcon: "bx bx-copy", | ||||
|                 enabled: isNotRoot && !isHoisted }, | ||||
|             { title: 'Clone to ... <kbd data-command="cloneNotesTo"></kbd>', command: "cloneNotesTo", uiIcon: "empty", | ||||
|             { title: 'Clone to ... <kbd data-command="cloneNotesTo"></kbd>', command: "cloneNotesTo", uiIcon: "bx bx-empty", | ||||
|                 enabled: isNotRoot && !isHoisted }, | ||||
|             { title: 'Cut <kbd data-command="cutNotesToClipboard"></kbd>', command: "cutNotesToClipboard", uiIcon: "cut", | ||||
|             { title: 'Cut <kbd data-command="cutNotesToClipboard"></kbd>', command: "cutNotesToClipboard", uiIcon: "bx bx-cut", | ||||
|                 enabled: isNotRoot && !isHoisted && parentNotSearch }, | ||||
|             { title: 'Move to ... <kbd data-command="moveNotesTo"></kbd>', command: "moveNotesTo", uiIcon: "empty", | ||||
|             { title: 'Move to ... <kbd data-command="moveNotesTo"></kbd>', command: "moveNotesTo", uiIcon: "bx bx-empty", | ||||
|                 enabled: isNotRoot && !isHoisted && parentNotSearch }, | ||||
|             { title: 'Paste into <kbd data-command="pasteNotesFromClipboard"></kbd>', command: "pasteNotesFromClipboard", uiIcon: "paste", | ||||
|             { title: 'Paste into <kbd data-command="pasteNotesFromClipboard"></kbd>', command: "pasteNotesFromClipboard", uiIcon: "bx bx-paste", | ||||
|                 enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes }, | ||||
|             { title: 'Paste after', command: "pasteNotesAfterFromClipboard", uiIcon: "paste", | ||||
|             { title: 'Paste after', command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste", | ||||
|                 enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes }, | ||||
|             { title: `Duplicate subtree <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "empty", | ||||
|             { title: `Duplicate subtree <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty", | ||||
|                 enabled: parentNotSearch && isNotRoot && !isHoisted }, | ||||
|             { title: "----" }, | ||||
|             { title: "Export", command: "exportNote", uiIcon: "empty", | ||||
|             { title: "Export", command: "exportNote", uiIcon: "bx bx-empty", | ||||
|                 enabled: notSearch && noSelectedNotes }, | ||||
|             { title: "Import into note", command: "importIntoNote", uiIcon: "empty", | ||||
|                 enabled: notSearch && noSelectedNotes } | ||||
|             { title: "Import into note", command: "importIntoNote", uiIcon: "bx bx-empty", | ||||
|                 enabled: notSearch && noSelectedNotes }, | ||||
|             { title: "Bulk assign attributes", command: "bulkAssignAttributes", uiIcon: "bx bx-empty", | ||||
|                 enabled: true } | ||||
|         ].filter(row => row !== null); | ||||
|     } | ||||
| 
 | ||||
|     async selectMenuItemHandler({command, type}) { | ||||
|         const noteId = this.node.data.noteId; | ||||
|     async selectMenuItemHandler({command, type, templateNoteId}) { | ||||
|         const notePath = treeService.getNotePath(this.node); | ||||
| 
 | ||||
|         if (command === 'openInTab') { | ||||
| @ -122,7 +110,8 @@ class TreeContextMenu { | ||||
|                 target: 'after', | ||||
|                 targetBranchId: this.node.data.branchId, | ||||
|                 type: type, | ||||
|                 isProtected: isProtected | ||||
|                 isProtected: isProtected, | ||||
|                 templateNoteId: templateNoteId | ||||
|             }); | ||||
|         } | ||||
|         else if (command === "insertChildNote") { | ||||
| @ -130,7 +119,8 @@ class TreeContextMenu { | ||||
| 
 | ||||
|             noteCreateService.createNote(parentNotePath, { | ||||
|                 type: type, | ||||
|                 isProtected: this.node.data.isProtected | ||||
|                 isProtected: this.node.data.isProtected, | ||||
|                 templateNoteId: templateNoteId | ||||
|             }); | ||||
|         } | ||||
|         else if (command === 'openNoteInSplit') { | ||||
|  | ||||
| @ -217,11 +217,11 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget { | ||||
|             y: e.pageY, | ||||
|             orientation: 'left', | ||||
|             items: [ | ||||
|                 {title: `Add new label <kbd data-command="addNewLabel"></kbd>`, command: "addNewLabel", uiIcon: "hash"}, | ||||
|                 {title: `Add new relation <kbd data-command="addNewRelation"></kbd>`, command: "addNewRelation", uiIcon: "transfer"}, | ||||
|                 {title: `Add new label <kbd data-command="addNewLabel"></kbd>`, command: "addNewLabel", uiIcon: "bx bx-hash"}, | ||||
|                 {title: `Add new relation <kbd data-command="addNewRelation"></kbd>`, command: "addNewRelation", uiIcon: "bx bx-transfer"}, | ||||
|                 {title: "----"}, | ||||
|                 {title: "Add new label definition", command: "addNewLabelDefinition", uiIcon: "empty"}, | ||||
|                 {title: "Add new relation definition", command: "addNewRelationDefinition", uiIcon: "empty"}, | ||||
|                 {title: "Add new label definition", command: "addNewLabelDefinition", uiIcon: "bx bx-empty"}, | ||||
|                 {title: "Add new relation definition", command: "addNewRelationDefinition", uiIcon: "bx bx-empty"}, | ||||
|             ], | ||||
|             selectMenuItemHandler: ({command}) => this.handleAddNewAttributeCommand(command) | ||||
|         }); | ||||
| @ -485,7 +485,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget { | ||||
|     } | ||||
| 
 | ||||
|     async createNoteForReferenceLink(title) { | ||||
|         const {note} = await noteCreateService.createNote(this.notePath, { | ||||
|         const {note} = await noteCreateService.createNoteWithTypePrompt(this.notePath, { | ||||
|             activate: false, | ||||
|             title: title | ||||
|         }); | ||||
|  | ||||
| @ -103,10 +103,22 @@ class BasicWidget extends Component { | ||||
|         this.$widget.toggleClass('hidden-int', !show); | ||||
|     } | ||||
| 
 | ||||
|     isHiddenInt() { | ||||
|         return this.$widget.hasClass('hidden-int'); | ||||
|     } | ||||
| 
 | ||||
|     toggleExt(show) { | ||||
|         this.$widget.toggleClass('hidden-ext', !show); | ||||
|     } | ||||
| 
 | ||||
|     isHiddenExt() { | ||||
|         return this.$widget.hasClass('hidden-ext'); | ||||
|     } | ||||
| 
 | ||||
|     canBeShown() { | ||||
|         return !this.isHiddenInt() && !this.isHiddenExt(); | ||||
|     } | ||||
| 
 | ||||
|     isVisible() { | ||||
|         return this.$widget.is(":visible"); | ||||
|     } | ||||
|  | ||||
| @ -3,10 +3,8 @@ import ws from "../../services/ws.js"; | ||||
| import Component from "../component.js"; | ||||
| import utils from "../../services/utils.js"; | ||||
| 
 | ||||
| export default class AbstractSearchAction extends Component { | ||||
| export default class AbstractBulkAction { | ||||
|     constructor(attribute, actionDef) { | ||||
|         super(); | ||||
| 
 | ||||
|         this.attribute = attribute; | ||||
|         this.actionDef = actionDef; | ||||
|     } | ||||
| @ -1,5 +1,5 @@ | ||||
| import SpacedUpdate from "../../services/spaced_update.js"; | ||||
| import AbstractSearchAction from "./abstract_search_action.js"; | ||||
| import AbstractBulkAction from "./abstract_bulk_action.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
| @ -33,8 +33,9 @@ const TPL = ` | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class ExecuteScriptSearchAction extends AbstractSearchAction { | ||||
| export default class ExecuteScriptBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "executeScript"; } | ||||
|     static get actionTitle() { return "Execute script"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         const $action = $(TPL); | ||||
| @ -1,11 +1,11 @@ | ||||
| import SpacedUpdate from "../../services/spaced_update.js"; | ||||
| import AbstractSearchAction from "./abstract_search_action.js"; | ||||
| import SpacedUpdate from "../../../services/spaced_update.js"; | ||||
| import AbstractBulkAction from "../abstract_bulk_action.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
|     <td colspan="2"> | ||||
|         <div style="display: flex; align-items: center"> | ||||
|             <div style="margin-right: 10px;" class="text-nowrap">Set label</div>  | ||||
|             <div style="margin-right: 10px;" class="text-nowrap">Add label</div>  | ||||
|              | ||||
|             <input type="text"  | ||||
|                 class="form-control label-name"  | ||||
| @ -37,8 +37,9 @@ const TPL = ` | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class SetLabelValueSearchAction extends AbstractSearchAction { | ||||
|     static get actionName() { return "setLabelValue"; } | ||||
| export default class AddLabelBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "addLabel"; } | ||||
|     static get actionTitle() { return "Add label"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         const $action = $(TPL); | ||||
| @ -1,5 +1,5 @@ | ||||
| import SpacedUpdate from "../../services/spaced_update.js"; | ||||
| import AbstractSearchAction from "./abstract_search_action.js"; | ||||
| import SpacedUpdate from "../../../services/spaced_update.js"; | ||||
| import AbstractBulkAction from "../abstract_bulk_action.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
| @ -18,8 +18,9 @@ const TPL = ` | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class DeleteLabelSearchAction extends AbstractSearchAction { | ||||
| export default class DeleteLabelBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "deleteLabel"; } | ||||
|     static get actionTitle() { return "Delete label"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         const $action = $(TPL); | ||||
| @ -1,5 +1,5 @@ | ||||
| import SpacedUpdate from "../../services/spaced_update.js"; | ||||
| import AbstractSearchAction from "./abstract_search_action.js"; | ||||
| import SpacedUpdate from "../../../services/spaced_update.js"; | ||||
| import AbstractBulkAction from "../abstract_bulk_action.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
| @ -27,8 +27,9 @@ const TPL = ` | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class RenameLabelSearchAction extends AbstractSearchAction { | ||||
| export default class RenameLabelBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "renameLabel"; } | ||||
|     static get actionTitle() { return "Rename label"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         const $action = $(TPL); | ||||
| @ -0,0 +1,60 @@ | ||||
| import SpacedUpdate from "../../../services/spaced_update.js"; | ||||
| import AbstractBulkAction from "../abstract_bulk_action.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
|     <td colspan="2"> | ||||
|         <div style="display: flex; align-items: center"> | ||||
|             <div style="margin-right: 10px;" class="text-nowrap">Update label value</div>  | ||||
|              | ||||
|             <input type="text"  | ||||
|                 class="form-control label-name"  | ||||
|                 placeholder="label name" | ||||
|                 pattern="[\\p{L}\\p{N}_:]+" | ||||
|                 title="Alphanumeric characters, underscore and colon are allowed characters."/> | ||||
|              | ||||
|             <div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">to value</div> | ||||
|              | ||||
|             <input type="text" class="form-control label-value" placeholder="new value"/> | ||||
|         </div> | ||||
|     </td> | ||||
|     <td class="button-column"> | ||||
|         <div class="dropdown help-dropdown"> | ||||
|             <span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span> | ||||
|             <div class="dropdown-menu dropdown-menu-right p-4"> | ||||
|                 <p>On all matched notes, change value of the existing label.</p> | ||||
|                  | ||||
|                 <p>You can also call this method without value, in such case label will be assigned to the note without value.</p> | ||||
|             </div>  | ||||
|         </div> | ||||
|      | ||||
|         <span class="bx bx-x icon-action action-conf-del"></span> | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class UpdateLabelValueBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "updateLabelValue"; } | ||||
|     static get actionTitle() { return "Update label value"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         const $action = $(TPL); | ||||
| 
 | ||||
|         const $labelName = $action.find('.label-name'); | ||||
|         $labelName.val(this.actionDef.labelName || ""); | ||||
| 
 | ||||
|         const $labelValue = $action.find('.label-value'); | ||||
|         $labelValue.val(this.actionDef.labelValue || ""); | ||||
| 
 | ||||
|         const spacedUpdate = new SpacedUpdate(async () => { | ||||
|             await this.saveAction({ | ||||
|                 labelName: $labelName.val(), | ||||
|                 labelValue: $labelValue.val() | ||||
|             }); | ||||
|         }, 1000) | ||||
| 
 | ||||
|         $labelName.on('input', () => spacedUpdate.scheduleUpdate()); | ||||
|         $labelValue.on('input', () => spacedUpdate.scheduleUpdate()); | ||||
| 
 | ||||
|         return $action; | ||||
|     } | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| import AbstractSearchAction from "./abstract_search_action.js"; | ||||
| import AbstractBulkAction from "../abstract_bulk_action.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
| @ -12,8 +12,9 @@ const TPL = ` | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class DeleteNoteSearchAction extends AbstractSearchAction { | ||||
| export default class DeleteNoteBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "deleteNote"; } | ||||
|     static get actionTitle() { return "Delete note"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         return $(TPL); | ||||
| @ -1,4 +1,4 @@ | ||||
| import AbstractSearchAction from "./abstract_search_action.js"; | ||||
| import AbstractBulkAction from "../abstract_bulk_action.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
| @ -19,8 +19,9 @@ const TPL = ` | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class DeleteNoteRevisionsSearchAction extends AbstractSearchAction { | ||||
| export default class DeleteNoteRevisionsBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "deleteNoteRevisions"; } | ||||
|     static get actionTitle() { return "Delete note revisions"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         return $(TPL); | ||||
| @ -1,6 +1,6 @@ | ||||
| import SpacedUpdate from "../../services/spaced_update.js"; | ||||
| import AbstractSearchAction from "./abstract_search_action.js"; | ||||
| import noteAutocompleteService from "../../services/note_autocomplete.js"; | ||||
| import SpacedUpdate from "../../../services/spaced_update.js"; | ||||
| import AbstractBulkAction from "../abstract_bulk_action.js"; | ||||
| import noteAutocompleteService from "../../../services/note_autocomplete.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
| @ -33,8 +33,9 @@ const TPL = ` | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class MoveNoteSearchAction extends AbstractSearchAction { | ||||
| export default class MoveNoteBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "moveNote"; } | ||||
|     static get actionTitle() { return "Move note"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         const $action = $(TPL); | ||||
							
								
								
									
										65
									
								
								src/public/app/widgets/bulk_actions/relation/add_relation.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/public/app/widgets/bulk_actions/relation/add_relation.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| import SpacedUpdate from "../../../services/spaced_update.js"; | ||||
| import AbstractBulkAction from "../abstract_bulk_action.js"; | ||||
| import noteAutocompleteService from "../../../services/note_autocomplete.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
|     <td colspan="2"> | ||||
|         <div style="display: flex; align-items: center"> | ||||
|             <div style="margin-right: 10px;" class="text-nowrap">Add relation</div>  | ||||
| 
 | ||||
|             <input type="text"  | ||||
|                 class="form-control relation-name"  | ||||
|                 placeholder="relation name" | ||||
|                 pattern="[\\p{L}\\p{N}_:]+" | ||||
|                 style="flex-shrink: 3" | ||||
|                 title="Alphanumeric characters, underscore and colon are allowed characters."/> | ||||
|                  | ||||
|             <div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">to</div> | ||||
|              | ||||
|             <div class="input-group" style="flex-shrink: 2"> | ||||
|                 <input type="text" class="form-control target-note" placeholder="target note"/> | ||||
|             </div> | ||||
|         </div> | ||||
|     </td> | ||||
|     <td class="button-column"> | ||||
|         <div class="dropdown help-dropdown"> | ||||
|             <span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span> | ||||
|             <div class="dropdown-menu dropdown-menu-right p-4"> | ||||
|                 <p>On all matched notes create given relation.</p> | ||||
|             </div>  | ||||
|         </div> | ||||
|      | ||||
|         <span class="bx bx-x icon-action action-conf-del"></span> | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class AddRelationBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "addRelation"; } | ||||
|     static get actionTitle() { return "Add relation"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         const $action = $(TPL); | ||||
| 
 | ||||
|         const $relationName = $action.find('.relation-name'); | ||||
|         $relationName.val(this.actionDef.relationName || ""); | ||||
| 
 | ||||
|         const $targetNote = $action.find('.target-note'); | ||||
|         noteAutocompleteService.initNoteAutocomplete($targetNote); | ||||
|         $targetNote.setNote(this.actionDef.targetNoteId); | ||||
| 
 | ||||
|         $targetNote.on('autocomplete:closed', () => spacedUpdate.scheduleUpdate()); | ||||
| 
 | ||||
|         const spacedUpdate = new SpacedUpdate(async () => { | ||||
|             await this.saveAction({ | ||||
|                 relationName: $relationName.val(), | ||||
|                 targetNoteId: $targetNote.getSelectedNoteId() | ||||
|             }); | ||||
|         }, 1000) | ||||
| 
 | ||||
|         $relationName.on('input', () => spacedUpdate.scheduleUpdate()); | ||||
|         $targetNote.on('input', () => spacedUpdate.scheduleUpdate()); | ||||
| 
 | ||||
|         return $action; | ||||
|     } | ||||
| } | ||||
| @ -1,5 +1,5 @@ | ||||
| import SpacedUpdate from "../../services/spaced_update.js"; | ||||
| import AbstractSearchAction from "./abstract_search_action.js"; | ||||
| import SpacedUpdate from "../../../services/spaced_update.js"; | ||||
| import AbstractBulkAction from "../abstract_bulk_action.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
| @ -20,8 +20,9 @@ const TPL = ` | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class DeleteRelationSearchAction extends AbstractSearchAction { | ||||
| export default class DeleteRelationBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "deleteRelation"; } | ||||
|     static get actionTitle() { return "Delete relation"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         const $action = $(TPL); | ||||
| @ -1,5 +1,5 @@ | ||||
| import SpacedUpdate from "../../services/spaced_update.js"; | ||||
| import AbstractSearchAction from "./abstract_search_action.js"; | ||||
| import SpacedUpdate from "../../../services/spaced_update.js"; | ||||
| import AbstractBulkAction from "../abstract_bulk_action.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
| @ -27,8 +27,9 @@ const TPL = ` | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class RenameRelationSearchAction extends AbstractSearchAction { | ||||
| export default class RenameRelationBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "renameRelation"; } | ||||
|     static get actionTitle() { return "Rename relation"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         const $action = $(TPL); | ||||
| @ -1,12 +1,12 @@ | ||||
| import SpacedUpdate from "../../services/spaced_update.js"; | ||||
| import AbstractSearchAction from "./abstract_search_action.js"; | ||||
| import noteAutocompleteService from "../../services/note_autocomplete.js"; | ||||
| import SpacedUpdate from "../../../services/spaced_update.js"; | ||||
| import AbstractBulkAction from "../abstract_bulk_action.js"; | ||||
| import noteAutocompleteService from "../../../services/note_autocomplete.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <tr> | ||||
|     <td colspan="2"> | ||||
|         <div style="display: flex; align-items: center"> | ||||
|             <div style="margin-right: 10px;" class="text-nowrap">Set relation</div>  | ||||
|             <div style="margin-right: 10px;" class="text-nowrap">Update relation</div>  | ||||
|              | ||||
|             <input type="text"  | ||||
|                 class="form-control relation-name"  | ||||
| @ -39,8 +39,9 @@ const TPL = ` | ||||
|     </td> | ||||
| </tr>`; | ||||
| 
 | ||||
| export default class SetRelationTargetSearchAction extends AbstractSearchAction { | ||||
|     static get actionName() { return "setRelationTarget"; } | ||||
| export default class UpdateRelationTargetBulkAction extends AbstractBulkAction { | ||||
|     static get actionName() { return "updateRelationTarget"; } | ||||
|     static get actionTitle() { return "Update relation target"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         const $action = $(TPL); | ||||
| @ -9,6 +9,9 @@ const WIDGET_TPL = ` | ||||
|     </div> | ||||
| </div>`; | ||||
| 
 | ||||
| /** | ||||
|  * TODO: rename, it's not collapsible anymore | ||||
|  */ | ||||
| export default class CollapsibleWidget extends NoteContextAwareWidget { | ||||
|     get widgetTitle() { return "Untitled widget"; } | ||||
| 
 | ||||
| @ -32,8 +35,4 @@ export default class CollapsibleWidget extends NoteContextAwareWidget { | ||||
| 
 | ||||
|     /** for overriding */ | ||||
|     async doRenderBody() {} | ||||
| 
 | ||||
|     isExpanded() { | ||||
|         return this.$bodyWrapper.hasClass("show"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -195,6 +195,12 @@ export default class RibbonContainer extends NoteContextAwareWidget { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async noteSwitched() { | ||||
|         this.lastActiveComponentId = null; | ||||
| 
 | ||||
|         await super.noteSwitched(); | ||||
|     } | ||||
| 
 | ||||
|     async refreshWithNote(note, noExplicitActivation = false) { | ||||
|         this.lastNoteType = note.type; | ||||
| 
 | ||||
|  | ||||
| @ -11,7 +11,9 @@ export default class RightPaneContainer extends FlexContainer { | ||||
|     } | ||||
| 
 | ||||
|     isEnabled() { | ||||
|         return super.isEnabled() && this.children.length > 0 && !!this.children.find(ch => ch.isEnabled()); | ||||
|         return super.isEnabled() | ||||
|             && this.children.length > 0 | ||||
|             && !!this.children.find(ch => ch.isEnabled() && ch.canBeShown()); | ||||
|     } | ||||
| 
 | ||||
|     handleEventInChildren(name, data) { | ||||
| @ -21,13 +23,20 @@ export default class RightPaneContainer extends FlexContainer { | ||||
|             // right pane is displayed only if some child widget is active
 | ||||
|             // we'll reevaluate the visibility based on events which are probable to cause visibility change
 | ||||
|             // but these events needs to be finished and only then we check
 | ||||
|             promise.then(() => { | ||||
|                 this.toggleInt(this.isEnabled()); | ||||
| 
 | ||||
|                 splitService.setupRightPaneResizer(); | ||||
|             }); | ||||
|             promise.then(() => this.reevaluateIsEnabledCommand()); | ||||
|         } | ||||
| 
 | ||||
|         return promise; | ||||
|     } | ||||
| 
 | ||||
|     reevaluateIsEnabledCommand() { | ||||
|         const oldToggle = !this.isHiddenInt(); | ||||
|         const newToggle = this.isEnabled(); | ||||
| 
 | ||||
|         if (oldToggle !== newToggle) { | ||||
|             this.toggleInt(newToggle); | ||||
| 
 | ||||
|             splitService.setupRightPaneResizer(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -65,8 +65,8 @@ export default class HistoryNavigationWidget extends BasicWidget { | ||||
|             items.push({ | ||||
|                 title, | ||||
|                 idx, | ||||
|                 uiIcon: idx == activeIndex ? "radio-circle-marked" : // compare with type coercion!
 | ||||
|                     (idx < activeIndex ? "left-arrow-alt" : "right-arrow-alt") | ||||
|                 uiIcon: idx == activeIndex ? "bx bx-radio-circle-marked" : // compare with type coercion!
 | ||||
|                     (idx < activeIndex ? "bx bx-left-arrow-alt" : "bx bx-right-arrow-alt") | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -18,9 +18,9 @@ class MobileDetailMenuWidget extends BasicWidget { | ||||
|                 x: e.pageX, | ||||
|                 y: e.pageY, | ||||
|                 items: [ | ||||
|                     { title: "Insert child note", command: "insertChildNote", uiIcon: "plus", | ||||
|                     { title: "Insert child note", command: "insertChildNote", uiIcon: "bx bx-plus", | ||||
|                         enabled: note.type !== 'search' }, | ||||
|                     { title: "Delete this note", command: "delete", uiIcon: "trash", | ||||
|                     { title: "Delete this note", command: "delete", uiIcon: "bx bx-trash", | ||||
|                         enabled: note.noteId !== 'root' } | ||||
|                 ], | ||||
|                 selectMenuItemHandler: async ({command}) => { | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import BasicWidget from "../basic_widget.js"; | ||||
| import protectedSessionHolder from "../../services/protected_session_holder.js"; | ||||
| 
 | ||||
| const WIDGET_TPL = ` | ||||
| <div id="global-buttons"> | ||||
| @ -39,6 +40,8 @@ const WIDGET_TPL = ` | ||||
| 
 | ||||
|         <div class="dropdown-menu dropdown-menu-right"> | ||||
|             <a class="dropdown-item" data-trigger-command="switchToDesktopVersion"><span class="bx bx-laptop"></span> Switch to desktop version</a> | ||||
|             <a class="dropdown-item" data-trigger-command="enterProtectedSession"><span class="bx bx-shield-quarter"></span> Enter protected session</a> | ||||
|             <a class="dropdown-item" data-trigger-command="leaveProtectedSession"><span class="bx bx-check-shield"></span> Leave protected session</a> | ||||
|             <a class="dropdown-item" data-trigger-command="logout"><span class="bx bx-log-out"></span> Logout</a> | ||||
|         </div> | ||||
|     </div> | ||||
| @ -48,6 +51,18 @@ const WIDGET_TPL = ` | ||||
| class MobileGlobalButtonsWidget extends BasicWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $(WIDGET_TPL); | ||||
|         this.updateSettings(); | ||||
|     } | ||||
| 
 | ||||
|     protectedSessionStartedEvent() { | ||||
|         this.updateSettings(); | ||||
|     } | ||||
| 
 | ||||
|     updateSettings() { | ||||
|         const protectedSession = protectedSessionHolder.isProtectedSessionAvailable(); | ||||
| 
 | ||||
|         this.$widget.find('[data-trigger-command="enterProtectedSession"]').toggle(!protectedSession); | ||||
|         this.$widget.find('[data-trigger-command="leaveProtectedSession"]').toggle(protectedSession); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -3,6 +3,12 @@ import protectedSessionHolder from "../services/protected_session_holder.js"; | ||||
| import SpacedUpdate from "../services/spaced_update.js"; | ||||
| import server from "../services/server.js"; | ||||
| import libraryLoader from "../services/library_loader.js"; | ||||
| import appContext from "../services/app_context.js"; | ||||
| import keyboardActionsService from "../services/keyboard_actions.js"; | ||||
| import noteCreateService from "../services/note_create.js"; | ||||
| import attributeService from "../services/attributes.js"; | ||||
| import attributeRenderer from "../services/attribute_renderer.js"; | ||||
| 
 | ||||
| import EmptyTypeWidget from "./type_widgets/empty.js"; | ||||
| import EditableTextTypeWidget from "./type_widgets/editable_text.js"; | ||||
| import EditableCodeTypeWidget from "./type_widgets/editable_code.js"; | ||||
| @ -13,16 +19,12 @@ import RelationMapTypeWidget from "./type_widgets/relation_map.js"; | ||||
| import CanvasTypeWidget from "./type_widgets/canvas.js"; | ||||
| import ProtectedSessionTypeWidget from "./type_widgets/protected_session.js"; | ||||
| import BookTypeWidget from "./type_widgets/book.js"; | ||||
| import appContext from "../services/app_context.js"; | ||||
| import keyboardActionsService from "../services/keyboard_actions.js"; | ||||
| import noteCreateService from "../services/note_create.js"; | ||||
| import DeletedTypeWidget from "./type_widgets/deleted.js"; | ||||
| import ReadOnlyTextTypeWidget from "./type_widgets/read_only_text.js"; | ||||
| import ReadOnlyCodeTypeWidget from "./type_widgets/read_only_code.js"; | ||||
| import NoneTypeWidget from "./type_widgets/none.js"; | ||||
| import attributeService from "../services/attributes.js"; | ||||
| import NoteMapTypeWidget from "./type_widgets/note_map.js"; | ||||
| import attributeRenderer from "../services/attribute_renderer.js"; | ||||
| import WebViewTypeWidget from "./type_widgets/web_view.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <div class="note-detail"> | ||||
| @ -54,7 +56,8 @@ const typeWidgetClasses = { | ||||
|     'canvas': CanvasTypeWidget, | ||||
|     'protected-session': ProtectedSessionTypeWidget, | ||||
|     'book': BookTypeWidget, | ||||
|     'note-map': NoteMapTypeWidget | ||||
|     'note-map': NoteMapTypeWidget, | ||||
|     'web-view': WebViewTypeWidget | ||||
| }; | ||||
| 
 | ||||
| export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
| @ -154,7 +157,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
|         // https://github.com/zadam/trilium/issues/2522
 | ||||
|         this.$widget.toggleClass("full-height", | ||||
|             !this.noteContext.hasNoteList() | ||||
|             && ['editable-text', 'editable-code', 'canvas'].includes(this.type) | ||||
|             && ['editable-text', 'editable-code', 'canvas', 'web-view'].includes(this.type) | ||||
|             && this.mime !== 'text/x-sqlite;schema=trilium'); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -309,13 +309,39 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | ||||
| 
 | ||||
|                 if (targetType === 'title' || targetType === 'icon') { | ||||
|                     if (event.shiftKey) { | ||||
|                         node.setSelected(!node.isSelected()); | ||||
|                         const activeNode = this.getActiveNode(); | ||||
| 
 | ||||
|                         if (activeNode.getParent() !== node.getParent()) { | ||||
|                             return; | ||||
|                         } | ||||
| 
 | ||||
|                         this.clearSelectedNodes(); | ||||
| 
 | ||||
|                         function selectInBetween(first, second) { | ||||
|                             for (let i = 0; first && first !== second && i < 10000; i++) { | ||||
|                                 first.setSelected(true); | ||||
|                                 first = first.getNextSibling(); | ||||
|                             } | ||||
| 
 | ||||
|                             second.setSelected(); | ||||
|                         } | ||||
| 
 | ||||
|                         if (activeNode.getIndex() < node.getIndex()) { | ||||
|                             selectInBetween(activeNode, node); | ||||
|                         } else { | ||||
|                             selectInBetween(node, activeNode); | ||||
|                         } | ||||
| 
 | ||||
|                         node.setFocus(true); | ||||
|                     } | ||||
|                     else if (event.ctrlKey) { | ||||
|                         const notePath = treeService.getNotePath(node); | ||||
|                         appContext.tabManager.openTabWithNoteWithHoisting(notePath); | ||||
|                     } | ||||
|                     else if (event.altKey) { | ||||
|                         node.setSelected(!node.isSelected()); | ||||
|                         node.setFocus(true); | ||||
|                     } | ||||
|                     else if (data.node.isActive()) { | ||||
|                         // this is important for single column mobile view, otherwise it's not possible to see again previously displayed note
 | ||||
|                         this.tree.reactivate(true); | ||||
| @ -513,6 +539,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | ||||
|                         subNode.load(); | ||||
|                     } | ||||
|                 }); | ||||
|             }, | ||||
|             select: () => { | ||||
|                 // TODO
 | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
| @ -1422,6 +1451,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | ||||
|         importDialog.showDialog(node.data.noteId); | ||||
|     } | ||||
| 
 | ||||
|     async bulkAssignAttributesCommand({node}) { | ||||
|         const bulkAssignAttributesDialog = await import('../dialogs/bulk_assign_attributes.js'); | ||||
|         bulkAssignAttributesDialog.showDialog(this.getSelectedOrActiveNodes(node)); | ||||
|     } | ||||
| 
 | ||||
|     forceNoteSyncCommand({node}) { | ||||
|         syncService.forceNoteSync(node.data.noteId); | ||||
|     } | ||||
|  | ||||
| @ -12,8 +12,9 @@ const NOTE_TYPES = [ | ||||
|     { type: "relation-map", mime: "application/json", title: "Relation Map", selectable: true }, | ||||
|     { type: "render", mime: '', title: "Render Note", selectable: true }, | ||||
|     { type: "canvas", mime: 'application/json', title: "Canvas", selectable: true }, | ||||
|     { type: "book", mime: '', title: "Book", selectable: true }, | ||||
|     { type: "mermaid", mime: 'text/mermaid', title: "Mermaid Diagram", selectable: true }, | ||||
|     { type: "book", mime: '', title: "Book", selectable: true }, | ||||
|     { type: "web-view", mime: '', title: "Web View", selectable: true }, | ||||
|     { type: "code", mime: 'text/plain', title: "Code", selectable: true } | ||||
| ]; | ||||
| 
 | ||||
|  | ||||
| @ -36,7 +36,7 @@ export default class NoteWrapperWidget extends FlexContainer { | ||||
|         const note = this.noteContext?.note; | ||||
| 
 | ||||
|         this.$widget.toggleClass("full-content-width", | ||||
|             ['image', 'mermaid', 'book', 'render', 'canvas'].includes(note?.type) | ||||
|             ['image', 'mermaid', 'book', 'render', 'canvas', 'web-view'].includes(note?.type) | ||||
|             || !!note?.hasLabel('fullContentWidth') | ||||
|         ); | ||||
|     } | ||||
|  | ||||
| @ -5,14 +5,6 @@ import ws from "../../services/ws.js"; | ||||
| import toastService from "../../services/toast.js"; | ||||
| import treeService from "../../services/tree.js"; | ||||
| 
 | ||||
| import DeleteNoteSearchAction from "../search_actions/delete_note.js"; | ||||
| import DeleteLabelSearchAction from "../search_actions/delete_label.js"; | ||||
| import DeleteRelationSearchAction from "../search_actions/delete_relation.js"; | ||||
| import RenameLabelSearchAction from "../search_actions/rename_label.js"; | ||||
| import SetLabelValueSearchAction from "../search_actions/set_label_value.js"; | ||||
| import SetRelationTargetSearchAction from "../search_actions/set_relation_target.js"; | ||||
| import RenameRelationSearchAction from "../search_actions/rename_relation.js"; | ||||
| import ExecuteScriptSearchAction from "../search_actions/execute_script.js" | ||||
| import SearchString from "../search_options/search_string.js"; | ||||
| import FastSearch from "../search_options/fast_search.js"; | ||||
| import Ancestor from "../search_options/ancestor.js"; | ||||
| @ -20,10 +12,9 @@ import IncludeArchivedNotes from "../search_options/include_archived_notes.js"; | ||||
| import OrderBy from "../search_options/order_by.js"; | ||||
| import SearchScript from "../search_options/search_script.js"; | ||||
| import Limit from "../search_options/limit.js"; | ||||
| import DeleteNoteRevisionsSearchAction from "../search_actions/delete_note_revisions.js"; | ||||
| import Debug from "../search_options/debug.js"; | ||||
| import appContext from "../../services/app_context.js"; | ||||
| import MoveNoteSearchAction from "../search_actions/move_note.js"; | ||||
| import bulkActionService from "../../services/bulk_action.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <div class="search-definition-widget"> | ||||
| @ -73,6 +64,10 @@ const TPL = ` | ||||
|     .add-search-option button { | ||||
|         margin-top: 5px; /* to give some spacing when buttons overflow on the next line */ | ||||
|     } | ||||
|      | ||||
|     .dropdown-header { | ||||
|         background-color: var(--accented-background-color); | ||||
|     } | ||||
|     </style> | ||||
| 
 | ||||
|     <div class="search-settings"> | ||||
| @ -127,28 +122,7 @@ const TPL = ` | ||||
|                         <span class="bx bxs-zap"></span> | ||||
|                         action | ||||
|                       </button> | ||||
|                       <div class="dropdown-menu"> | ||||
|                         <a class="dropdown-item" href="#" data-action-add="moveNote"> | ||||
|                             Move note</a> | ||||
|                         <a class="dropdown-item" href="#" data-action-add="deleteNote"> | ||||
|                             Delete note</a> | ||||
|                         <a class="dropdown-item" href="#" data-action-add="deleteNoteRevisions"> | ||||
|                             Delete note revisions</a> | ||||
|                         <a class="dropdown-item" href="#" data-action-add="deleteLabel"> | ||||
|                             Delete label</a> | ||||
|                         <a class="dropdown-item" href="#" data-action-add="deleteRelation"> | ||||
|                             Delete relation</a> | ||||
|                         <a class="dropdown-item" href="#" data-action-add="renameLabel"> | ||||
|                             Rename label</a> | ||||
|                         <a class="dropdown-item" href="#" data-action-add="renameRelation"> | ||||
|                             Rename relation</a> | ||||
|                         <a class="dropdown-item" href="#" data-action-add="setLabelValue"> | ||||
|                             Set label value</a> | ||||
|                         <a class="dropdown-item" href="#" data-action-add="setRelationTarget"> | ||||
|                             Set relation target</a> | ||||
|                         <a class="dropdown-item" href="#" data-action-add="executeScript"> | ||||
|                             Execute script</a> | ||||
|                       </div> | ||||
|                       <div class="dropdown-menu action-list"></div> | ||||
|                     </div> | ||||
|                 </td> | ||||
|             </tr> | ||||
| @ -193,24 +167,11 @@ const OPTION_CLASSES = [ | ||||
|     Debug | ||||
| ]; | ||||
| 
 | ||||
| const ACTION_CLASSES = {}; | ||||
| 
 | ||||
| for (const clazz of [ | ||||
|     MoveNoteSearchAction, | ||||
|     DeleteNoteSearchAction, | ||||
|     DeleteNoteRevisionsSearchAction, | ||||
|     DeleteLabelSearchAction, | ||||
|     DeleteRelationSearchAction, | ||||
|     RenameLabelSearchAction, | ||||
|     RenameRelationSearchAction, | ||||
|     SetLabelValueSearchAction, | ||||
|     SetRelationTargetSearchAction, | ||||
|     ExecuteScriptSearchAction | ||||
| ]) { | ||||
|     ACTION_CLASSES[clazz.actionName] = clazz; | ||||
| } | ||||
| 
 | ||||
| export default class SearchDefinitionWidget extends NoteContextAwareWidget { | ||||
|     get name() { | ||||
|         return "searchDefinition"; | ||||
|     } | ||||
| 
 | ||||
|     isEnabled() { | ||||
|         return this.note && this.note.type === 'search'; | ||||
|     } | ||||
| @ -228,6 +189,19 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget { | ||||
|         this.$widget = $(TPL); | ||||
|         this.contentSized(); | ||||
|         this.$component = this.$widget.find('.search-definition-widget'); | ||||
|         this.$actionList = this.$widget.find('.action-list'); | ||||
| 
 | ||||
|         for (const actionGroup of bulkActionService.ACTION_GROUPS) { | ||||
|             this.$actionList.append($('<h6 class="dropdown-header">').append(actionGroup.title)); | ||||
| 
 | ||||
|             for (const action of actionGroup.actions) { | ||||
|                 this.$actionList.append( | ||||
|                     $('<a class="dropdown-item" href="#">') | ||||
|                         .attr('data-action-add', action.actionName) | ||||
|                         .text(action.actionTitle) | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.$widget.on('click', '[data-search-option-add]', async event => { | ||||
|             const searchOptionName = $(event.target).attr('data-search-option-add'); | ||||
| @ -244,19 +218,11 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget { | ||||
|         }); | ||||
| 
 | ||||
|         this.$widget.on('click', '[data-action-add]', async event => { | ||||
|             const actionName = $(event.target).attr('data-action-add'); | ||||
| 
 | ||||
|             await server.post(`notes/${this.noteId}/attributes`, { | ||||
|                 type: 'label', | ||||
|                 name: 'action', | ||||
|                 value: JSON.stringify({ | ||||
|                     name: actionName | ||||
|                 }) | ||||
|             }); | ||||
| 
 | ||||
|             this.$widget.find('.action-add-toggle').dropdown('toggle'); | ||||
| 
 | ||||
|             await ws.waitForMaxKnownEntityChangeId(); | ||||
|             const actionName = $(event.target).attr('data-action-add'); | ||||
| 
 | ||||
|             await bulkActionService.addAction(this.noteId, actionName); | ||||
| 
 | ||||
|             this.refresh(); | ||||
|         }); | ||||
| @ -319,35 +285,13 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.$actionOptions.empty(); | ||||
|         const actions = bulkActionService.parseActions(this.note); | ||||
| 
 | ||||
|         const actionLabels = this.note.getLabels('action'); | ||||
|         this.$actionOptions | ||||
|             .empty() | ||||
|             .append(...actions.map(action => action.render())); | ||||
| 
 | ||||
|         for (const actionAttr of actionLabels) { | ||||
|             let actionDef; | ||||
| 
 | ||||
|             try { | ||||
|                 actionDef = JSON.parse(actionAttr.value); | ||||
|             } | ||||
|             catch (e) { | ||||
|                 logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`); | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             const ActionClass = ACTION_CLASSES[actionDef.name]; | ||||
| 
 | ||||
|             if (!ActionClass) { | ||||
|                 logError(`No action class for '${actionDef.name}' found.`); | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             const action = new ActionClass(actionAttr, actionDef).setParent(this); | ||||
|             this.child(action); | ||||
| 
 | ||||
|             this.$actionOptions.append(action.render()); | ||||
|         } | ||||
| 
 | ||||
|         this.$searchAndExecuteButton.css('visibility', actionLabels.length > 0 ? 'visible' : 'hidden'); | ||||
|         this.$searchAndExecuteButton.css('visibility', actions.length > 0 ? 'visible' : 'hidden'); | ||||
|     } | ||||
| 
 | ||||
|     getContent() { | ||||
|  | ||||
| @ -262,9 +262,9 @@ export default class TabRowWidget extends BasicWidget { | ||||
|                 x: e.pageX, | ||||
|                 y: e.pageY, | ||||
|                 items: [ | ||||
|                     {title: "Move this tab to a new window", command: "moveTabToNewWindow", uiIcon: "window-open"}, | ||||
|                     {title: "Close all tabs", command: "removeAllTabs", uiIcon: "x"}, | ||||
|                     {title: "Close all tabs except for this", command: "removeAllTabsExceptForThis", uiIcon: "x"}, | ||||
|                     {title: "Move this tab to a new window", command: "moveTabToNewWindow", uiIcon: "bx bx-window-open"}, | ||||
|                     {title: "Close all tabs", command: "removeAllTabs", uiIcon: "bx bx-x"}, | ||||
|                     {title: "Close all tabs except for this", command: "removeAllTabsExceptForThis", uiIcon: "bx bx-x"}, | ||||
|                 ], | ||||
|                 selectMenuItemHandler: ({command}) => { | ||||
|                     this.triggerCommand(command, {ntxId}); | ||||
|  | ||||
							
								
								
									
										272
									
								
								src/public/app/widgets/toc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								src/public/app/widgets/toc.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,272 @@ | ||||
| /** | ||||
|  * Table of contents widget | ||||
|  * (c) Antonio Tejada 2022 | ||||
|  * | ||||
|  * By design there's no support for non-sensical or malformed constructs: | ||||
|  * - headings inside elements (eg Trilium allows headings inside tables, but | ||||
|  *   not inside lists) | ||||
|  * - nested headings when using raw HTML <H2><H3></H3></H2> | ||||
|  * - malformed headings when using raw HTML <H2></H3></H2><H3> | ||||
|  * - etc. | ||||
|  * | ||||
|  * In those cases the generated TOC may be incorrect or the navigation may lead | ||||
|  * to the wrong heading (although what "right" means in those cases is not | ||||
|  * clear), but it won't crash. | ||||
|  */ | ||||
| 
 | ||||
| import attributeService from "../services/attributes.js"; | ||||
| import CollapsibleWidget from "./collapsible_widget.js"; | ||||
| 
 | ||||
| const TPL = `<div class="toc-widget">
 | ||||
|     <style> | ||||
|         .toc-widget { | ||||
|             padding: 10px; | ||||
|             contain: none;  | ||||
|             overflow:auto; | ||||
|         } | ||||
|          | ||||
|         .toc ol { | ||||
|             padding-left: 25px; | ||||
|         } | ||||
|          | ||||
|         .toc > ol { | ||||
|             padding-left: 10px; | ||||
|         } | ||||
|     </style> | ||||
| 
 | ||||
|     <span class="toc"></span> | ||||
| </div>`; | ||||
| 
 | ||||
| /** | ||||
|  * Find a heading node in the parent's children given its index. | ||||
|  * | ||||
|  * @param {Element} parent Parent node to find a headingIndex'th in. | ||||
|  * @param {uint} headingIndex Index for the heading | ||||
|  * @returns {Element|null} Heading node with the given index, null couldn't be | ||||
|  *          found (ie malformed like nested headings, etc) | ||||
|  */ | ||||
| function findHeadingNodeByIndex(parent, headingIndex) { | ||||
|     let headingNode = null; | ||||
|     for (let i = 0; i < parent.childCount; ++i) { | ||||
|         let child = parent.getChild(i); | ||||
| 
 | ||||
|         // Headings appear as flattened top level children in the CKEditor
 | ||||
|         // document named as "heading" plus the level, eg "heading2",
 | ||||
|         // "heading3", "heading2", etc and not nested wrt the heading level. If
 | ||||
|         // a heading node is found, decrement the headingIndex until zero is
 | ||||
|         // reached
 | ||||
|         if (child.name.startsWith("heading")) { | ||||
|             if (headingIndex === 0) { | ||||
|                 headingNode = child; | ||||
|                 break; | ||||
|             } | ||||
|             headingIndex--; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return headingNode; | ||||
| } | ||||
| 
 | ||||
| function findHeadingElementByIndex(parent, headingIndex) { | ||||
|     let headingElement = null; | ||||
|     for (let i = 0; i < parent.children.length; ++i) { | ||||
|         const child = parent.children[i]; | ||||
|         // Headings appear as flattened top level children in the DOM named as
 | ||||
|         // "H" plus the level, eg "H2", "H3", "H2", etc and not nested wrt the
 | ||||
|         // heading level. If a heading node is found, decrement the headingIndex
 | ||||
|         // until zero is reached
 | ||||
| 
 | ||||
|         if (child.tagName.match(/H\d+/i) !== null) { | ||||
|             if (headingIndex === 0) { | ||||
|                 headingElement = child; | ||||
|                 break; | ||||
|             } | ||||
|             headingIndex--; | ||||
|         } | ||||
|     } | ||||
|     return headingElement; | ||||
| } | ||||
| 
 | ||||
| const MIN_HEADING_COUNT = 3; | ||||
| 
 | ||||
| export default class TocWidget extends CollapsibleWidget { | ||||
|     get widgetTitle() { | ||||
|         return "Table of Contents"; | ||||
|     } | ||||
| 
 | ||||
|     isEnabled() { | ||||
|         return super.isEnabled() | ||||
|             && this.note.type === 'text' | ||||
|             && !this.note.hasLabel('noToc'); | ||||
|     } | ||||
| 
 | ||||
|     async doRenderBody() { | ||||
|         this.$body.empty().append($(TPL)); | ||||
|         this.$toc = this.$body.find('.toc'); | ||||
|     } | ||||
| 
 | ||||
|     async refreshWithNote(note) { | ||||
|         let $toc = "", headingCount = 0; | ||||
|         // Check for type text unconditionally in case alwaysShowWidget is set
 | ||||
|         if (this.note.type === 'text') { | ||||
|             const { content } = await note.getNoteComplement(); | ||||
|             ({$toc, headingCount} = await this.getToc(content)); | ||||
|         } | ||||
| 
 | ||||
|         this.$toc.html($toc); | ||||
|         this.toggleInt(headingCount >= MIN_HEADING_COUNT); | ||||
|         this.triggerCommand("reevaluateIsEnabled"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Builds a jquery table of contents. | ||||
|      * | ||||
|      * @param {String} html Note's html content | ||||
|      * @returns {$toc: jQuery, headingCount: integer} ordered list table of headings, nested by heading level | ||||
|      *         with an onclick event that will cause the document to scroll to | ||||
|      *         the desired position. | ||||
|      */ | ||||
|     getToc(html) { | ||||
|         // Regular expression for headings <h1>...</h1> using non-greedy
 | ||||
|         // matching and backreferences
 | ||||
|         const headingTagsRegex = /<h(\d+)>(.*?)<\/h\1>/g; | ||||
| 
 | ||||
|         // Use jquery to build the table rather than html text, since it makes
 | ||||
|         // it easier to set the onclick event that will be executed with the
 | ||||
|         // right captured callback context
 | ||||
|         const $toc = $("<ol>"); | ||||
|         // Note heading 2 is the first level Trilium makes available to the note
 | ||||
|         let curLevel = 2; | ||||
|         const $ols = [$toc]; | ||||
|         let headingCount; | ||||
|         for (let m = null, headingIndex = 0; ((m = headingTagsRegex.exec(html)) !== null); headingIndex++) { | ||||
|             //
 | ||||
|             // Nest/unnest whatever necessary number of ordered lists
 | ||||
|             //
 | ||||
|             const newLevel = m[1]; | ||||
|             const levelDelta = newLevel - curLevel; | ||||
|             if (levelDelta > 0) { | ||||
|                 // Open as many lists as newLevel - curLevel
 | ||||
|                 for (let i = 0; i < levelDelta; i++) { | ||||
|                     const $ol = $("<ol>"); | ||||
|                     $ols[$ols.length - 1].append($ol); | ||||
|                     $ols.push($ol); | ||||
|                 } | ||||
|             } else if (levelDelta < 0) { | ||||
|                 // Close as many lists as curLevel - newLevel
 | ||||
|                 for (let i = 0; i < -levelDelta; ++i) { | ||||
|                     $ols.pop(); | ||||
|                 } | ||||
|             } | ||||
|             curLevel = newLevel; | ||||
| 
 | ||||
|             //
 | ||||
|             // Create the list item and set up the click callback
 | ||||
|             //
 | ||||
|             const $li = $('<li style="cursor:pointer">' + m[2] + '</li>'); | ||||
|             // XXX Do this with CSS? How to inject CSS in doRender?
 | ||||
|             $li.hover(function () { | ||||
|                 $(this).css("font-weight", "bold"); | ||||
|             }).mouseout(function () { | ||||
|                 $(this).css("font-weight", "normal"); | ||||
|             }); | ||||
|             $li.on("click", () => this.jumpToHeading(headingIndex)); | ||||
|             $ols[$ols.length - 1].append($li); | ||||
|             headingCount = headingIndex; | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             $toc, | ||||
|             headingCount | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     async jumpToHeading(headingIndex) { | ||||
|         // A readonly note can change state to "readonly disabled
 | ||||
|         // temporarily" (ie "edit this note" button) without any
 | ||||
|         // intervening events, do the readonly calculation at navigation
 | ||||
|         // time and not at outline creation time
 | ||||
|         // See https://github.com/zadam/trilium/issues/2828
 | ||||
|         const isReadOnly = await this.noteContext.isReadOnly(); | ||||
| 
 | ||||
|         if (isReadOnly) { | ||||
|             const $readonlyTextContent = await this.noteContext.getContentElement(); | ||||
| 
 | ||||
|             const headingElement = findHeadingElementByIndex($readonlyTextContent[0], headingIndex); | ||||
| 
 | ||||
|             if (headingElement != null) { | ||||
|                 headingElement.scrollIntoView(); | ||||
|             } | ||||
|         } else { | ||||
|             const textEditor = await this.noteContext.getTextEditor(); | ||||
| 
 | ||||
|             const model = textEditor.model; | ||||
|             const doc = model.document; | ||||
|             const root = doc.getRoot(); | ||||
| 
 | ||||
|             const headingNode = findHeadingNodeByIndex(root, headingIndex); | ||||
| 
 | ||||
|             // headingNode could be null if the html was malformed or
 | ||||
|             // with headings inside elements, just ignore and don't
 | ||||
|             // navigate (note that the TOC rendering and other TOC
 | ||||
|             // entries' navigation could be wrong too)
 | ||||
|             if (headingNode != null) { | ||||
|                 // Setting the selection alone doesn't scroll to the
 | ||||
|                 // caret, needs to be done explicitly and outside of
 | ||||
|                 // the writer change callback so the scroll is
 | ||||
|                 // guaranteed to happen after the selection is
 | ||||
|                 // updated.
 | ||||
| 
 | ||||
|                 // In addition, scrolling to a caret later in the
 | ||||
|                 // document (ie "forward scrolls"), only scrolls
 | ||||
|                 // barely enough to place the caret at the bottom of
 | ||||
|                 // the screen, which is a usability issue, you would
 | ||||
|                 // like the caret to be placed at the top or center
 | ||||
|                 // of the screen.
 | ||||
| 
 | ||||
|                 // To work around that issue, first scroll to the
 | ||||
|                 // end of the document, then scroll to the desired
 | ||||
|                 // point. This causes all the scrolls to be
 | ||||
|                 // "backward scrolls" no matter the current caret
 | ||||
|                 // position, which places the caret at the top of
 | ||||
|                 // the screen.
 | ||||
| 
 | ||||
|                 // XXX This could be fixed in another way by using
 | ||||
|                 //     the underlying CKEditor5
 | ||||
|                 //     scrollViewportToShowTarget, which allows to
 | ||||
|                 //     provide a larger "viewportOffset", but that
 | ||||
|                 //     has coding complications (requires calling an
 | ||||
|                 //     internal CKEditor utils funcion and passing
 | ||||
|                 //     an HTML element, not a CKEditor node, and
 | ||||
|                 //     CKEditor5 doesn't seem to have a
 | ||||
|                 //     straightforward way to convert a node to an
 | ||||
|                 //     HTML element? (in CKEditor4 this was done
 | ||||
|                 //     with $(node.$) )
 | ||||
| 
 | ||||
|                 // Scroll to the end of the note to guarantee the
 | ||||
|                 // next scroll is a backwards scroll that places the
 | ||||
|                 // caret at the top of the screen
 | ||||
|                 model.change(writer => { | ||||
|                     writer.setSelection(root.getChild(root.childCount - 1), 0); | ||||
|                 }); | ||||
|                 textEditor.editing.view.scrollToTheSelection(); | ||||
|                 // Backwards scroll to the heading
 | ||||
|                 model.change(writer => { | ||||
|                     writer.setSelection(headingNode, 0); | ||||
|                 }); | ||||
|                 textEditor.editing.view.scrollToTheSelection(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async entitiesReloadedEvent({loadResults}) { | ||||
|         if (loadResults.isNoteContentReloaded(this.noteId)) { | ||||
|             await this.refresh(); | ||||
|         } else if (loadResults.getAttributes().find(attr => attr.type === 'label' | ||||
|             && (attr.name.toLowerCase().includes('readonly') || attr.name === 'noToc') | ||||
|             && attributeService.isAffecting(attr, this.note))) { | ||||
| 
 | ||||
|             await this.refresh(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -8,7 +8,7 @@ const {sleep} = utils; | ||||
| 
 | ||||
| const TPL = ` | ||||
|     <div class="canvas-widget note-detail-canvas note-detail-printable note-detail"> | ||||
|         <style type="text/css"> | ||||
|         <style> | ||||
|         .excalidraw .App-menu_top .buttonList { | ||||
|             display: flex; | ||||
|         } | ||||
| @ -336,6 +336,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget { | ||||
|             setDimensions(dimensions); | ||||
| 
 | ||||
|             const onResize = () => { | ||||
|                 if (this.note?.type !== 'canvas') { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 const dimensions = { | ||||
|                     width: excalidrawWrapperRef.current.getBoundingClientRect().width, | ||||
|                     height: excalidrawWrapperRef.current.getBoundingClientRect().height | ||||
|  | ||||
| @ -305,7 +305,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|     } | ||||
| 
 | ||||
|     async createNoteForReferenceLink(title) { | ||||
|         const {note} = await noteCreateService.createNote(this.notePath, { | ||||
|         const {note} = await noteCreateService.createNoteWithTypePrompt(this.notePath, { | ||||
|             activate: false, | ||||
|             title: title | ||||
|         }); | ||||
|  | ||||
| @ -138,9 +138,9 @@ export default class RelationMapTypeWidget extends TypeWidget { | ||||
|                 x: e.pageX, | ||||
|                 y: e.pageY, | ||||
|                 items: [ | ||||
|                     {title: "Open in new tab", command: "openInNewTab", uiIcon: "empty"}, | ||||
|                     {title: "Remove note", command: "remove", uiIcon: "trash"}, | ||||
|                     {title: "Edit title", command: "editTitle", uiIcon: "pencil"}, | ||||
|                     {title: "Open in new tab", command: "openInNewTab", uiIcon: "bx bx-empty"}, | ||||
|                     {title: "Remove note", command: "remove", uiIcon: "bx bx-trash"}, | ||||
|                     {title: "Edit title", command: "editTitle", uiIcon: "bx bx-pencil"}, | ||||
|                 ], | ||||
|                 selectMenuItemHandler: ({command}) => this.contextMenuHandler(command, e.target) | ||||
|             }); | ||||
| @ -446,7 +446,7 @@ export default class RelationMapTypeWidget extends TypeWidget { | ||||
|                 contextMenu.show({ | ||||
|                     x: event.pageX, | ||||
|                     y: event.pageY, | ||||
|                     items: [ {title: "Remove relation", command: "remove", uiIcon: "trash"} ], | ||||
|                     items: [ {title: "Remove relation", command: "remove", uiIcon: "bx bx-trash"} ], | ||||
|                     selectMenuItemHandler: async ({command}) => { | ||||
|                         if (command === 'remove') { | ||||
|                             const confirmDialog = await import('../../dialogs/confirm.js'); | ||||
|  | ||||
							
								
								
									
										67
									
								
								src/public/app/widgets/type_widgets/web_view.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/public/app/widgets/type_widgets/web_view.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,67 @@ | ||||
| import TypeWidget from "./type_widget.js"; | ||||
| import attributeService from "../../services/attributes.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <div class="note-detail-web-view note-detail-printable" style="height: 100%"> | ||||
|     <div class="note-detail-web-view-help alert alert-warning" style="margin: 50px; padding: 20px;"> | ||||
|         <p><strong>This help note is shown because this note of type WebView HTML doesn't have required label to function properly.</strong></p> | ||||
| 
 | ||||
|         <p>Please create label with a URL address you want to embed, e.g. <code>#webViewSrc="http://www.google.com"</code></p> | ||||
|     </div> | ||||
| 
 | ||||
|     <webview class="note-detail-web-view-content"></webview> | ||||
| </div>`; | ||||
| 
 | ||||
| export default class WebViewTypeWidget extends TypeWidget { | ||||
|     static getType() { return "web-view"; } | ||||
| 
 | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|         this.$noteDetailWebViewHelp = this.$widget.find('.note-detail-web-view-help'); | ||||
|         this.$noteDetailWebViewContent = this.$widget.find('.note-detail-web-view-content'); | ||||
| 
 | ||||
|         window.addEventListener('resize', () => this.setDimensions(), false); | ||||
| 
 | ||||
|         super.doRender(); | ||||
|     } | ||||
| 
 | ||||
|     async doRefresh(note) { | ||||
|         this.$widget.show(); | ||||
|         this.$noteDetailWebViewHelp.hide(); | ||||
|         this.$noteDetailWebViewContent.hide(); | ||||
| 
 | ||||
|         const webViewSrc = this.note.getLabelValue('webViewSrc'); | ||||
| 
 | ||||
|         if (webViewSrc) { | ||||
|             this.$noteDetailWebViewContent | ||||
|                 .show() | ||||
|                 .attr("src", webViewSrc); | ||||
|         } | ||||
|         else { | ||||
|             this.$noteDetailWebViewContent.hide(); | ||||
|             this.$noteDetailWebViewHelp.show(); | ||||
|         } | ||||
| 
 | ||||
|         this.setDimensions(); | ||||
| 
 | ||||
|         setTimeout(() => this.setDimensions(), 1000); | ||||
|     } | ||||
| 
 | ||||
|     cleanup() { | ||||
|         this.$noteDetailWebViewContent.removeAttribute("src"); | ||||
|     } | ||||
| 
 | ||||
|     setDimensions() { | ||||
|         const $parent = this.$widget; | ||||
| 
 | ||||
|         this.$noteDetailWebViewContent | ||||
|             .height($parent.height()) | ||||
|             .width($parent.width()); | ||||
|     } | ||||
| 
 | ||||
|     entitiesReloadedEvent({loadResults}) { | ||||
|         if (loadResults.getAttributes().find(attr => attr.name === 'webViewSrc' && attributeService.isAffecting(attr, this.noteContext.note))) { | ||||
|             this.refresh(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -192,6 +192,7 @@ div.ui-tooltip { | ||||
| .dropdown-menu a:hover:not(.disabled), .dropdown-item:hover:not(.disabled) { | ||||
|     color: var(--hover-item-text-color) !important; | ||||
|     background-color: var(--hover-item-background-color) !important; | ||||
|     border-color: var(--hover-item-border-color) !important; | ||||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| @ -210,17 +211,20 @@ div.ui-tooltip { | ||||
|     padding-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .dropdown-item { | ||||
| .dropdown-item, .dropdown-header { | ||||
|     color: var(--menu-text-color) !important; | ||||
|     border: 1px solid transparent !important; | ||||
| } | ||||
| 
 | ||||
| .dropdown-item.disabled, .dropdown-item.disabled kbd { | ||||
|     color: #aaa !important; | ||||
| } | ||||
| 
 | ||||
| .dropdown-item.active { | ||||
| .dropdown-item.active, .dropdown-item:focus { | ||||
|     color: var(--active-item-text-color) !important; | ||||
|     background-color: var(--active-item-background-color) !important; | ||||
|     border-color: var(--active-item-border-color) !important; | ||||
|     outline: none; | ||||
| } | ||||
| 
 | ||||
| .CodeMirror { | ||||
| @ -475,8 +479,8 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th | ||||
| } | ||||
| 
 | ||||
| .algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor { | ||||
|     color: var(--hover-item-text-color); | ||||
|     background-color: var(--hover-item-background-color); | ||||
|     color: var(--active-item-text-color); | ||||
|     background-color: var(--active-item-background-color); | ||||
| } | ||||
| 
 | ||||
| .help-button { | ||||
| @ -945,7 +949,6 @@ input { | ||||
|     border: 0; | ||||
|     height: 100%; | ||||
|     overflow: auto; | ||||
|     max-height: 300px; | ||||
| } | ||||
| 
 | ||||
| #right-pane .card-body ul { | ||||
|  | ||||
| @ -36,11 +36,13 @@ | ||||
|     --input-text-color: #ccc; | ||||
|     --input-background-color: #333; | ||||
| 
 | ||||
|     --hover-item-text-color: black; | ||||
|     --hover-item-background-color: #777; | ||||
|     --hover-item-text-color: #ccc; | ||||
|     --hover-item-background-color: transparent; | ||||
|     --hover-item-border-color: #aaa; | ||||
| 
 | ||||
|     --active-item-text-color: black; | ||||
|     --active-item-background-color: #777; | ||||
|     --active-item-border-color: transparent; | ||||
| 
 | ||||
|     --menu-text-color: white; | ||||
|     --menu-background-color: #222; | ||||
|  | ||||
| @ -41,10 +41,12 @@ html { | ||||
|     --input-background-color: transparent; | ||||
| 
 | ||||
|     --hover-item-text-color: black; | ||||
|     --hover-item-background-color: #ddd; | ||||
|     --hover-item-background-color: transparent; | ||||
|     --hover-item-border-color: #ccc; | ||||
| 
 | ||||
|     --active-item-text-color: black; | ||||
|     --active-item-background-color: #ddd; | ||||
|     --active-item-border-color: transparent; | ||||
| 
 | ||||
|     --menu-text-color: black; | ||||
|     --menu-background-color: white; | ||||
|  | ||||
| @ -94,13 +94,13 @@ function undeleteNote(req) { | ||||
| 
 | ||||
| function sortChildNotes(req) { | ||||
|     const noteId = req.params.noteId; | ||||
|     const {sortBy, sortDirection} = req.body; | ||||
|     const {sortBy, sortDirection, foldersFirst} = req.body; | ||||
| 
 | ||||
|     log.info(`Sorting '${noteId}' children with ${sortBy} ${sortDirection}`); | ||||
|     log.info(`Sorting '${noteId}' children with ${sortBy} ${sortDirection}, foldersFirst=${foldersFirst}`); | ||||
| 
 | ||||
|     const reverse = sortDirection === 'desc'; | ||||
| 
 | ||||
|     treeService.sortNotes(noteId, sortBy, reverse); | ||||
|     treeService.sortNotes(noteId, sortBy, reverse, foldersFirst); | ||||
| } | ||||
| 
 | ||||
| function protectNote(req) { | ||||
|  | ||||
| @ -5,9 +5,7 @@ const SearchContext = require('../../services/search/search_context'); | ||||
| const log = require('../../services/log'); | ||||
| const scriptService = require('../../services/script'); | ||||
| const searchService = require('../../services/search/services/search'); | ||||
| const noteRevisionService = require("../../services/note_revisions"); | ||||
| const branchService = require("../../services/branches"); | ||||
| const cloningService = require("../../services/cloning"); | ||||
| const bulkActionService = require("../../services/bulk_actions"); | ||||
| const {formatAttrForSearch} = require("../../services/attribute_formatter"); | ||||
| const utils = require("../../services/utils.js"); | ||||
| 
 | ||||
| @ -60,98 +58,6 @@ async function searchFromNote(req) { | ||||
|     return await searchFromNoteInt(note); | ||||
| } | ||||
| 
 | ||||
| const ACTION_HANDLERS = { | ||||
|     deleteNote: (action, note) => { | ||||
|         const deleteId = 'searchbulkaction-' + utils.randomString(10); | ||||
| 
 | ||||
|         note.deleteNote(deleteId); | ||||
|     }, | ||||
|     deleteNoteRevisions: (action, note) => { | ||||
|         noteRevisionService.eraseNoteRevisions(note.getNoteRevisions().map(rev => rev.noteRevisionId)); | ||||
|     }, | ||||
|     deleteLabel: (action, note) => { | ||||
|         for (const label of note.getOwnedLabels(action.labelName)) { | ||||
|             label.markAsDeleted(); | ||||
|         } | ||||
|     }, | ||||
|     deleteRelation: (action, note) => { | ||||
|         for (const relation of note.getOwnedRelations(action.relationName)) { | ||||
|             relation.markAsDeleted(); | ||||
|         } | ||||
|     }, | ||||
|     renameLabel: (action, note) => { | ||||
|         for (const label of note.getOwnedLabels(action.oldLabelName)) { | ||||
|             label.name = action.newLabelName; | ||||
|             label.save(); | ||||
|         } | ||||
|     }, | ||||
|     renameRelation: (action, note) => { | ||||
|         for (const relation of note.getOwnedRelations(action.oldRelationName)) { | ||||
|             relation.name = action.newRelationName; | ||||
|             relation.save(); | ||||
|         } | ||||
|     }, | ||||
|     setLabelValue: (action, note) => { | ||||
|         note.setLabel(action.labelName, action.labelValue); | ||||
|     }, | ||||
|     setRelationTarget: (action, note) => { | ||||
|         note.setRelation(action.relationName, action.targetNoteId); | ||||
|     }, | ||||
|     moveNote: (action, note) => { | ||||
|         const targetParentNote = becca.getNote(action.targetParentNoteId); | ||||
| 
 | ||||
|         if (!targetParentNote) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let res; | ||||
| 
 | ||||
|         if (note.getParentBranches().length > 1) { | ||||
|             res = cloningService.cloneNoteToNote(note.noteId, action.targetParentNoteId); | ||||
|         } | ||||
|         else { | ||||
|             res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId); | ||||
|         } | ||||
| 
 | ||||
|         if (!res.success) { | ||||
|             log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`); | ||||
|         } | ||||
|     }, | ||||
|     executeScript: (action, note) => { | ||||
|         if (!action.script || !action.script.trim()) { | ||||
|             log.info("Ignoring executeScript since the script is empty.") | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const scriptFunc = new Function("note", action.script); | ||||
|         scriptFunc(note); | ||||
| 
 | ||||
|         note.save(); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| function getActions(note) { | ||||
|     return note.getLabels('action') | ||||
|         .map(actionLabel => { | ||||
|             let action; | ||||
| 
 | ||||
|             try { | ||||
|                 action = JSON.parse(actionLabel.value); | ||||
|             } catch (e) { | ||||
|                 log.error(`Cannot parse '${actionLabel.value}' into search action, skipping.`); | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             if (!(action.name in ACTION_HANDLERS)) { | ||||
|                 log.error(`Cannot find '${action.name}' search action handler, skipping.`); | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             return action; | ||||
|         }) | ||||
|         .filter(a => !!a); | ||||
| } | ||||
| 
 | ||||
| function searchAndExecute(req) { | ||||
|     const note = becca.getNote(req.params.noteId); | ||||
| 
 | ||||
| @ -170,26 +76,7 @@ function searchAndExecute(req) { | ||||
| 
 | ||||
|     const searchResultNoteIds = searchFromNoteInt(note); | ||||
| 
 | ||||
|     const actions = getActions(note); | ||||
| 
 | ||||
|     for (const resultNoteId of searchResultNoteIds) { | ||||
|         const resultNote = becca.getNote(resultNoteId); | ||||
| 
 | ||||
|         if (!resultNote || resultNote.isDeleted) { | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         for (const action of actions) { | ||||
|             try { | ||||
|                 log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`); | ||||
| 
 | ||||
|                 ACTION_HANDLERS[action.name](action, resultNote); | ||||
|             } | ||||
|             catch (e) { | ||||
|                 log.error(`ExecuteScript search action failed with ${e.message}`); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     bulkActionService.executeActions(note, searchResultNoteIds); | ||||
| } | ||||
| 
 | ||||
| function searchFromRelation(note, relationName) { | ||||
| @ -296,10 +183,20 @@ function getRelatedNotes(req) { | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| function searchTemplates() { | ||||
|     const query = formatAttrForSearch({type: 'label', name: "template"}, false); | ||||
| 
 | ||||
|     return searchService.searchNotes(query, { | ||||
|         includeArchivedNotes: true, | ||||
|         ignoreHoistedNote: false | ||||
|     }).map(note => note.noteId); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|     searchFromNote, | ||||
|     searchAndExecute, | ||||
|     getRelatedNotes, | ||||
|     quickSearch, | ||||
|     search | ||||
|     search, | ||||
|     searchTemplates | ||||
| }; | ||||
|  | ||||
| @ -355,6 +355,7 @@ function register(app) { | ||||
|     apiRoute(POST, '/api/search-and-execute-note/:noteId', searchRoute.searchAndExecute); | ||||
|     apiRoute(POST, '/api/search-related', searchRoute.getRelatedNotes); | ||||
|     apiRoute(GET, '/api/search/:searchString', searchRoute.search); | ||||
|     apiRoute(GET, '/api/search-templates', searchRoute.searchTemplates); | ||||
| 
 | ||||
|     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)
 | ||||
|  | ||||
| @ -4,7 +4,7 @@ const build = require('./build'); | ||||
| const packageJson = require('../../package'); | ||||
| const {TRILIUM_DATA_DIR} = require('./data_dir'); | ||||
| 
 | ||||
| const APP_DB_VERSION = 195; | ||||
| const APP_DB_VERSION = 196; | ||||
| const SYNC_VERSION = 25; | ||||
| const CLIPPER_PROTOCOL_VERSION = "1.0"; | ||||
| 
 | ||||
|  | ||||
| @ -51,6 +51,7 @@ module.exports = [ | ||||
|     { type: 'label', name: 'displayRelations' }, | ||||
|     { type: 'label', name: 'hideRelations' }, | ||||
|     { type: 'label', name: 'titleTemplate', isDangerous: true }, | ||||
|     { type: 'label', name: 'template' }, | ||||
| 
 | ||||
|     // relation names
 | ||||
|     { type: 'relation', name: 'internalLink' }, | ||||
|  | ||||
							
								
								
									
										142
									
								
								src/services/bulk_actions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/services/bulk_actions.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,142 @@ | ||||
| const log = require("./log"); | ||||
| const noteRevisionService = require("./note_revisions"); | ||||
| const becca = require("../becca/becca"); | ||||
| const cloningService = require("./cloning"); | ||||
| const branchService = require("./branches"); | ||||
| const utils = require("./utils"); | ||||
| 
 | ||||
| const ACTION_HANDLERS = { | ||||
|     addLabel: (action, note) => { | ||||
|         note.addLabel(action.labelName, action.labelValue); | ||||
|     }, | ||||
|     addRelation: (action, note) => { | ||||
|         note.addRelation(action.relationName, action.targetNoteId); | ||||
|     }, | ||||
|     deleteNote: (action, note) => { | ||||
|         const deleteId = 'searchbulkaction-' + utils.randomString(10); | ||||
| 
 | ||||
|         note.deleteNote(deleteId); | ||||
|     }, | ||||
|     deleteNoteRevisions: (action, note) => { | ||||
|         noteRevisionService.eraseNoteRevisions(note.getNoteRevisions().map(rev => rev.noteRevisionId)); | ||||
|     }, | ||||
|     deleteLabel: (action, note) => { | ||||
|         for (const label of note.getOwnedLabels(action.labelName)) { | ||||
|             label.markAsDeleted(); | ||||
|         } | ||||
|     }, | ||||
|     deleteRelation: (action, note) => { | ||||
|         for (const relation of note.getOwnedRelations(action.relationName)) { | ||||
|             relation.markAsDeleted(); | ||||
|         } | ||||
|     }, | ||||
|     renameLabel: (action, note) => { | ||||
|         for (const label of note.getOwnedLabels(action.oldLabelName)) { | ||||
|             // attribute name is immutable, renaming means delete old + create new
 | ||||
|             const newLabel = label.createClone('label', action.newLabelName, label.value); | ||||
| 
 | ||||
|             newLabel.save(); | ||||
|             label.markAsDeleted(); | ||||
|         } | ||||
|     }, | ||||
|     renameRelation: (action, note) => { | ||||
|         for (const relation of note.getOwnedRelations(action.oldRelationName)) { | ||||
|             // attribute name is immutable, renaming means delete old + create new
 | ||||
|             const newRelation = relation.createClone('relation', action.newRelationName, relation.value); | ||||
| 
 | ||||
|             newRelation.save(); | ||||
|             relation.markAsDeleted(); | ||||
|         } | ||||
|     }, | ||||
|     updateLabelValue: (action, note) => { | ||||
|         for (const label of note.getOwnedLabels(action.labelName)) { | ||||
|             label.value = action.labelValue; | ||||
|             label.save(); | ||||
|         } | ||||
|     }, | ||||
|     updateRelationTarget: (action, note) => { | ||||
|         for (const relation of note.getOwnedLabels(action.relationName)) { | ||||
|             relation.value = action.targetNoteId; | ||||
|             relation.save(); | ||||
|         } | ||||
|     }, | ||||
|     moveNote: (action, note) => { | ||||
|         const targetParentNote = becca.getNote(action.targetParentNoteId); | ||||
| 
 | ||||
|         if (!targetParentNote) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let res; | ||||
| 
 | ||||
|         if (note.getParentBranches().length > 1) { | ||||
|             res = cloningService.cloneNoteToNote(note.noteId, action.targetParentNoteId); | ||||
|         } | ||||
|         else { | ||||
|             res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId); | ||||
|         } | ||||
| 
 | ||||
|         if (!res.success) { | ||||
|             log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`); | ||||
|         } | ||||
|     }, | ||||
|     executeScript: (action, note) => { | ||||
|         if (!action.script || !action.script.trim()) { | ||||
|             log.info("Ignoring executeScript since the script is empty.") | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const scriptFunc = new Function("note", action.script); | ||||
|         scriptFunc(note); | ||||
| 
 | ||||
|         note.save(); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| function getActions(note) { | ||||
|     return note.getLabels('action') | ||||
|         .map(actionLabel => { | ||||
|             let action; | ||||
| 
 | ||||
|             try { | ||||
|                 action = JSON.parse(actionLabel.value); | ||||
|             } catch (e) { | ||||
|                 log.error(`Cannot parse '${actionLabel.value}' into search action, skipping.`); | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             if (!(action.name in ACTION_HANDLERS)) { | ||||
|                 log.error(`Cannot find '${action.name}' search action handler, skipping.`); | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             return action; | ||||
|         }) | ||||
|         .filter(a => !!a); | ||||
| } | ||||
| 
 | ||||
| function executeActions(note, searchResultNoteIds) { | ||||
|     const actions = getActions(note); | ||||
| 
 | ||||
|     for (const resultNoteId of searchResultNoteIds) { | ||||
|         const resultNote = becca.getNote(resultNoteId); | ||||
| 
 | ||||
|         if (!resultNote || resultNote.isDeleted) { | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         for (const action of actions) { | ||||
|             try { | ||||
|                 log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`); | ||||
| 
 | ||||
|                 ACTION_HANDLERS[action.name](action, resultNote); | ||||
|             } catch (e) { | ||||
|                 log.error(`ExecuteScript search action failed with ${e.message}`); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|     executeActions | ||||
| }; | ||||
| @ -1,13 +1,14 @@ | ||||
| module.exports = [ | ||||
|     'text',  | ||||
|     'code',  | ||||
|     'render',  | ||||
|     'file',  | ||||
|     'image',  | ||||
|     'search',  | ||||
|     'relation-map',  | ||||
|     'book',  | ||||
|     'text', | ||||
|     'code', | ||||
|     'render', | ||||
|     'file', | ||||
|     'image', | ||||
|     'search', | ||||
|     'relation-map', | ||||
|     'book', | ||||
|     'note-map', | ||||
|     'mermaid', | ||||
|     'canvas' | ||||
| ]; | ||||
|     'canvas', | ||||
|     'web-view' | ||||
| ]; | ||||
|  | ||||
| @ -55,7 +55,7 @@ function deriveMime(type, mime) { | ||||
|         mime = 'text/plain'; | ||||
|     } else if (['relation-map', 'search', 'canvas'].includes(type)) { | ||||
|         mime = 'application/json'; | ||||
|     } else if (['render', 'book'].includes(type)) { | ||||
|     } else if (['render', 'book', 'iframe'].includes(type)) { | ||||
|         mime = ''; | ||||
|     } else { | ||||
|         mime = 'application/octet-stream'; | ||||
| @ -155,6 +155,14 @@ function createNewNote(params) { | ||||
| 
 | ||||
|         scanForLinks(note); | ||||
| 
 | ||||
|         if (params.templateNoteId) { | ||||
|             if (!becca.getNote(params.templateNoteId)) { | ||||
|                 throw new Error(`Template note '${params.templateNoteId}' does not exist.`); | ||||
|             } | ||||
| 
 | ||||
|             note.addRelation('template', params.templateNoteId); | ||||
|         } | ||||
| 
 | ||||
|         copyChildAttributes(parentNote, note); | ||||
| 
 | ||||
|         triggerNoteTitleChanged(note); | ||||
|  | ||||
| @ -219,10 +219,28 @@ function getShareRoot() { | ||||
|     return shareRoot; | ||||
| } | ||||
| 
 | ||||
| function getBulkActionNote() { | ||||
|     let bulkActionNote = becca.getNote('bulkaction'); | ||||
| 
 | ||||
|     if (!bulkActionNote) { | ||||
|         bulkActionNote = noteService.createNewNote({ | ||||
|             branchId: 'bulkaction', | ||||
|             noteId: 'bulkaction', | ||||
|             title: 'Bulk action', | ||||
|             type: 'text', | ||||
|             content: '', | ||||
|             parentNoteId: getHiddenRoot().noteId | ||||
|         }).note; | ||||
|     } | ||||
| 
 | ||||
|     return bulkActionNote; | ||||
| } | ||||
| 
 | ||||
| function createMissingSpecialNotes() { | ||||
|     getSinglesNoteRoot(); | ||||
|     getSqlConsoleRoot(); | ||||
|     getGlobalNoteMap(); | ||||
|     getBulkActionNote(); | ||||
|     // share root is not automatically created since it's visible in the tree and many won't need it/use it
 | ||||
| 
 | ||||
|     const hidden = getHiddenRoot(); | ||||
| @ -239,5 +257,6 @@ module.exports = { | ||||
|     createSearchNote, | ||||
|     saveSearchNote, | ||||
|     createMissingSpecialNotes, | ||||
|     getShareRoot | ||||
|     getShareRoot, | ||||
|     getBulkActionNote, | ||||
| }; | ||||
|  | ||||
| @ -67,7 +67,8 @@ async function createMainWindow() { | ||||
|             enableRemoteModule: true, | ||||
|             nodeIntegration: true, | ||||
|             contextIsolation: false, | ||||
|             spellcheck: spellcheckEnabled | ||||
|             spellcheck: spellcheckEnabled, | ||||
|             webviewTag: true | ||||
|         }, | ||||
|         frame: optionService.getOptionBool('nativeTitleBarVisible'), | ||||
|         icon: getIcon() | ||||
|  | ||||
| @ -92,7 +92,7 @@ document.addEventListener("DOMContentLoaded", function() { | ||||
|         header += `<script src="../../node_modules/react/umd/react.production.min.js"></script>`; | ||||
|         header += `<script src="../../node_modules/react-dom/umd/react-dom.production.min.js"></script>`; | ||||
|         header += `<script src="../../node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"></script>`; | ||||
|         header += `<style type="text/css">
 | ||||
|         header += `<style>
 | ||||
| 
 | ||||
|             .excalidraw-wrapper { | ||||
|                 height: 100%; | ||||
|  | ||||
| @ -40,6 +40,8 @@ | ||||
| <%- include('dialogs/sort_child_notes.ejs') %> | ||||
| <%- include('dialogs/delete_notes.ejs') %> | ||||
| <%- include('dialogs/password_not_set.ejs') %> | ||||
| <%- include('dialogs/bulk_assign_attributes.ejs') %> | ||||
| <%- include('dialogs/note_type_chooser.ejs') %> | ||||
| 
 | ||||
| <script type="text/javascript"> | ||||
|     global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */ | ||||
|  | ||||
| @ -35,6 +35,7 @@ | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div class="form-group" id="add-link-title-form-group"> | ||||
|                             <br/> | ||||
|                             <label for="link-title">Link title</label> | ||||
|                             <input id="link-title" class="form-control" style="width: 100%;"> | ||||
|                         </div> | ||||
|  | ||||
							
								
								
									
										40
									
								
								src/views/dialogs/bulk_assign_attributes.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/views/dialogs/bulk_assign_attributes.ejs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| <style> | ||||
|     #bulk-available-action-list button { | ||||
|         padding: 2px 7px; | ||||
|         margin-right: 10px; | ||||
|         margin-bottom: 5px; | ||||
|     } | ||||
| </style> | ||||
| 
 | ||||
| <div id="bulk-assign-attributes-dialog" class="modal mx-auto" tabindex="-1" role="dialog"> | ||||
|     <div class="modal-dialog modal-lg" style="max-width: 1000px" role="document"> | ||||
|         <div class="modal-content"> | ||||
|             <div class="modal-header"> | ||||
|                 <h5 class="modal-title mr-auto">Bulk assign attributes</h5> | ||||
| 
 | ||||
|                 <button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;"> | ||||
|                     <span aria-hidden="true">×</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <div class="modal-body"> | ||||
|                 Affected notes: <span id="affected-note-count">0</span> | ||||
| 
 | ||||
|                 <div class="form-check"> | ||||
|                     <input class="form-check-input" type="checkbox" value="" id="include-descendants"> | ||||
|                     <label class="form-check-label" for="include-descendants"> | ||||
|                         Include descendant notes | ||||
|                     </label> | ||||
|                 </div> | ||||
| 
 | ||||
|                 Available actions: | ||||
| 
 | ||||
|                 <table id="bulk-available-action-list"></table> | ||||
| 
 | ||||
|                 <div id="bulk-existing-action-list"></div> | ||||
|             </div> | ||||
|             <div class="modal-footer"> | ||||
|                 <button type="submit" class="btn btn-primary">Execute bulk actions</button> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										34
									
								
								src/views/dialogs/note_type_chooser.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/views/dialogs/note_type_chooser.ejs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| <style> | ||||
|     #note-type-dropdown { | ||||
|         position: relative; | ||||
|         font-size: large; | ||||
|         padding: 20px; | ||||
|         width: 100%; | ||||
|         margin-top: 15px; | ||||
|         max-height: 80vh; | ||||
|         overflow: auto; | ||||
|     } | ||||
| </style> | ||||
| 
 | ||||
| <div id="note-type-chooser-dialog" class="modal mx-auto" tabindex="-1" role="dialog"> | ||||
|     <div class="modal-dialog" style="max-width: 500px;" role="document"> | ||||
|         <div class="modal-content"> | ||||
|             <div class="modal-header"> | ||||
|                 <h5 class="modal-title mr-auto">Choose note type</h5> | ||||
| 
 | ||||
|                 <button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;"> | ||||
|                     <span aria-hidden="true">×</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <div class="modal-body"> | ||||
|                 Choose note type / template of the new note: | ||||
| 
 | ||||
|                 <div class="dropdown"> | ||||
|                     <button id="note-type-dropdown-trigger" type="button" style="display: none;" data-toggle="dropdown">Dropdown trigger</button> | ||||
| 
 | ||||
|                     <div id="note-type-dropdown" class="dropdown-menu"></div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| @ -50,6 +50,17 @@ | ||||
|                             descending | ||||
|                         </label> | ||||
|                     </div> | ||||
| 
 | ||||
|                     <br /> | ||||
| 
 | ||||
|                     <h5>Folders</h5> | ||||
| 
 | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="checkbox" name="sort-folders-first" value="1" id="sort-folders-first"> | ||||
|                         <label class="form-check-label" for="sort-folders-first"> | ||||
|                             sort folders at the top | ||||
|                         </label> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|                     <button type="submit" class="btn btn-primary">Sort <kbd>enter</kbd></button> | ||||
|  | ||||
| @ -103,6 +103,7 @@ | ||||
| <div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div> | ||||
| 
 | ||||
| <%- include('dialogs/confirm.ejs') %> | ||||
| <%- include('dialogs/protected_session_password.ejs') %> | ||||
| 
 | ||||
| <script type="text/javascript"> | ||||
|     global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */ | ||||
| @ -116,7 +117,8 @@ | ||||
|         instanceName: '<%= instanceName %>', | ||||
|         csrfToken: '<%= csrfToken %>', | ||||
|         isDev: <%= isDev %>, | ||||
|         appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %> | ||||
|         appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>, | ||||
|         isProtectedSessionAvailable: <%= isProtectedSessionAvailable %> | ||||
|     }; | ||||
| </script> | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 zadam
						zadam