mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/develop' into ai-llm-integration
This commit is contained in:
		
							
								
								
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							| @@ -5,6 +5,7 @@ | |||||||
|     "vitest.explorer", |     "vitest.explorer", | ||||||
|     "ms-playwright.playwright", |     "ms-playwright.playwright", | ||||||
|     "tobermory.es6-string-html", |     "tobermory.es6-string-html", | ||||||
|     "dbaeumer.vscode-eslint" |     "dbaeumer.vscode-eslint", | ||||||
|  |     "yzhang.markdown-all-in-one" | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -25,6 +25,11 @@ keyPath= | |||||||
| # expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7) | # expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7) | ||||||
| trustedReverseProxy=false | trustedReverseProxy=false | ||||||
|  |  | ||||||
|  | # setting the CORS headers for cross-origin requests | ||||||
|  | # corsAllowOrigin='*' | ||||||
|  | # corsAllowMethods='GET,POST,PUT,DELETE,PATCH' | ||||||
|  | # corsAllowHeaders='Content-Type,Authorization' | ||||||
|  |  | ||||||
|  |  | ||||||
| [Session] | [Session] | ||||||
| # Use this setting to set a custom value for the "Max-Age" Attribute of the session cookie. | # Use this setting to set a custom value for the "Max-Age" Attribute of the session cookie. | ||||||
|   | |||||||
| @@ -1,7 +1,4 @@ | |||||||
| # v0.93.0 | # v0.93.0 | ||||||
| ## 💡 Key highlights |  | ||||||
|  |  | ||||||
| *   … |  | ||||||
|  |  | ||||||
| ## 🐞 Bugfixes | ## 🐞 Bugfixes | ||||||
|  |  | ||||||
| @@ -15,6 +12,10 @@ | |||||||
| *   [config.Session.cookieMaxAge is ignored](https://github.com/TriliumNext/Notes/issues/1709) by @pano9000 | *   [config.Session.cookieMaxAge is ignored](https://github.com/TriliumNext/Notes/issues/1709) by @pano9000 | ||||||
| *   [Return correct HTTP status code on failed login attempts instead of 200](https://github.com/TriliumNext/Notes/issues/1707) by @pano9000 | *   [Return correct HTTP status code on failed login attempts instead of 200](https://github.com/TriliumNext/Notes/issues/1707) by @pano9000 | ||||||
| *   [Calendar stops displaying notes after adding a Day Note](https://github.com/TriliumNext/Notes/issues/1705) | *   [Calendar stops displaying notes after adding a Day Note](https://github.com/TriliumNext/Notes/issues/1705) | ||||||
|  | *   Full anonymization not redacting attachment titles. | ||||||
|  | *   Unable to trigger "Move to" dialog via keyboard shortcut. | ||||||
|  | *   [Note ordering doesn't load correctly, only shows up right after moving a note](https://github.com/TriliumNext/Notes/issues/1727) | ||||||
|  | *   [Note selection dialog shows icon class when selecting result with arrow button (jump to note / create link)](https://github.com/TriliumNext/Notes/issues/1721) | ||||||
|  |  | ||||||
| ## ✨ Improvements | ## ✨ Improvements | ||||||
|  |  | ||||||
| @@ -23,6 +24,7 @@ | |||||||
|     *   Reduce extra whitespace between list items. |     *   Reduce extra whitespace between list items. | ||||||
|     *   Preserve include note. |     *   Preserve include note. | ||||||
|     *   Handle note titles that contain inline code. |     *   Handle note titles that contain inline code. | ||||||
|  |     *   Support to-do lists. | ||||||
| *   In-app help: | *   In-app help: | ||||||
|     *   Document structure is now precalculated, so start-up time should be slightly better. |     *   Document structure is now precalculated, so start-up time should be slightly better. | ||||||
|     *   Optimized the content in order to reduce the size on disk. |     *   Optimized the content in order to reduce the size on disk. | ||||||
| @@ -35,10 +37,8 @@ | |||||||
| *   Basic Touch Bar support for macOS. | *   Basic Touch Bar support for macOS. | ||||||
| *   [Support Bearer Token](https://github.com/TriliumNext/Notes/issues/1701) | *   [Support Bearer Token](https://github.com/TriliumNext/Notes/issues/1701) | ||||||
| *   The tab bar is now scrollable when there are many tabs by @SiriusXT | *   The tab bar is now scrollable when there are many tabs by @SiriusXT | ||||||
|  | *   Make each part of the note path clickable by @SiriusXT | ||||||
| ## 🌍 Internationalization | *   Allow setting CORS headers by @yiranlus | ||||||
|  |  | ||||||
| *   … |  | ||||||
|  |  | ||||||
| ## 📖 Documentation | ## 📖 Documentation | ||||||
|  |  | ||||||
| @@ -47,6 +47,5 @@ | |||||||
|  |  | ||||||
| ## 🛠️ Technical updates | ## 🛠️ Technical updates | ||||||
|  |  | ||||||
| *   upgrade to express 5.1.0 by @pano9000 |  | ||||||
| *   update dependency mind-elixir to v4.5.1 | *   update dependency mind-elixir to v4.5.1 | ||||||
| *   remove non-working cookiePath option by @pano9000 | *   remove non-working cookiePath option by @pano9000 | ||||||
| @@ -9968,6 +9968,26 @@ | |||||||
|                                     "format": "markdown", |                                     "format": "markdown", | ||||||
|                                     "dataFileName": "Trilium instance.md", |                                     "dataFileName": "Trilium instance.md", | ||||||
|                                     "attachments": [] |                                     "attachments": [] | ||||||
|  |                                 }, | ||||||
|  |                                 { | ||||||
|  |                                     "isClone": false, | ||||||
|  |                                     "noteId": "LWtBjFej3wX3", | ||||||
|  |                                     "notePath": [ | ||||||
|  |                                         "pOsGYCXsbNQG", | ||||||
|  |                                         "tC7s2alapj8V", | ||||||
|  |                                         "Gzjqa934BdH4", | ||||||
|  |                                         "LWtBjFej3wX3" | ||||||
|  |                                     ], | ||||||
|  |                                     "title": "Cross-Origin Resource Sharing (CORS)", | ||||||
|  |                                     "notePosition": 20, | ||||||
|  |                                     "prefix": null, | ||||||
|  |                                     "isExpanded": false, | ||||||
|  |                                     "type": "text", | ||||||
|  |                                     "mime": "text/html", | ||||||
|  |                                     "attributes": [], | ||||||
|  |                                     "format": "markdown", | ||||||
|  |                                     "dataFileName": "Cross-Origin Resource Sharing .md", | ||||||
|  |                                     "attachments": [] | ||||||
|                                 } |                                 } | ||||||
|                             ] |                             ] | ||||||
|                         }, |                         }, | ||||||
|   | |||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | # Cross-Origin Resource Sharing (CORS) | ||||||
|  | By default, Trilium cannot be accessed in web browsers by requests coming from other domains/origins than Trilium itself.  | ||||||
|  |  | ||||||
|  | However, it is possible to manually configure [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) since Trilium v0.93.0 using environment variables or `config.ini`, as follows: | ||||||
|  |  | ||||||
|  | <figure class="table" style="width:100%;"><table class="ck-table-resized"><colgroup><col style="width:26.93%;"><col style="width:32.46%;"><col style="width:40.61%;"></colgroup><thead><tr><th>CORS Header</th><th>Corresponding option in <code>config.ini</code></th><th>Corresponding option in environment variables in the <code>Network</code> section</th></tr></thead><tbody><tr><td><code>Access-Control-Allow-Origin</code></td><td><code>TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code></td><td><code>corsAllowOrigin</code> </td></tr><tr><td><code>Access-Control-Allow-Methods</code></td><td><code>TRILIUM_NETWORK_CORS_ALLOW_METHODS</code></td><td><code>corsAllowMethods</code> </td></tr><tr><td><code>Access-Control-Allow-Headers</code></td><td><code>TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code></td><td><code>corsAllowHeaders</code></td></tr></tbody></table></figure> | ||||||
							
								
								
									
										20
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										20
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,12 +1,12 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "version": "0.92.7", |   "version": "0.93.0", | ||||||
|   "lockfileVersion": 3, |   "lockfileVersion": 3, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "packages": { |   "packages": { | ||||||
|     "": { |     "": { | ||||||
|       "name": "trilium", |       "name": "trilium", | ||||||
|       "version": "0.92.7", |       "version": "0.93.0", | ||||||
|       "license": "AGPL-3.0-only", |       "license": "AGPL-3.0-only", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@anthropic-ai/sdk": "0.39.0", |         "@anthropic-ai/sdk": "0.39.0", | ||||||
| @@ -50,7 +50,7 @@ | |||||||
|         "html2plaintext": "2.1.4", |         "html2plaintext": "2.1.4", | ||||||
|         "http-proxy-agent": "7.0.2", |         "http-proxy-agent": "7.0.2", | ||||||
|         "https-proxy-agent": "7.0.6", |         "https-proxy-agent": "7.0.6", | ||||||
|         "i18next": "24.2.3", |         "i18next": "25.0.0", | ||||||
|         "i18next-fs-backend": "2.6.0", |         "i18next-fs-backend": "2.6.0", | ||||||
|         "image-type": "5.2.0", |         "image-type": "5.2.0", | ||||||
|         "ini": "5.0.0", |         "ini": "5.0.0", | ||||||
| @@ -73,7 +73,7 @@ | |||||||
|         "rand-token": "1.0.1", |         "rand-token": "1.0.1", | ||||||
|         "safe-compare": "1.1.4", |         "safe-compare": "1.1.4", | ||||||
|         "sanitize-filename": "1.6.3", |         "sanitize-filename": "1.6.3", | ||||||
|         "sanitize-html": "2.15.0", |         "sanitize-html": "2.16.0", | ||||||
|         "sax": "1.4.1", |         "sax": "1.4.1", | ||||||
|         "serve-favicon": "2.5.0", |         "serve-favicon": "2.5.0", | ||||||
|         "session-file-store": "1.5.0", |         "session-file-store": "1.5.0", | ||||||
| @@ -12742,9 +12742,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/i18next": { |     "node_modules/i18next": { | ||||||
|       "version": "24.2.3", |       "version": "25.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", |       "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.0.0.tgz", | ||||||
|       "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==", |       "integrity": "sha512-POPvwjOPR1GQvRnbikTMPEhQD+ekd186MHE6NtVxl3Lby+gPp0iq60eCqGrY6wfRnp1lejjFNu0EKs1afA322w==", | ||||||
|       "funding": [ |       "funding": [ | ||||||
|         { |         { | ||||||
|           "type": "individual", |           "type": "individual", | ||||||
| @@ -18324,9 +18324,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/sanitize-html": { |     "node_modules/sanitize-html": { | ||||||
|       "version": "2.15.0", |       "version": "2.16.0", | ||||||
|       "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.15.0.tgz", |       "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.16.0.tgz", | ||||||
|       "integrity": "sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==", |       "integrity": "sha512-0s4caLuHHaZFVxFTG74oW91+j6vW7gKbGD6CD2+miP73CE6z6YtOBN0ArtLd2UGyi4IC7K47v3ENUbQX4jV3Mg==", | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "deepmerge": "^4.2.2", |         "deepmerge": "^4.2.2", | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "productName": "TriliumNext Notes", |   "productName": "TriliumNext Notes", | ||||||
|   "description": "Build your personal knowledge base with TriliumNext Notes", |   "description": "Build your personal knowledge base with TriliumNext Notes", | ||||||
|   "version": "0.92.7", |   "version": "0.93.0", | ||||||
|   "license": "AGPL-3.0-only", |   "license": "AGPL-3.0-only", | ||||||
|   "main": "./electron-main.js", |   "main": "./electron-main.js", | ||||||
|   "author": { |   "author": { | ||||||
| @@ -110,7 +110,7 @@ | |||||||
|     "html2plaintext": "2.1.4", |     "html2plaintext": "2.1.4", | ||||||
|     "http-proxy-agent": "7.0.2", |     "http-proxy-agent": "7.0.2", | ||||||
|     "https-proxy-agent": "7.0.6", |     "https-proxy-agent": "7.0.6", | ||||||
|     "i18next": "24.2.3", |     "i18next": "25.0.0", | ||||||
|     "i18next-fs-backend": "2.6.0", |     "i18next-fs-backend": "2.6.0", | ||||||
|     "image-type": "5.2.0", |     "image-type": "5.2.0", | ||||||
|     "ini": "5.0.0", |     "ini": "5.0.0", | ||||||
| @@ -133,7 +133,7 @@ | |||||||
|     "rand-token": "1.0.1", |     "rand-token": "1.0.1", | ||||||
|     "safe-compare": "1.1.4", |     "safe-compare": "1.1.4", | ||||||
|     "sanitize-filename": "1.6.3", |     "sanitize-filename": "1.6.3", | ||||||
|     "sanitize-html": "2.15.0", |     "sanitize-html": "2.16.0", | ||||||
|     "sax": "1.4.1", |     "sax": "1.4.1", | ||||||
|     "serve-favicon": "2.5.0", |     "serve-favicon": "2.5.0", | ||||||
|     "session-file-store": "1.5.0", |     "session-file-store": "1.5.0", | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								src/app.ts
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/app.ts
									
									
									
									
									
								
							| @@ -7,6 +7,7 @@ import compression from "compression"; | |||||||
| import { fileURLToPath } from "url"; | import { fileURLToPath } from "url"; | ||||||
| import { dirname } from "path"; | import { dirname } from "path"; | ||||||
| import sessionParser from "./routes/session_parser.js"; | import sessionParser from "./routes/session_parser.js"; | ||||||
|  | import config from "./services/config.js"; | ||||||
| import utils from "./services/utils.js"; | import utils from "./services/utils.js"; | ||||||
| import assets from "./routes/assets.js"; | import assets from "./routes/assets.js"; | ||||||
| import routes from "./routes/routes.js"; | import routes from "./routes/routes.js"; | ||||||
| @@ -71,6 +72,17 @@ app.set("views", path.join(scriptDir, "views")); | |||||||
| app.set("view engine", "ejs"); | app.set("view engine", "ejs"); | ||||||
|  |  | ||||||
| app.use((req, res, next) => { | app.use((req, res, next) => { | ||||||
|  |     // set CORS header | ||||||
|  |     if (config["Network"]["corsAllowOrigin"]) { | ||||||
|  |         res.header("Access-Control-Allow-Origin", config["Network"]["corsAllowOrigin"]); | ||||||
|  |     } | ||||||
|  |     if (config["Network"]["corsAllowMethods"]) { | ||||||
|  |         res.header("Access-Control-Allow-Methods", config["Network"]["corsAllowMethods"]); | ||||||
|  |     } | ||||||
|  |     if (config["Network"]["corsAllowHeaders"]) { | ||||||
|  |         res.header("Access-Control-Allow-Headers", config["Network"]["corsAllowHeaders"]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     res.locals.t = t; |     res.locals.t = t; | ||||||
|     return next(); |     return next(); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -53,8 +53,8 @@ export interface ContextMenuCommandData extends CommandData { | |||||||
|     node: Fancytree.FancytreeNode; |     node: Fancytree.FancytreeNode; | ||||||
|     notePath?: string; |     notePath?: string; | ||||||
|     noteId?: string; |     noteId?: string; | ||||||
|     selectedOrActiveBranchIds?: any; // TODO: Remove any once type is defined |     selectedOrActiveBranchIds: string[]; | ||||||
|     selectedOrActiveNoteIds: any; // TODO: Remove  any once type is defined |     selectedOrActiveNoteIds?: string[]; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface NoteCommandData extends CommandData { | export interface NoteCommandData extends CommandData { | ||||||
|   | |||||||
| @@ -18,10 +18,26 @@ export default class MainTreeExecutors extends Component { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async cloneNotesToCommand({ selectedOrActiveNoteIds }: EventData<"cloneNotesTo">) { |     async cloneNotesToCommand({ selectedOrActiveNoteIds }: EventData<"cloneNotesTo">) { | ||||||
|  |         if (!selectedOrActiveNoteIds && this.tree) { | ||||||
|  |             selectedOrActiveNoteIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.noteId); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!selectedOrActiveNoteIds) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         this.triggerCommand("cloneNoteIdsTo", { noteIds: selectedOrActiveNoteIds }); |         this.triggerCommand("cloneNoteIdsTo", { noteIds: selectedOrActiveNoteIds }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async moveNotesToCommand({ selectedOrActiveBranchIds }: EventData<"moveNotesTo">) { |     async moveNotesToCommand({ selectedOrActiveBranchIds }: EventData<"moveNotesTo">) { | ||||||
|  |         if (!selectedOrActiveBranchIds && this.tree) { | ||||||
|  |             selectedOrActiveBranchIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.branchId); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!selectedOrActiveBranchIds) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         this.triggerCommand("moveBranchIdsTo", { branchIds: selectedOrActiveBranchIds }); |         this.triggerCommand("moveBranchIdsTo", { branchIds: selectedOrActiveBranchIds }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | <p>By default, Trilium cannot be accessed in web browsers by requests coming | ||||||
|  |   from other domains/origins than Trilium itself. </p> | ||||||
|  | <p>However, it is possible to manually configure <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">Cross-Origin Resource Sharing (CORS)</a> since | ||||||
|  |   Trilium v0.93.0 using environment variables or <code>config.ini</code>, | ||||||
|  |   as follows:</p> | ||||||
|  | <figure class="table" style="width:100%;"> | ||||||
|  |   <table class="ck-table-resized"> | ||||||
|  |     <colgroup> | ||||||
|  |       <col style="width:26.93%;"> | ||||||
|  |         <col style="width:32.46%;"> | ||||||
|  |           <col style="width:40.61%;"> | ||||||
|  |     </colgroup> | ||||||
|  |     <thead> | ||||||
|  |       <tr> | ||||||
|  |         <th>CORS Header</th> | ||||||
|  |         <th>Corresponding option in <code>config.ini</code> | ||||||
|  |         </th> | ||||||
|  |         <th>Corresponding option in environment variables in the <code>Network</code> section</th> | ||||||
|  |       </tr> | ||||||
|  |     </thead> | ||||||
|  |     <tbody> | ||||||
|  |       <tr> | ||||||
|  |         <td><code>Access-Control-Allow-Origin</code> | ||||||
|  |         </td> | ||||||
|  |         <td><code>TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code> | ||||||
|  |         </td> | ||||||
|  |         <td><code>corsAllowOrigin</code> </td> | ||||||
|  |       </tr> | ||||||
|  |       <tr> | ||||||
|  |         <td><code>Access-Control-Allow-Methods</code> | ||||||
|  |         </td> | ||||||
|  |         <td><code>TRILIUM_NETWORK_CORS_ALLOW_METHODS</code> | ||||||
|  |         </td> | ||||||
|  |         <td><code>corsAllowMethods</code> </td> | ||||||
|  |       </tr> | ||||||
|  |       <tr> | ||||||
|  |         <td><code>Access-Control-Allow-Headers</code> | ||||||
|  |         </td> | ||||||
|  |         <td><code>TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code> | ||||||
|  |         </td> | ||||||
|  |         <td><code>corsAllowHeaders</code> | ||||||
|  |         </td> | ||||||
|  |       </tr> | ||||||
|  |     </tbody> | ||||||
|  |   </table> | ||||||
|  | </figure> | ||||||
| @@ -152,7 +152,7 @@ class FNote { | |||||||
|  |  | ||||||
|         for (const branchId of Object.values(this.childToBranch)) { |         for (const branchId of Object.values(this.childToBranch)) { | ||||||
|             const notePosition = this.froca.getBranch(branchId)?.notePosition; |             const notePosition = this.froca.getBranch(branchId)?.notePosition; | ||||||
|             if (notePosition) { |             if (notePosition !== undefined) { | ||||||
|                 branchIdPos[branchId] = notePosition; |                 branchIdPos[branchId] = notePosition; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -278,15 +278,20 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | |||||||
|     const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink); |     const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink); | ||||||
|  |  | ||||||
|     const ctrlKey = utils.isCtrlKey(evt); |     const ctrlKey = utils.isCtrlKey(evt); | ||||||
|  |     const shiftKey = evt.shiftKey; | ||||||
|     const isLeftClick = "which" in evt && evt.which === 1; |     const isLeftClick = "which" in evt && evt.which === 1; | ||||||
|     const isMiddleClick = "which" in evt && evt.which === 2; |     const isMiddleClick = "which" in evt && evt.which === 2; | ||||||
|     const targetIsBlank = ($link?.attr("target") === "_blank"); |     const targetIsBlank = ($link?.attr("target") === "_blank"); | ||||||
|     const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank; |     const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank; | ||||||
|  |     const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey); | ||||||
|  |     const openInNewWindow = isLeftClick && evt.shiftKey && !ctrlKey; | ||||||
|  |  | ||||||
|     if (notePath) { |     if (notePath) { | ||||||
|         if (openInNewTab) { |         if (openInNewWindow) { | ||||||
|  |             appContext.triggerCommand("openInWindow", { notePath, viewScope }); | ||||||
|  |         } else if (openInNewTab) { | ||||||
|             appContext.tabManager.openTabWithNoteWithHoisting(notePath, { |             appContext.tabManager.openTabWithNoteWithHoisting(notePath, { | ||||||
|                 activate: targetIsBlank, |                 activate: activate ? true : targetIsBlank, | ||||||
|                 viewScope |                 viewScope | ||||||
|             }); |             }); | ||||||
|         } else if (isLeftClick) { |         } else if (isLeftClick) { | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ function getSearchDelay(notesCount: number): number { | |||||||
| } | } | ||||||
| let searchDelay = getSearchDelay(notesCount); | let searchDelay = getSearchDelay(notesCount); | ||||||
|  |  | ||||||
|  | // TODO: Deduplicate with server. | ||||||
| export interface Suggestion { | export interface Suggestion { | ||||||
|     noteTitle?: string; |     noteTitle?: string; | ||||||
|     externalLink?: string; |     externalLink?: string; | ||||||
| @@ -29,6 +30,7 @@ export interface Suggestion { | |||||||
|     highlightedNotePathTitle?: string; |     highlightedNotePathTitle?: string; | ||||||
|     action?: string | "create-note" | "search-notes" | "external-link"; |     action?: string | "create-note" | "search-notes" | "external-link"; | ||||||
|     parentNoteId?: string; |     parentNoteId?: string; | ||||||
|  |     icon?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface Options { | interface Options { | ||||||
| @@ -262,7 +264,7 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) { | |||||||
|                 }, |                 }, | ||||||
|                 displayKey: "notePathTitle", |                 displayKey: "notePathTitle", | ||||||
|                 templates: { |                 templates: { | ||||||
|                     suggestion: (suggestion) => suggestion.highlightedNotePathTitle |                     suggestion: (suggestion) => `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}` | ||||||
|                 }, |                 }, | ||||||
|                 // we can't cache identical searches because notes can be created / renamed, new recent notes can be added |                 // we can't cache identical searches because notes can be created / renamed, new recent notes can be added | ||||||
|                 cache: false |                 cache: false | ||||||
|   | |||||||
| @@ -19,15 +19,15 @@ const TPL = /*html*/` | |||||||
|         margin-top: 10px; |         margin-top: 10px; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     .note-path-list .path-current { |     .note-path-list .path-current a { | ||||||
|         font-weight: bold; |         font-weight: bold; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     .note-path-list .path-archived { |     .note-path-list .path-archived a { | ||||||
|         color: var(--muted-text-color) !important; |         color: var(--muted-text-color) !important; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     .note-path-list .path-search { |     .note-path-list .path-search a { | ||||||
|         font-style: italic; |         font-style: italic; | ||||||
|     } |     } | ||||||
|     </style> |     </style> | ||||||
| @@ -72,7 +72,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget { | |||||||
|         this.$notePathList.empty(); |         this.$notePathList.empty(); | ||||||
|  |  | ||||||
|         if (!this.note || this.noteId === "root") { |         if (!this.note || this.noteId === "root") { | ||||||
|             this.$notePathList.empty().append(await this.getRenderedPath("root")); |             this.$notePathList.empty().append(await this.getRenderedPath(["root"])); | ||||||
|  |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @@ -88,7 +88,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget { | |||||||
|         const renderedPaths = []; |         const renderedPaths = []; | ||||||
|  |  | ||||||
|         for (const notePathRecord of sortedNotePaths) { |         for (const notePathRecord of sortedNotePaths) { | ||||||
|             const notePath = notePathRecord.notePath.join("/"); |             const notePath = notePathRecord.notePath; | ||||||
|  |  | ||||||
|             renderedPaths.push(await this.getRenderedPath(notePath, notePathRecord)); |             renderedPaths.push(await this.getRenderedPath(notePath, notePathRecord)); | ||||||
|         } |         } | ||||||
| @@ -96,42 +96,54 @@ export default class NotePathsWidget extends NoteContextAwareWidget { | |||||||
|         this.$notePathList.empty().append(...renderedPaths); |         this.$notePathList.empty().append(...renderedPaths); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getRenderedPath(notePath: string, notePathRecord: NotePathRecord | null = null) { |     async getRenderedPath(notePath: string[], notePathRecord: NotePathRecord | null = null) { | ||||||
|         const title = await treeService.getNotePathTitle(notePath); |         const $pathItem = $("<li>"); | ||||||
|  |         const pathSegments: string[] = []; | ||||||
|  |         const lastIndex = notePath.length - 1; | ||||||
|          |          | ||||||
|         const $noteLink = await linkService.createLink(notePath, { title }); |         for (let i = 0; i < notePath.length; i++) { | ||||||
|  |             const noteId = notePath[i]; | ||||||
|  |             pathSegments.push(noteId); | ||||||
|  |             const title = await treeService.getNoteTitle(noteId); | ||||||
|  |             const $noteLink = await linkService.createLink(pathSegments.join("/"), { title }); | ||||||
|  |  | ||||||
|             $noteLink.find("a").addClass("no-tooltip-preview tn-link"); |             $noteLink.find("a").addClass("no-tooltip-preview tn-link"); | ||||||
|  |             $pathItem.append($noteLink); | ||||||
|  |              | ||||||
|  |             if (i != lastIndex) { | ||||||
|  |                 $pathItem.append(" / "); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         const icons = []; |         const icons = []; | ||||||
|  |  | ||||||
|         if (this.notePath === notePath) { |         if (this.notePath === notePath.join("/")) { | ||||||
|             $noteLink.addClass("path-current"); |             $pathItem.addClass("path-current"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!notePathRecord || notePathRecord.isInHoistedSubTree) { |         if (!notePathRecord || notePathRecord.isInHoistedSubTree) { | ||||||
|             $noteLink.addClass("path-in-hoisted-subtree"); |             $pathItem.addClass("path-in-hoisted-subtree"); | ||||||
|         } else { |         } else { | ||||||
|             icons.push(`<span class="bx bx-trending-up" title="${t("note_paths.outside_hoisted")}"></span>`); |             icons.push(`<span class="bx bx-trending-up" title="${t("note_paths.outside_hoisted")}"></span>`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (notePathRecord?.isArchived) { |         if (notePathRecord?.isArchived) { | ||||||
|             $noteLink.addClass("path-archived"); |             $pathItem.addClass("path-archived"); | ||||||
|  |  | ||||||
|             icons.push(`<span class="bx bx-archive" title="${t("note_paths.archived")}"></span>`); |             icons.push(`<span class="bx bx-archive" title="${t("note_paths.archived")}"></span>`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (notePathRecord?.isSearch) { |         if (notePathRecord?.isSearch) { | ||||||
|             $noteLink.addClass("path-search"); |             $pathItem.addClass("path-search"); | ||||||
|  |  | ||||||
|             icons.push(`<span class="bx bx-search" title="${t("note_paths.search")}"></span>`); |             icons.push(`<span class="bx bx-search" title="${t("note_paths.search")}"></span>`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (icons.length > 0) { |         if (icons.length > 0) { | ||||||
|             $noteLink.append(` ${icons.join(" ")}`); |             $pathItem.append(` ${icons.join(" ")}`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return $("<li>").append($noteLink); |         return $pathItem; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { |     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||||
|   | |||||||
| @@ -75,7 +75,8 @@ function getRecentNotes(activeNoteId: string) { | |||||||
|             notePath: rn.notePath, |             notePath: rn.notePath, | ||||||
|             noteTitle: title, |             noteTitle: title, | ||||||
|             notePathTitle, |             notePathTitle, | ||||||
|             highlightedNotePathTitle: `<span class="${icon ?? "bx bx-note"}"></span> ${notePathTitle}` |             highlightedNotePathTitle: utils.escapeHtml(notePathTitle), | ||||||
|  |             icon: icon ?? "bx bx-note" | ||||||
|         }; |         }; | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,11 +13,12 @@ function getFullAnonymizationScript() { | |||||||
|         .map((attr) => `'${attr.name}'`) |         .map((attr) => `'${attr.name}'`) | ||||||
|         .join(", "); |         .join(", "); | ||||||
|  |  | ||||||
|     const anonymizeScript = ` |     const anonymizeScript = /*sql*/`\ | ||||||
| UPDATE etapi_tokens SET tokenHash = 'API token hash value'; | UPDATE etapi_tokens SET tokenHash = 'API token hash value'; | ||||||
| UPDATE notes SET title = 'title' WHERE title NOT IN ('root', '_hidden', '_share'); | UPDATE notes SET title = 'title' WHERE title NOT IN ('root', '_hidden', '_share'); | ||||||
| UPDATE blobs SET content = 'text' WHERE content IS NOT NULL; | UPDATE blobs SET content = 'text' WHERE content IS NOT NULL; | ||||||
| UPDATE revisions SET title = 'title'; | UPDATE revisions SET title = 'title'; | ||||||
|  | UPDATE attachments SET title = 'title'; | ||||||
|  |  | ||||||
| UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN(${builtinAttrNames}); | UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN(${builtinAttrNames}); | ||||||
| UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN (${builtinAttrNames}); | UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN (${builtinAttrNames}); | ||||||
|   | |||||||
| @@ -29,6 +29,9 @@ export interface TriliumConfig { | |||||||
|         certPath: string; |         certPath: string; | ||||||
|         keyPath: string; |         keyPath: string; | ||||||
|         trustedReverseProxy: boolean | string; |         trustedReverseProxy: boolean | string; | ||||||
|  |         corsAllowOrigin: string; | ||||||
|  |         corsAllowMethods: string; | ||||||
|  |         corsAllowHeaders: string; | ||||||
|     }; |     }; | ||||||
|     Session: { |     Session: { | ||||||
|         cookieMaxAge: number; |         cookieMaxAge: number; | ||||||
| @@ -79,7 +82,16 @@ const config: TriliumConfig = { | |||||||
|             process.env.TRILIUM_NETWORK_KEYPATH || iniConfig.Network.keyPath || "", |             process.env.TRILIUM_NETWORK_KEYPATH || iniConfig.Network.keyPath || "", | ||||||
|  |  | ||||||
|         trustedReverseProxy: |         trustedReverseProxy: | ||||||
|             process.env.TRILIUM_NETWORK_TRUSTEDREVERSEPROXY || iniConfig.Network.trustedReverseProxy || false |             process.env.TRILIUM_NETWORK_TRUSTEDREVERSEPROXY || iniConfig.Network.trustedReverseProxy || false, | ||||||
|  |  | ||||||
|  |         corsAllowOrigin: | ||||||
|  |             process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN || iniConfig.Network.corsAllowOrigin || "", | ||||||
|  |  | ||||||
|  |         corsAllowMethods: | ||||||
|  |             process.env.TRILIUM_NETWORK_CORS_ALLOW_METHODS || iniConfig.Network.corsAllowMethods || "", | ||||||
|  |  | ||||||
|  |         corsAllowHeaders: | ||||||
|  |             process.env.TRILIUM_NETWORK_CORS_ALLOW_HEADERS || iniConfig.Network.corsAllowHeaders || "" | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     Session: { |     Session: { | ||||||
|   | |||||||
| @@ -321,4 +321,25 @@ describe("Markdown export", () => { | |||||||
|         expect(markdownExportService.toMarkdown(html)).toBe(expected); |         expect(markdownExportService.toMarkdown(html)).toBe(expected); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     it("exports todo lists properly", () => { | ||||||
|  |         const html = trimIndentation/*html*/`\ | ||||||
|  |             <ul class="todo-list"> | ||||||
|  |                 <li> | ||||||
|  |                     <label class="todo-list__label"> | ||||||
|  |                         <input type="checkbox" checked="checked" disabled="disabled"><span class="todo-list__label__description">Hello</span> | ||||||
|  |                     </label> | ||||||
|  |                 </li> | ||||||
|  |                 <li> | ||||||
|  |                     <label class="todo-list__label"> | ||||||
|  |                         <input type="checkbox" disabled="disabled"><span class="todo-list__label__description">World</span> | ||||||
|  |                     </label> | ||||||
|  |                 </li> | ||||||
|  |             </ul> | ||||||
|  |         `; | ||||||
|  |         const expected = trimIndentation`\ | ||||||
|  |             - [x] Hello | ||||||
|  |             - [ ] World`; | ||||||
|  |         expect(markdownExportService.toMarkdown(html)).toBe(expected); | ||||||
|  |     }); | ||||||
|  |  | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -230,7 +230,11 @@ function buildListItemFilter(): Rule { | |||||||
|                 var start = parent.getAttribute('start') |                 var start = parent.getAttribute('start') | ||||||
|                 var index = Array.prototype.indexOf.call(parent.children, node) |                 var index = Array.prototype.indexOf.call(parent.children, node) | ||||||
|                 prefix = (start ? Number(start) + index : index + 1) + '.  ' |                 prefix = (start ? Number(start) + index : index + 1) + '.  ' | ||||||
|  |             } else if (parent.classList.contains("todo-list")) { | ||||||
|  |                 const isChecked = node.querySelector("input[type=checkbox]:checked"); | ||||||
|  |                 prefix = (isChecked ? "- [x] " : "- [ ] "); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             const result = prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : ''); |             const result = prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : ''); | ||||||
|             return result; |             return result; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -233,4 +233,12 @@ second line 2</code></pre><ul><li>Hello</li><li>world</li></ul><ol><li>Hello</li | |||||||
|         expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); |         expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     it("imports todo lists properly", () => { | ||||||
|  |          const input = trimIndentation`\ | ||||||
|  |             - [x] Hello | ||||||
|  |             - [ ] World`; | ||||||
|  |         const expected = `<ul class="todo-list"><li><label class="todo-list__label"><input type="checkbox" checked="checked" disabled="disabled"><span class="todo-list__label__description">Hello</span></label></li><li><label class="todo-list__label"><input type="checkbox" disabled="disabled"><span class="todo-list__label__description">World</span></label></li></ul>`; | ||||||
|  |         expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); | ||||||
|  |     }); | ||||||
|  |  | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -48,12 +48,52 @@ class CustomMarkdownRenderer extends Renderer { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     list(token: Tokens.List): string { |     list(token: Tokens.List): string { | ||||||
|         return super.list(token) |         let result = super.list(token) | ||||||
|             .replace("\n", "")  // we replace the first one only. |             .replace("\n", "")  // we replace the first one only. | ||||||
|             .trimEnd(); |             .trimEnd(); | ||||||
|  |  | ||||||
|  |         // Handle todo-list in the CKEditor format. | ||||||
|  |         if (token.items.some(item => item.task)) { | ||||||
|  |             result = result.replace(/^<ul>/, "<ul class=\"todo-list\">"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     checkbox({ checked }: Tokens.Checkbox): string { | ||||||
|  |         return '<input type="checkbox"' | ||||||
|  |             + (checked ? 'checked="checked" ' : '') | ||||||
|  |             + 'disabled="disabled">'; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     listitem(item: Tokens.ListItem): string { |     listitem(item: Tokens.ListItem): string { | ||||||
|  |         // Handle todo-list in the CKEditor format. | ||||||
|  |         if (item.task) { | ||||||
|  |             let itemBody = ''; | ||||||
|  |             const checkbox = this.checkbox({ checked: !!item.checked }); | ||||||
|  |             if (item.loose) { | ||||||
|  |                 if (item.tokens[0]?.type === 'paragraph') { | ||||||
|  |                     item.tokens[0].text = checkbox + item.tokens[0].text; | ||||||
|  |                     if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { | ||||||
|  |                         item.tokens[0].tokens[0].text = checkbox + escape(item.tokens[0].tokens[0].text); | ||||||
|  |                         item.tokens[0].tokens[0].escaped = true; | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     item.tokens.unshift({ | ||||||
|  |                         type: 'text', | ||||||
|  |                         raw: checkbox, | ||||||
|  |                         text: checkbox, | ||||||
|  |                         escaped: true, | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 itemBody += checkbox; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             itemBody += `<span class="todo-list__label__description">${this.parser.parse(item.tokens, !!item.loose)}</span>`; | ||||||
|  |             return `<li><label class="todo-list__label">${itemBody}</label></li>`; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return super.listitem(item).trimEnd(); |         return super.listitem(item).trimEnd(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -358,8 +358,9 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) { | |||||||
|         return { |         return { | ||||||
|             notePath: result.notePath, |             notePath: result.notePath, | ||||||
|             noteTitle: title, |             noteTitle: title, | ||||||
|             notePathTitle: `${icon} ${result.notePathTitle}`, |             notePathTitle: result.notePathTitle, | ||||||
|             highlightedNotePathTitle: `<span class="${icon ?? "bx bx-note"}"></span> ${result.highlightedNotePathTitle}` |             highlightedNotePathTitle: result.highlightedNotePathTitle, | ||||||
|  |             icon: icon ?? "bx bx-note" | ||||||
|         }; |         }; | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user