Compare commits
	
		
			334 Commits
		
	
	
		
			fix/resolv
			...
			v0.97.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | fd25c735c1 | ||
|  | 7de33907c5 | ||
|  | a3014434cf | ||
|  | 3ebab2c126 | ||
|  | 954619bd36 | ||
|  | 6995fbfd06 | ||
|  | 83b72eafa6 | ||
|  | 757a6777be | ||
|  | d003e91b89 | ||
|  | 4a35df745a | ||
|  | 713a0f5b09 | ||
|  | 2cf9c98b43 | ||
|  | d7af196a0c | ||
|  | c363be57b7 | ||
|  | 10645790de | ||
|  | 8b18cf382c | ||
|  | 7a131e0bcc | ||
|  | 3d264379cc | ||
|  | f405682ec1 | ||
|  | 3debf3ce1c | ||
|  | 5a76883969 | ||
|  | 6f51c5e0cc | ||
|  | 2c730d1f0b | ||
|  | d487da0b2f | ||
|  | cb8a5cbb62 | ||
|  | ceb08593d8 | ||
|  | 9dd0eb7b9b | ||
|  | ebff644d24 | ||
|  | beb1c15fa5 | ||
|  | 40a5eee211 | ||
|  | 8f393d0bae | ||
|  | 94dad49e2f | ||
|  | 409638151c | ||
|  | 0d3de92890 | ||
|  | 5d619131ec | ||
|  | e2c8443778 | ||
|  | daa4743967 | ||
|  | 56553078ef | ||
|  | 5584a06cb3 | ||
|  | cfeb69ace6 | ||
|  | b0c8f110de | ||
|  | aba1266c45 | ||
|  | c331e0103d | ||
|  | 13978574e0 | ||
|  | be85963558 | ||
|  | 8c19261ced | ||
|  | 7ca17fa609 | ||
|  | 3d107572df | ||
|  | f7488655a7 | ||
|  | 876e0a29d4 | ||
|  | af74375695 | ||
|  | 896965fec5 | ||
|  | ba5ef93c1a | ||
|  | ef1153d336 | ||
|  | 0d347f8823 | ||
|  | 897cdc26ae | ||
|  | aba621c099 | ||
|  | 839813ebde | ||
|  | 545e2ddbfc | ||
|  | 1d63a5903a | ||
|  | 2b34c00a0c | ||
|  | 123068062a | ||
|  | 9a668e8709 | ||
|  | f6f8937d64 | ||
|  | c9f53a2880 | ||
|  | 2887e712c3 | ||
|  | 5d3a0ed1b4 | ||
|  | 334b6319de | ||
|  | 4c118c0fd4 | ||
|  | db00d60684 | ||
|  | 25b74af363 | ||
|  | eb57cf97ad | ||
|  | c92e24363f | ||
|  | 8d5d00ac0f | ||
|  | 8b457384ba | ||
|  | fab2d53ece | ||
|  | 774f27d8d2 | ||
|  | d7f02ef1b3 | ||
|  | 97eaa6294c | ||
|  | dc02bb0850 | ||
|  | 2c8c041e1c | ||
|  | 874b1c6654 | ||
|  | fb982c7097 | ||
|  | b7f5ce600e | ||
|  | 91604c9e26 | ||
|  | c874333a37 | ||
|  | 1298b968f2 | ||
|  | 6fe5a854a7 | ||
|  | aba3b5cb19 | ||
|  | 282aed22b5 | ||
|  | 669a3d9dcf | ||
|  | 9d7455d28a | ||
|  | 4f0c8b081c | ||
|  | a5db5298a0 | ||
|  | 876c6e9252 | ||
|  | aef824d262 | ||
|  | a25ce42490 | ||
|  | 8b0fdaccf4 | ||
|  | bd840a2421 | ||
|  | 27d515f289 | ||
|  | df3b9faf8d | ||
|  | 0f129734ae | ||
|  | 275aacfba9 | ||
|  | e7f47a0663 | ||
|  | 66486541fe | ||
|  | 34f1a84769 | ||
|  | 2244f0368f | ||
|  | 9d85005255 | ||
|  | ad8629dca6 | ||
|  | cccfe0e05a | ||
|  | a8874257e8 | ||
|  | f689c55f56 | ||
|  | 853c7be8b8 | ||
|  | 823df1e12d | ||
|  | 7570f818e9 | ||
|  | 03aa5aea2c | ||
|  | a4e86ac353 | ||
|  | cf6efc050a | ||
|  | 3e0802176b | ||
|  | 697954d4d9 | ||
|  | 741f6c1114 | ||
|  | b2237ffa51 | ||
|  | 7b6d11bffa | ||
|  | 97565e8f36 | ||
|  | c0dfee8439 | ||
|  | fc98240614 | ||
|  | 169d1203c2 | ||
|  | f3350bc8f5 | ||
|  | 504a19275c | ||
|  | 14cdc52670 | ||
|  | cf8063f311 | ||
|  | aa8902f5b9 | ||
|  | 7cd0e664ac | ||
|  | a04804d3fa | ||
|  | 86f90e6685 | ||
|  | 8131a4b3d2 | ||
|  | b91a3e13b0 | ||
|  | 5a7a0d32d1 | ||
|  | 3f5df18d6c | ||
|  | df2cede075 | ||
|  | 4321c161ac | ||
|  | b1f0c64ef2 | ||
|  | c9b37dcc77 | ||
|  | ab093ed9a0 | ||
|  | cf31367acd | ||
|  | e3d306cac3 | ||
|  | 960d321019 | ||
|  | 2d4ac93221 | ||
|  | d4a4f15416 | ||
|  | 504a842d37 | ||
|  | ded5b1f5d2 | ||
|  | fcbbc21a80 | ||
|  | 38fce25b86 | ||
|  | 4cc2fa5300 | ||
|  | 4a82c3f65a | ||
|  | b255d70e18 | ||
|  | caa842cd55 | ||
|  | cd338085fb | ||
|  | e703ce92a8 | ||
|  | 84479a2c2a | ||
|  | c13969217c | ||
|  | 402540f483 | ||
|  | 8c56315313 | ||
|  | b29c3eff6e | ||
|  | ec7dacfc9b | ||
|  | 5f9a6a9f76 | ||
|  | 28f4aea3d5 | ||
|  | 8d29c5fe1b | ||
|  | ccd935b562 | ||
|  | d77a49857b | ||
|  | e30478e5d4 | ||
|  | 71863752cd | ||
|  | e4a2a8e56d | ||
|  | 0f1c505823 | ||
|  | 1ecce11113 | ||
|  | 2287d67fb5 | ||
|  | 5b4f17ef3d | ||
|  | 3720ab6df6 | ||
|  | 3c893d69e5 | ||
|  | b93a4a3e42 | ||
|  | 23cef0ab94 | ||
|  | c8ffb8d694 | ||
|  | 08e08d8920 | ||
|  | 7acd300163 | ||
|  | d8d95db4ec | ||
|  | af97d3ef1d | ||
|  | c65ec14943 | ||
|  | adfdc7edb4 | ||
|  | 8cced607eb | ||
|  | 5dd5af90c2 | ||
|  | 7a48333b4f | ||
|  | 7044533398 | ||
|  | 560aad8df6 | ||
|  | 36c2099b2e | ||
|  | 6c157675d7 | ||
|  | 458d66cb21 | ||
|  | 201e8911c5 | ||
|  | 1b1ed2408f | ||
|  | 62487d21d8 | ||
|  | bc752bdb0b | ||
|  | 9e00d421fb | ||
|  | e7f02fe22b | ||
|  | 6d694f8e53 | ||
|  | 977befd0a7 | ||
|  | 1566ae4fbd | ||
|  | 4e97490cc6 | ||
|  | 446d5a0fcc | ||
|  | 1fd6465012 | ||
|  | 6cea8e3b87 | ||
|  | 28a63e0326 | ||
|  | b73da46111 | ||
|  | abafa8c2d2 | ||
|  | 4ae3272cdf | ||
|  | 6aa3b8dbd7 | ||
|  | 395e9b2228 | ||
|  | be33f68c52 | ||
|  | 29d96381fa | ||
|  | da8eecf774 | ||
|  | de91326c12 | ||
|  | ee1c3c35d7 | ||
|  | 70eece1429 | ||
|  | b4f2be332b | ||
|  | 23fe76989b | ||
|  | 275d07659d | ||
|  | a901e92573 | ||
|  | 6ead31b45f | ||
|  | d4ce12dca9 | ||
|  | bb6e22cdb7 | ||
|  | 2c9fc4812e | ||
|  | 60f4554afa | ||
|  | 3c486bfd1b | ||
|  | 26b9a95bb2 | ||
|  | f7c9217cea | ||
|  | e92022b73c | ||
|  | 61ff2353c8 | ||
|  | c8cca26ca4 | ||
|  | aa556ed4d5 | ||
|  | 5d694a7bdf | ||
|  | c4787dae23 | ||
|  | 9f5f329c53 | ||
|  | f82b96fcc4 | ||
|  | d4b24fa427 | ||
|  | c852f67c59 | ||
|  | 92c228a3c9 | ||
|  | 42f948e2b3 | ||
|  | 13e8932117 | ||
|  | 910d34bd42 | ||
|  | b204ba29e7 | ||
|  | d49244cbc8 | ||
|  | ef2f2f17b4 | ||
|  | b9f21dcf4c | ||
|  | 808fe690cc | ||
|  | 901eec04e5 | ||
|  | 9272394ada | ||
|  | 4457982fae | ||
|  | 7f67b2b461 | ||
|  | 7f3934f4c3 | ||
|  | a3b80a2cc4 | ||
|  | 6d967e5e51 | ||
|  | b674ca90d1 | ||
|  | 95edb60a84 | ||
|  | 40add78ccb | ||
|  | 1029c24c06 | ||
|  | 94d94fe8fb | ||
|  | 49489c0f45 | ||
|  | 215833a2c9 | ||
|  | a7471a3d47 | ||
|  | 909aaefbd7 | ||
|  | 15c2f56bf2 | ||
|  | 84cdfec415 | ||
|  | 91572ab8b9 | ||
|  | ed758f4c92 | ||
|  | f1fc15e115 | ||
|  | 22300e8151 | ||
|  | 292646e14a | ||
|  | b4921a20d8 | ||
|  | 54be79a725 | ||
|  | 4fc47370fe | ||
|  | 9e30bcf233 | ||
|  | e5712c54e6 | ||
|  | 2a4fe21a39 | ||
|  | b259558f0f | ||
|  | e2f6d9e0d6 | ||
|  | 4fc2b0fa5e | ||
|  | 8dca79ecf2 | ||
|  | c7f49f0e21 | ||
|  | bce2094fb2 | ||
|  | 65c33e1aa0 | ||
|  | 8e108bc5e2 | ||
|  | 4e75ce7fdb | ||
|  | 1e42574d28 | ||
|  | 85ebaf6afa | ||
|  | 661c7e4056 | ||
|  | 1e8ea54dbc | ||
|  | ddbe7e9936 | ||
|  | cab86175ef | ||
|  | ec7414b174 | ||
|  | 8343a5d1dd | ||
|  | 18c55784c7 | ||
|  | 39eac83d38 | ||
|  | 55bd6fb57d | ||
|  | 6fdec52332 | ||
|  | 824a3c5fcc | ||
|  | 87da644027 | ||
|  | 4f42f543d8 | ||
|  | 97ea3ac3fc | ||
|  | f04b75fd36 | ||
|  | f5bffc38f1 | ||
|  | 27738acefc | ||
|  | 59ce2072c5 | ||
|  | ed68dda70b | ||
|  | 892ab02f06 | ||
|  | 7d9196d5e1 | ||
|  | dccdb5ceb7 | ||
|  | f961698e44 | ||
|  | 278fe3262e | ||
|  | 1fc860b052 | ||
|  | 88a8311173 | ||
|  | 63dc5697dd | ||
|  | b595d1fade | ||
|  | d91c59b7d0 | ||
|  | aa2ab0da31 | ||
|  | 91f94106fb | ||
|  | 308f319138 | ||
|  | fa0c01591a | ||
|  | cb5a771490 | ||
|  | 0c17a13462 | ||
|  | 04593cb2d7 | ||
|  | aa872f47f2 | ||
|  | 5adca76a9a | ||
|  | e7467f6446 | ||
|  | e49473fbd3 | ||
|  | bfec44aa5a | ||
|  | 55b3bf6036 | 
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -2,3 +2,5 @@ | ||||
|  | ||||
| github: [eliandoran] | ||||
| custom: ["https://paypal.me/eliandoran"] | ||||
| liberapay: ElianDoran | ||||
| buy_me_a_coffee: eliandoran | ||||
|   | ||||
							
								
								
									
										17
									
								
								.github/workflows/checks.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| name: Checks | ||||
| on: | ||||
|   push: | ||||
|   pull_request_target: | ||||
|     types: [synchronize] | ||||
|  | ||||
| jobs: | ||||
|   main: | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       contents: write | ||||
|     steps: | ||||
|       - name: Check if PRs have conflicts | ||||
|         uses: eps1lon/actions-label-merge-conflict@v3 | ||||
|         with: | ||||
|           dirtyLabel: "merge-conflicts" | ||||
|           repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}" | ||||
							
								
								
									
										4
									
								
								.mailmap
									
									
									
									
									
								
							
							
						
						| @@ -1,2 +1,2 @@ | ||||
| Adam Zivner <adam.zivner@gmail.com> | ||||
| Adam Zivner <zadam.apps@gmail.com> | ||||
| zadam <adam.zivner@gmail.com> | ||||
| zadam <zadam.apps@gmail.com> | ||||
							
								
								
									
										9
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -28,5 +28,12 @@ | ||||
|     "typescript.validate.enable": true, | ||||
|     "typescript.tsserver.experimental.enableProjectDiagnostics": true, | ||||
|     "typescript.tsdk": "node_modules/typescript/lib", | ||||
|     "typescript.enablePromptUseWorkspaceTsdk": true | ||||
|     "typescript.enablePromptUseWorkspaceTsdk": true, | ||||
|     "search.exclude": { | ||||
|         "**/node_modules": true, | ||||
|         "docs/**/*.html": true, | ||||
|         "docs/**/*.png": true, | ||||
|         "apps/server/src/assets/doc_notes/**": true, | ||||
|         "apps/edit-docs/demo/**": true | ||||
|     } | ||||
| } | ||||
							
								
								
									
										15
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,6 +1,7 @@ | ||||
| # Trilium Notes | ||||
|  | ||||
|  | ||||
| Donate:   | ||||
|  | ||||
|  | ||||
|  | ||||
| [](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) | ||||
| @@ -119,8 +120,8 @@ To install TriliumNext on your own server (including via Docker from [Dockerhub] | ||||
|  | ||||
| Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080): | ||||
| ```shell | ||||
| git clone https://github.com/TriliumNext/Notes.git | ||||
| cd Notes | ||||
| git clone https://github.com/TriliumNext/Trilium.git | ||||
| cd Trilium | ||||
| pnpm install | ||||
| pnpm run server:start | ||||
| ``` | ||||
| @@ -129,8 +130,8 @@ pnpm run server:start | ||||
|  | ||||
| Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation: | ||||
| ```shell | ||||
| git clone https://github.com/TriliumNext/Notes.git | ||||
| cd Notes | ||||
| git clone https://github.com/TriliumNext/Trilium.git | ||||
| cd Trilium | ||||
| pnpm install | ||||
| pnpm nx run edit-docs:edit-docs | ||||
| ``` | ||||
| @@ -138,8 +139,8 @@ pnpm nx run edit-docs:edit-docs | ||||
| ### Building the Executable | ||||
| Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows: | ||||
| ```shell | ||||
| git clone https://github.com/TriliumNext/Notes.git | ||||
| cd Notes | ||||
| git clone https://github.com/TriliumNext/Trilium.git | ||||
| cd Trilium | ||||
| pnpm install | ||||
| pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32 | ||||
| ``` | ||||
|   | ||||
| @@ -35,13 +35,13 @@ | ||||
|     "chore:generate-openapi": "tsx bin/generate-openapi.js" | ||||
|   }, | ||||
|   "devDependencies": {     | ||||
|     "@playwright/test": "1.53.2", | ||||
|     "@stylistic/eslint-plugin": "5.1.0",         | ||||
|     "@playwright/test": "1.54.1", | ||||
|     "@stylistic/eslint-plugin": "5.2.0",         | ||||
|     "@types/express": "5.0.3",     | ||||
|     "@types/node": "22.16.2",     | ||||
|     "@types/node": "22.16.5",     | ||||
|     "@types/yargs": "17.0.33", | ||||
|     "@vitest/coverage-v8": "3.2.4", | ||||
|     "eslint": "9.30.1", | ||||
|     "eslint": "9.31.0", | ||||
|     "eslint-plugin-simple-import-sort": "12.1.1", | ||||
|     "esm": "3.2.25", | ||||
|     "jsdoc": "4.0.4", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "@triliumnext/client", | ||||
|   "version": "0.96.0", | ||||
|   "version": "0.97.1", | ||||
|   "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", | ||||
|   "private": true, | ||||
|   "license": "AGPL-3.0-only", | ||||
| @@ -10,7 +10,7 @@ | ||||
|     "url": "https://github.com/TriliumNext/Notes" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@eslint/js": "9.30.1", | ||||
|     "@eslint/js": "9.31.0", | ||||
|     "@excalidraw/excalidraw": "0.18.0", | ||||
|     "@fullcalendar/core": "6.1.18", | ||||
|     "@fullcalendar/daygrid": "6.1.18", | ||||
| @@ -46,9 +46,9 @@ | ||||
|     "leaflet": "1.9.4", | ||||
|     "leaflet-gpx": "2.2.0", | ||||
|     "mark.js": "8.11.1", | ||||
|     "marked": "16.0.0", | ||||
|     "mermaid": "11.8.1", | ||||
|     "mind-elixir": "5.0.1", | ||||
|     "marked": "16.1.1", | ||||
|     "mermaid": "11.9.0", | ||||
|     "mind-elixir": "5.0.2", | ||||
|     "normalize.css": "8.0.1", | ||||
|     "panzoom": "9.4.3", | ||||
|     "preact": "10.26.9", | ||||
| @@ -58,7 +58,7 @@ | ||||
|     "vanilla-js-wheel-zoom": "9.0.4" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@ckeditor/ckeditor5-inspector": "4.1.0", | ||||
|     "@ckeditor/ckeditor5-inspector": "5.0.0", | ||||
|     "@types/bootstrap": "5.2.10", | ||||
|     "@types/jquery": "3.5.32", | ||||
|     "@types/leaflet": "1.9.20", | ||||
| @@ -68,7 +68,7 @@ | ||||
|     "copy-webpack-plugin": "13.0.0", | ||||
|     "happy-dom": "18.0.1", | ||||
|     "script-loader": "0.7.2", | ||||
|     "vite-plugin-static-copy": "3.1.0" | ||||
|     "vite-plugin-static-copy": "3.1.1" | ||||
|   }, | ||||
|   "nx": { | ||||
|     "name": "client", | ||||
|   | ||||
| @@ -28,6 +28,8 @@ import TouchBarComponent from "./touch_bar.js"; | ||||
| import type { CKTextEditor } from "@triliumnext/ckeditor5"; | ||||
| import type CodeMirror from "@triliumnext/codemirror"; | ||||
| import { StartupChecks } from "./startup_checks.js"; | ||||
| import type { CreateNoteOpts } from "../services/note_create.js"; | ||||
| import { ColumnComponent } from "tabulator-tables"; | ||||
|  | ||||
| interface Layout { | ||||
|     getRootWidget: (appContext: AppContext) => RootWidget; | ||||
| @@ -122,6 +124,7 @@ export type CommandMappings = { | ||||
|     showImportDialog: CommandData & { noteId: string }; | ||||
|     openNewNoteSplit: NoteCommandData; | ||||
|     openInWindow: NoteCommandData; | ||||
|     openInPopup: CommandData & { noteIdOrPath: string; }; | ||||
|     openNoteInNewTab: CommandData; | ||||
|     openNoteInNewSplit: CommandData; | ||||
|     openNoteInNewWindow: CommandData; | ||||
| @@ -140,6 +143,7 @@ export type CommandMappings = { | ||||
|     }; | ||||
|     openInTab: ContextMenuCommandData; | ||||
|     openNoteInSplit: ContextMenuCommandData; | ||||
|     openNoteInPopup: ContextMenuCommandData; | ||||
|     toggleNoteHoisting: ContextMenuCommandData; | ||||
|     insertNoteAfter: ContextMenuCommandData; | ||||
|     insertChildNote: ContextMenuCommandData; | ||||
| @@ -274,6 +278,21 @@ export type CommandMappings = { | ||||
|  | ||||
|     geoMapCreateChildNote: CommandData; | ||||
|  | ||||
|     // Table view | ||||
|     addNewRow: CommandData & { | ||||
|         customOpts: CreateNoteOpts; | ||||
|         parentNotePath?: string; | ||||
|     }; | ||||
|     addNewTableColumn: CommandData & { | ||||
|         columnToEdit?: ColumnComponent; | ||||
|         referenceColumn?: ColumnComponent; | ||||
|         direction?: "before" | "after"; | ||||
|         type?: "label" | "relation"; | ||||
|     }; | ||||
|     deleteTableColumn: CommandData & { | ||||
|         columnToDelete?: ColumnComponent; | ||||
|     }; | ||||
|  | ||||
|     buildTouchBar: CommandData & { | ||||
|         TouchBar: typeof TouchBar; | ||||
|         buildIcon(name: string): NativeImage; | ||||
|   | ||||
| @@ -256,6 +256,20 @@ class FNote { | ||||
|         return this.children; | ||||
|     } | ||||
|  | ||||
|     async getSubtreeNoteIds() { | ||||
|         let noteIds: (string | string[])[] = []; | ||||
|         for (const child of await this.getChildNotes()) { | ||||
|             noteIds.push(child.noteId); | ||||
|             noteIds.push(await child.getSubtreeNoteIds()); | ||||
|         } | ||||
|         return noteIds.flat(); | ||||
|     } | ||||
|  | ||||
|     async getSubtreeNotes() { | ||||
|         const noteIds = await this.getSubtreeNoteIds(); | ||||
|         return this.froca.getNotes(noteIds); | ||||
|     } | ||||
|  | ||||
|     async getChildNotes() { | ||||
|         return await this.froca.getNotes(this.children); | ||||
|     } | ||||
|   | ||||
| @@ -46,28 +46,7 @@ import SharedInfoWidget from "../widgets/shared_info.js"; | ||||
| import FindWidget from "../widgets/find.js"; | ||||
| import TocWidget from "../widgets/toc.js"; | ||||
| import HighlightsListWidget from "../widgets/highlights_list.js"; | ||||
| import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js"; | ||||
| import AboutDialog from "../widgets/dialogs/about.js"; | ||||
| import HelpDialog from "../widgets/dialogs/help.js"; | ||||
| import RecentChangesDialog from "../widgets/dialogs/recent_changes.js"; | ||||
| import BranchPrefixDialog from "../widgets/dialogs/branch_prefix.js"; | ||||
| import SortChildNotesDialog from "../widgets/dialogs/sort_child_notes.js"; | ||||
| import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js"; | ||||
| import IncludeNoteDialog from "../widgets/dialogs/include_note.js"; | ||||
| import NoteTypeChooserDialog from "../widgets/dialogs/note_type_chooser.js"; | ||||
| import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js"; | ||||
| import AddLinkDialog from "../widgets/dialogs/add_link.js"; | ||||
| import CloneToDialog from "../widgets/dialogs/clone_to.js"; | ||||
| import MoveToDialog from "../widgets/dialogs/move_to.js"; | ||||
| import ImportDialog from "../widgets/dialogs/import.js"; | ||||
| import ExportDialog from "../widgets/dialogs/export.js"; | ||||
| import MarkdownImportDialog from "../widgets/dialogs/markdown_import.js"; | ||||
| import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js"; | ||||
| import RevisionsDialog from "../widgets/dialogs/revisions.js"; | ||||
| import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js"; | ||||
| import InfoDialog from "../widgets/dialogs/info.js"; | ||||
| import ConfirmDialog from "../widgets/dialogs/confirm.js"; | ||||
| import PromptDialog from "../widgets/dialogs/prompt.js"; | ||||
| import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; | ||||
| import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; | ||||
| import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js"; | ||||
| @@ -83,7 +62,7 @@ import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_ref | ||||
| import ScrollPaddingWidget from "../widgets/scroll_padding.js"; | ||||
| import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; | ||||
| import options from "../services/options.js"; | ||||
| import utils, { hasTouchBar } from "../services/utils.js"; | ||||
| import utils from "../services/utils.js"; | ||||
| import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js"; | ||||
| import ContextualHelpButton from "../widgets/floating_buttons/help_button.js"; | ||||
| import CloseZenButton from "../widgets/close_zen_button.js"; | ||||
| @@ -229,7 +208,7 @@ export default class DesktopLayout { | ||||
|                                                                 .child(new PromotedAttributesWidget()) | ||||
|                                                                 .child(new SqlTableSchemasWidget()) | ||||
|                                                                 .child(new NoteDetailWidget()) | ||||
|                                                                 .child(new NoteListWidget()) | ||||
|                                                                 .child(new NoteListWidget(false)) | ||||
|                                                                 .child(new SearchResultWidget()) | ||||
|                                                                 .child(new SqlResultWidget()) | ||||
|                                                                 .child(new ScrollPaddingWidget()) | ||||
|   | ||||
| @@ -22,6 +22,14 @@ import RevisionsDialog from "../widgets/dialogs/revisions.js"; | ||||
| import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js"; | ||||
| import InfoDialog from "../widgets/dialogs/info.js"; | ||||
| import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js"; | ||||
| import PopupEditorDialog from "../widgets/dialogs/popup_editor.js"; | ||||
| import FlexContainer from "../widgets/containers/flex_container.js"; | ||||
| import NoteIconWidget from "../widgets/note_icon.js"; | ||||
| import NoteTitleWidget from "../widgets/note_title.js"; | ||||
| import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; | ||||
| import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; | ||||
| import NoteDetailWidget from "../widgets/note_detail.js"; | ||||
| import NoteListWidget from "../widgets/note_list.js"; | ||||
|  | ||||
| export function applyModals(rootContainer: RootContainer) { | ||||
|     rootContainer | ||||
| @@ -47,4 +55,15 @@ export function applyModals(rootContainer: RootContainer) { | ||||
|         .child(new ConfirmDialog()) | ||||
|         .child(new PromptDialog()) | ||||
|         .child(new IncorrectCpuArchDialog()) | ||||
|         .child(new PopupEditorDialog() | ||||
|                 .child(new FlexContainer("row") | ||||
|                     .class("title-row") | ||||
|                     .css("align-items", "center") | ||||
|                     .cssBlock(".title-row > * { margin: 5px; }") | ||||
|                     .child(new NoteIconWidget()) | ||||
|                     .child(new NoteTitleWidget())) | ||||
|                 .child(new ClassicEditorToolbar()) | ||||
|                 .child(new PromotedAttributesWidget()) | ||||
|                 .child(new NoteDetailWidget()) | ||||
|                 .child(new NoteListWidget(true))) | ||||
| } | ||||
|   | ||||
| @@ -162,7 +162,7 @@ export default class MobileLayout { | ||||
|                                     .filling() | ||||
|                                     .contentSized() | ||||
|                                     .child(new NoteDetailWidget()) | ||||
|                                     .child(new NoteListWidget()) | ||||
|                                     .child(new NoteListWidget(false)) | ||||
|                                     .child(new FilePropertiesWidget().css("font-size", "smaller")) | ||||
|                             ) | ||||
|                             .child(new MobileEditorToolbar()) | ||||
|   | ||||
| @@ -26,6 +26,11 @@ export interface MenuCommandItem<T> { | ||||
|     title: string; | ||||
|     command?: T; | ||||
|     type?: string; | ||||
|     /** | ||||
|      * The icon to display in the menu item. | ||||
|      * | ||||
|      * If not set, no icon is displayed and the item will appear shifted slightly to the left if there are other items with icons. To avoid this, use `bx bx-empty`. | ||||
|      */ | ||||
|     uiIcon?: string; | ||||
|     badges?: MenuItemBadge[]; | ||||
|     templateNoteId?: string; | ||||
|   | ||||
| @@ -16,7 +16,8 @@ function getItems(): MenuItem<CommandNames>[] { | ||||
|     return [ | ||||
|         { title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" }, | ||||
|         { title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" }, | ||||
|         { title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" } | ||||
|         { title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" }, | ||||
|         { title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" } | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| @@ -40,6 +41,8 @@ function handleLinkContextMenuItem(command: string | undefined, notePath: string | ||||
|         appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope }); | ||||
|     } else if (command === "openNoteInNewWindow") { | ||||
|         appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); | ||||
|     } else if (command === "openNoteInPopup") { | ||||
|         appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath }) | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -70,8 +70,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree | ||||
|  | ||||
|         const items: (MenuItem<TreeCommandNames> | null)[] = [ | ||||
|             { title: `${t("tree-context-menu.open-in-a-new-tab")}`, command: "openInTab", uiIcon: "bx bx-link-external", enabled: noSelectedNotes }, | ||||
|  | ||||
|             { title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes }, | ||||
|             { title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes }, | ||||
|  | ||||
|             isHoisted | ||||
|                 ? null | ||||
| @@ -129,13 +129,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree | ||||
|                         enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp | ||||
|                     }, | ||||
|                     { title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp }, | ||||
|                     { | ||||
|                         title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`, | ||||
|                         command: "duplicateSubtree", | ||||
|                         uiIcon: "bx bx-outline", | ||||
|                         enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp | ||||
|                     }, | ||||
|  | ||||
|                      | ||||
|                     { title: "----" }, | ||||
|  | ||||
|                     { title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes }, | ||||
| @@ -188,6 +182,13 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree | ||||
|  | ||||
|             { title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted }, | ||||
|  | ||||
|             { | ||||
|                 title: `${t("tree-context-menu.duplicate")} <kbd data-command="duplicateSubtree">`, | ||||
|                 command: "duplicateSubtree", | ||||
|                 uiIcon: "bx bx-outline", | ||||
|                 enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp | ||||
|             }, | ||||
|  | ||||
|             { | ||||
|                 title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, | ||||
|                 command: "deleteNotes", | ||||
| @@ -246,6 +247,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree | ||||
|             const { ntxId } = subContexts?.[subContexts.length - 1] ?? {}; | ||||
|  | ||||
|             this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath }); | ||||
|         } else if (command === "openNoteInPopup") { | ||||
|             appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath }) | ||||
|         } else if (command === "convertNoteToAttachment") { | ||||
|             if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) { | ||||
|                 return; | ||||
|   | ||||
| @@ -12,11 +12,12 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe | ||||
|     }); | ||||
| } | ||||
|  | ||||
| export async function setLabel(noteId: string, name: string, value: string = "") { | ||||
| export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) { | ||||
|     await server.put(`notes/${noteId}/set-attribute`, { | ||||
|         type: "label", | ||||
|         name: name, | ||||
|         value: value | ||||
|         value: value, | ||||
|         isInheritable | ||||
|     }); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -95,7 +95,15 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false) { | ||||
| /** | ||||
|  * Shows the delete confirmation screen | ||||
|  * | ||||
|  * @param branchIdsToDelete the list of branch IDs to delete. | ||||
|  * @param forceDeleteAllClones whether to check by default the "Delete also all clones" checkbox. | ||||
|  * @param moveToParent whether to automatically go to the parent note path after a succesful delete. Usually makes sense if deleting the active note(s). | ||||
|  * @returns promise that returns false if the operation was cancelled or there was nothing to delete, true if the operation succeeded. | ||||
|  */ | ||||
| async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false, moveToParent = true) { | ||||
|     branchIdsToDelete = filterRootNote(branchIdsToDelete); | ||||
|  | ||||
|     if (branchIdsToDelete.length === 0) { | ||||
| @@ -110,10 +118,12 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|         await activateParentNotePath(); | ||||
|     } catch (e) { | ||||
|         console.error(e); | ||||
|     if (moveToParent) { | ||||
|         try { | ||||
|             await activateParentNotePath(); | ||||
|         } catch (e) { | ||||
|             console.error(e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const taskId = utils.randomString(10); | ||||
|   | ||||
| @@ -15,6 +15,8 @@ import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation | ||||
| import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; | ||||
| import { t } from "./i18n.js"; | ||||
| import type FNote from "../entities/fnote.js"; | ||||
| import toast from "./toast.js"; | ||||
| import { BulkAction } from "@triliumnext/commons"; | ||||
|  | ||||
| const ACTION_GROUPS = [ | ||||
|     { | ||||
| @@ -89,6 +91,17 @@ function parseActions(note: FNote) { | ||||
|         .filter((action) => !!action); | ||||
| } | ||||
|  | ||||
| export async function executeBulkActions(parentNoteId: string, actions: BulkAction[]) { | ||||
|     await server.post("bulk-action/execute", { | ||||
|         noteIds: [ parentNoteId ], | ||||
|         includeDescendants: true, | ||||
|         actions | ||||
|     }); | ||||
|  | ||||
|     await ws.waitForMaxKnownEntityChangeId(); | ||||
|     toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000); | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     addAction, | ||||
|     parseActions, | ||||
|   | ||||
| @@ -4,14 +4,14 @@ import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptio | ||||
| import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; | ||||
| import { focusSavedElement, saveFocusedElement } from "./focus.js"; | ||||
|  | ||||
| export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) { | ||||
| export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) { | ||||
|     if (closeActDialog) { | ||||
|         closeActiveDialog(); | ||||
|         glob.activeDialog = $dialog; | ||||
|     } | ||||
|  | ||||
|     saveFocusedElement(); | ||||
|     Modal.getOrCreateInstance($dialog[0]).show(); | ||||
|     Modal.getOrCreateInstance($dialog[0], config).show(); | ||||
|  | ||||
|     $dialog.on("hidden.bs.modal", () => { | ||||
|         const $autocompleteEl = $(".aa-input"); | ||||
| @@ -41,8 +41,14 @@ async function info(message: string) { | ||||
|     return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Displays a confirmation dialog with the given message. | ||||
|  * | ||||
|  * @param message the message to display in the dialog. | ||||
|  * @returns A promise that resolves to true if the user confirmed, false otherwise. | ||||
|  */ | ||||
| async function confirm(message: string) { | ||||
|     return new Promise((res) => | ||||
|     return new Promise<boolean>((res) => | ||||
|         appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{ | ||||
|             message, | ||||
|             callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed) | ||||
|   | ||||
| @@ -231,6 +231,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) { | ||||
|     let ntxId: string | null = null; | ||||
|     let hoistedNoteId: string | null = null; | ||||
|     let searchString: string | null = null; | ||||
|     let openInPopup = false; | ||||
|  | ||||
|     if (paramString) { | ||||
|         for (const pair of paramString.split("&")) { | ||||
| @@ -246,6 +247,8 @@ export function parseNavigationStateFromUrl(url: string | undefined) { | ||||
|                 searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla | ||||
|             } else if (["viewMode", "attachmentId"].includes(name)) { | ||||
|                 (viewScope as any)[name] = value; | ||||
|             } else if (name === "popup") { | ||||
|                 openInPopup = true; | ||||
|             } else { | ||||
|                 console.warn(`Unrecognized hash parameter '${name}'.`); | ||||
|             } | ||||
| @@ -266,7 +269,8 @@ export function parseNavigationStateFromUrl(url: string | undefined) { | ||||
|         ntxId, | ||||
|         hoistedNoteId, | ||||
|         viewScope, | ||||
|         searchString | ||||
|         searchString, | ||||
|         openInPopup | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @@ -299,11 +303,12 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink); | ||||
|     const { notePath, viewScope, openInPopup } = parseNavigationStateFromUrl(hrefLink); | ||||
|  | ||||
|     const ctrlKey = evt && utils.isCtrlKey(evt); | ||||
|     const shiftKey = evt?.shiftKey; | ||||
|     const isLeftClick = !evt || ("which" in evt && evt.which === 1); | ||||
|     // Right click is handled separately. | ||||
|     const isMiddleClick = evt && "which" in evt && evt.which === 2; | ||||
|     const targetIsBlank = ($link?.attr("target") === "_blank"); | ||||
|     const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank; | ||||
| @@ -311,7 +316,9 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | ||||
|     const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey; | ||||
|  | ||||
|     if (notePath) { | ||||
|         if (openInNewWindow) { | ||||
|         if (isLeftClick && openInPopup) { | ||||
|             appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath }); | ||||
|         } else if (openInNewWindow) { | ||||
|             appContext.triggerCommand("openInWindow", { notePath, viewScope }); | ||||
|         } else if (openInNewTab) { | ||||
|             appContext.tabManager.openTabWithNoteWithHoisting(notePath, { | ||||
| @@ -387,12 +394,18 @@ function linkContextMenu(e: PointerEvent) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (utils.isCtrlKey(e) && e.button === 2) { | ||||
|         appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath }); | ||||
|         e.preventDefault(); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     e.preventDefault(); | ||||
|  | ||||
|     linkContextMenuService.openContextMenu(notePath, e, viewScope, null); | ||||
| } | ||||
|  | ||||
| export async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null | undefined = null) { | ||||
| async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null | undefined = null) { | ||||
|     const $link = $el[0].tagName === "A" ? $el : $el.find("a"); | ||||
|  | ||||
|     href = href || $link.attr("href"); | ||||
|   | ||||
| @@ -40,7 +40,10 @@ interface Options { | ||||
|     allowCreatingNotes?: boolean; | ||||
|     allowJumpToSearchNotes?: boolean; | ||||
|     allowExternalLinks?: boolean; | ||||
|     /** If set, hides the right-side button corresponding to go to selected note. */ | ||||
|     hideGoToSelectedNoteButton?: boolean; | ||||
|     /** If set, hides all right-side buttons in the autocomplete dropdown */ | ||||
|     hideAllButtons?: boolean; | ||||
| } | ||||
|  | ||||
| async function autocompleteSourceForCKEditor(queryText: string) { | ||||
| @@ -190,9 +193,11 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) { | ||||
|  | ||||
|     const $goToSelectedNoteButton = $("<a>").addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right"); | ||||
|  | ||||
|     $el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton); | ||||
|     if (!options.hideAllButtons) { | ||||
|         $el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton); | ||||
|     } | ||||
|  | ||||
|     if (!options.hideGoToSelectedNoteButton) { | ||||
|     if (!options.hideGoToSelectedNoteButton && !options.hideAllButtons) { | ||||
|         $el.after($goToSelectedNoteButton); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import type FBranch from "../entities/fbranch.js"; | ||||
| import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; | ||||
| import type { CKTextEditor } from "@triliumnext/ckeditor5"; | ||||
|  | ||||
| interface CreateNoteOpts { | ||||
| export interface CreateNoteOpts { | ||||
|     isProtected?: boolean; | ||||
|     saveSelection?: boolean; | ||||
|     title?: string | null; | ||||
|   | ||||
| @@ -6,33 +6,18 @@ import TableView from "../widgets/view_widgets/table_view/index.js"; | ||||
| import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js"; | ||||
| import type ViewMode from "../widgets/view_widgets/view_mode.js"; | ||||
|  | ||||
| export type ArgsWithoutNoteId = Omit<ViewModeArgs, "noteIds">; | ||||
| export type ViewTypeOptions = "list" | "grid" | "calendar" | "table" | "geoMap"; | ||||
|  | ||||
| export default class NoteListRenderer { | ||||
|  | ||||
|     private viewType: ViewTypeOptions; | ||||
|     public viewMode: ViewMode<any> | null; | ||||
|     private args: ArgsWithoutNoteId; | ||||
|     public viewMode?: ViewMode<any>; | ||||
|  | ||||
|     constructor(args: ViewModeArgs) { | ||||
|     constructor(args: ArgsWithoutNoteId) { | ||||
|         this.args = args; | ||||
|         this.viewType = this.#getViewType(args.parentNote); | ||||
|  | ||||
|         switch (this.viewType) { | ||||
|             case "list": | ||||
|             case "grid": | ||||
|                 this.viewMode = new ListOrGridView(this.viewType, args); | ||||
|                 break; | ||||
|             case "calendar": | ||||
|                 this.viewMode = new CalendarView(args); | ||||
|                 break; | ||||
|             case "table": | ||||
|                 this.viewMode = new TableView(args); | ||||
|                 break; | ||||
|             case "geoMap": | ||||
|                 this.viewMode = new GeoView(args); | ||||
|                 break; | ||||
|             default: | ||||
|                 this.viewMode = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #getViewType(parentNote: FNote): ViewTypeOptions { | ||||
| @@ -47,15 +32,36 @@ export default class NoteListRenderer { | ||||
|     } | ||||
|  | ||||
|     get isFullHeight() { | ||||
|         return this.viewMode?.isFullHeight; | ||||
|         switch (this.viewType) { | ||||
|             case "list": | ||||
|             case "grid": | ||||
|                 return false; | ||||
|             default: | ||||
|                 return true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async renderList() { | ||||
|         if (!this.viewMode) { | ||||
|             return null; | ||||
|         } | ||||
|         const args = this.args; | ||||
|         const viewMode = this.#buildViewMode(args); | ||||
|         this.viewMode = viewMode; | ||||
|         await viewMode.beforeRender(); | ||||
|         return await viewMode.renderList(); | ||||
|     } | ||||
|  | ||||
|         return await this.viewMode.renderList(); | ||||
|     #buildViewMode(args: ViewModeArgs) { | ||||
|         switch (this.viewType) { | ||||
|             case "calendar": | ||||
|                 return new CalendarView(args); | ||||
|             case "table": | ||||
|                 return new TableView(args); | ||||
|             case "geoMap": | ||||
|                 return new GeoView(args); | ||||
|             case "list": | ||||
|             case "grid": | ||||
|             default: | ||||
|                 return new ListOrGridView(this.viewType, args); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -168,7 +168,10 @@ async function renderTooltip(note: FNote | null) { | ||||
|         if (isContentEmpty) { | ||||
|             classes.push("note-no-content"); | ||||
|         } | ||||
|         content = `<h5 class="${classes.join(" ")}"><a href="#${note.noteId}" data-no-context-menu="true">${noteTitleWithPathAsSuffix.prop("outerHTML")}</a></h5>`; | ||||
|         content = `\ | ||||
|             <h5 class="${classes.join(" ")}"> | ||||
|                 <a href="#${note.noteId}" data-no-context-menu="true">${noteTitleWithPathAsSuffix.prop("outerHTML")}</a> | ||||
|             </h5>`; | ||||
|     } | ||||
|  | ||||
|     content = `${content}<div class="note-tooltip-attributes">${$renderedAttributes[0].outerHTML}</div>`; | ||||
| @@ -176,6 +179,7 @@ async function renderTooltip(note: FNote | null) { | ||||
|         content += $renderedContent[0].outerHTML; | ||||
|     } | ||||
|  | ||||
|     content += `<a class="open-popup-button" title="${t("note_tooltip.quick-edit")}" href="#${note.noteId}?popup"><span class="bx bx-edit" /></a>`; | ||||
|     return content; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -81,8 +81,8 @@ body { | ||||
|  | ||||
|     /* -- Overrides the default colors used by the ckeditor5-image package. --------------------- */ | ||||
|  | ||||
|     --ck-color-image-caption-background: var(--main-background-color); | ||||
|     --ck-color-image-caption-text: var(--main-text-color); | ||||
|     --ck-content-color-image-caption-background: var(--main-background-color); | ||||
|     --ck-content-color-image-caption-text: var(--main-text-color); | ||||
|  | ||||
|     /* -- Overrides the default colors used by the ckeditor5-widget package. -------------------- */ | ||||
|  | ||||
|   | ||||
| @@ -327,7 +327,8 @@ button kbd { | ||||
|     } | ||||
| } | ||||
|  | ||||
| .dropdown-menu { | ||||
| .dropdown-menu, | ||||
| .tabulator-popup-container { | ||||
|     color: var(--menu-text-color) !important; | ||||
|     font-size: inherit; | ||||
|     background-color: var(--menu-background-color) !important; | ||||
| @@ -342,7 +343,8 @@ button kbd { | ||||
|     break-after: avoid; | ||||
| } | ||||
|  | ||||
| body.desktop .dropdown-menu { | ||||
| body.desktop .dropdown-menu, | ||||
| body.desktop .tabulator-popup-container { | ||||
|     border: 1px solid var(--dropdown-border-color); | ||||
|     box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity)); | ||||
|     animation: dropdown-menu-opening 100ms ease-in; | ||||
| @@ -385,7 +387,8 @@ body.desktop .dropdown-menu { | ||||
| } | ||||
|  | ||||
| .dropdown-menu a:hover:not(.disabled), | ||||
| .dropdown-item:hover:not(.disabled, .dropdown-item-container) { | ||||
| .dropdown-item:hover:not(.disabled, .dropdown-item-container), | ||||
| .tabulator-menu-item:hover { | ||||
|     color: var(--hover-item-text-color) !important; | ||||
|     background-color: var(--hover-item-background-color) !important; | ||||
|     border-color: var(--hover-item-border-color) !important; | ||||
| @@ -540,6 +543,7 @@ button.btn-sm { | ||||
|     /* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */ | ||||
|     min-width: 0; | ||||
|     padding: 0; | ||||
|     z-index: 1000; | ||||
| } | ||||
|  | ||||
| pre:not(.hljs) { | ||||
| @@ -771,6 +775,14 @@ table.promoted-attributes-in-tooltip th { | ||||
|     font-size: small; | ||||
| } | ||||
|  | ||||
| .note-tooltip-content .open-popup-button { | ||||
|     position: absolute; | ||||
|     right: 15px; | ||||
|     bottom: 8px; | ||||
|     font-size: 1.2em; | ||||
|     color: inherit; | ||||
| } | ||||
|  | ||||
| .note-tooltip-attributes { | ||||
|     display: -webkit-box; | ||||
|     -webkit-box-orient: vertical; | ||||
| @@ -912,6 +924,13 @@ div[data-notify="container"] { | ||||
|     font-family: var(--monospace-font-family); | ||||
| } | ||||
|  | ||||
| .ck-content { | ||||
|     --ck-content-font-family: var(--detail-font-family); | ||||
|     --ck-content-font-size: 1.1em; | ||||
|     --ck-content-font-color: var(--main-text-color); | ||||
|     --ck-content-line-height: var(--bs-body-line-height); | ||||
| } | ||||
|  | ||||
| .ck-content .table table th { | ||||
|     background-color: var(--accented-background-color); | ||||
| } | ||||
| @@ -1198,12 +1217,14 @@ body.mobile .dropdown-submenu > .dropdown-menu { | ||||
| } | ||||
|  | ||||
| #context-menu-container, | ||||
| #context-menu-container .dropdown-menu { | ||||
|     padding: 3px 0 0; | ||||
| #context-menu-container .dropdown-menu, | ||||
| .tabulator-popup-container { | ||||
|     padding: 3px 0; | ||||
|     z-index: 2000; | ||||
| } | ||||
|  | ||||
| #context-menu-container .dropdown-item { | ||||
| #context-menu-container .dropdown-item, | ||||
| .tabulator-menu .tabulator-menu-item { | ||||
|     padding: 0 7px 0 10px; | ||||
|     cursor: pointer; | ||||
|     user-select: none; | ||||
|   | ||||
							
								
								
									
										199
									
								
								apps/client/src/stylesheets/table.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,199 @@ | ||||
| .tabulator { | ||||
|     --table-background-color: var(--main-background-color); | ||||
|  | ||||
|     --col-header-background-color: var(--main-background-color); | ||||
|     --col-header-hover-background-color: var(--accented-background-color); | ||||
|     --col-header-text-color: var(--main-text-color); | ||||
|     --col-header-arrow-active-color: var(--main-text-color); | ||||
|     --col-header-arrow-inactive-color: var(--more-accented-background-color); | ||||
|     --col-header-separator-border: none; | ||||
|     --col-header-bottom-border: 2px solid var(--main-border-color); | ||||
|  | ||||
|     --row-background-color: var(--main-background-color); | ||||
|     --row-alternate-background-color: var(--main-background-color); | ||||
|     --row-moving-background-color: var(--accented-background-color); | ||||
|     --row-text-color: var(--main-text-color); | ||||
|     --row-delimiter-color: var(--more-accented-background-color); | ||||
|      | ||||
|     --cell-horiz-padding-size: 8px; | ||||
|     --cell-vert-padding-size: 8px; | ||||
|      | ||||
|     --cell-editable-hover-outline-color: var(--main-border-color); | ||||
|     --cell-read-only-text-color: var(--muted-text-color); | ||||
|      | ||||
|     --cell-editing-border-color: var(--main-border-color); | ||||
|     --cell-editing-border-width: 2px; | ||||
|     --cell-editing-background-color: var(--ck-color-selector-focused-cell-background); | ||||
|     --cell-editing-text-color: initial; | ||||
|  | ||||
|     background: unset; | ||||
|     border: unset; | ||||
| } | ||||
|  | ||||
| .tabulator .tabulator-tableholder .tabulator-table { | ||||
|     background: var(--table-background-color); | ||||
| } | ||||
|  | ||||
| /* Column headers */ | ||||
|  | ||||
| .tabulator div.tabulator-header { | ||||
|     border-bottom: var(--col-header-bottom-border); | ||||
|     background: var(--col-header-background-color); | ||||
|     color: var(--col-header-text-color); | ||||
| } | ||||
|  | ||||
| .tabulator .tabulator-col-content { | ||||
|     padding: 8px 4px !important; | ||||
| } | ||||
|  | ||||
| @media (hover: hover) and (pointer: fine) { | ||||
|   .tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover { | ||||
|     background-color: var(--col-header-hover-background-color); | ||||
|   } | ||||
| } | ||||
|  | ||||
| .tabulator div.tabulator-header .tabulator-col.tabulator-moving { | ||||
|     border: none; | ||||
|     background: var(--col-header-hover-background-color); | ||||
| } | ||||
|  | ||||
| .tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow { | ||||
|     border-bottom-color: var(--col-header-arrow-active-color); | ||||
|     border-top-color: var(--col-header-arrow-active-color); | ||||
| } | ||||
|  | ||||
| .tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="none"] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow { | ||||
|     border-bottom-color: var(--col-header-arrow-inactive-color); | ||||
| } | ||||
|  | ||||
| .tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left { | ||||
|     margin-left: var(--cell-editing-border-width); | ||||
| } | ||||
|  | ||||
| .tabulator div.tabulator-header .tabulator-col, | ||||
| .tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left { | ||||
|     background: var(--col-header-background-color); | ||||
|     border-right: var(--col-header-separator-border); | ||||
| } | ||||
|  | ||||
| /* Table body */ | ||||
|  | ||||
| .tabulator-tableholder { | ||||
|     padding-top: 10px; | ||||
|     height: unset !important; /* Don't extend on the full height */ | ||||
| } | ||||
|  | ||||
| /* Rows */ | ||||
|  | ||||
| .tabulator-row .tabulator-cell { | ||||
|     padding: var(--cell-vert-padding-size) var(--cell-horiz-padding-size); | ||||
| } | ||||
|  | ||||
| .tabulator-row .tabulator-cell input { | ||||
|     padding-left: var(--cell-horiz-padding-size) !important; | ||||
|     padding-right: var(--cell-horiz-padding-size) !important; | ||||
| } | ||||
|  | ||||
| .tabulator-row { | ||||
|     background: transparent; | ||||
|     border-top: none; | ||||
|     border-bottom: 1px solid var(--row-delimiter-color); | ||||
|     color: var(--row-text-color); | ||||
| } | ||||
|  | ||||
| .tabulator-row.tabulator-row-odd { | ||||
|     background: var(--row-background-color); | ||||
| } | ||||
|  | ||||
| .tabulator-row.tabulator-row-even { | ||||
|     background: var(--row-alternate-background-color); | ||||
| } | ||||
|  | ||||
| .tabulator-row.tabulator-moving { | ||||
|     border-color: transparent; | ||||
|     background-color: var(--row-moving-background-color); | ||||
| } | ||||
|  | ||||
| /* Cell */ | ||||
|  | ||||
| .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left { | ||||
|     margin-right: var(--cell-editing-border-width); | ||||
| } | ||||
|  | ||||
| .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left, | ||||
| .tabulator-row .tabulator-cell { | ||||
|     border-right-color: transparent; | ||||
| } | ||||
|  | ||||
| .tabulator-row .tabulator-cell:not(.tabulator-editable) { | ||||
|     color: var(--cell-read-only-text-color); | ||||
| } | ||||
|  | ||||
| .tabulator:not(.tabulator-editing) .tabulator-row .tabulator-cell.tabulator-editable:hover { | ||||
|     outline: 2px solid var(--cell-editable-hover-outline-color); | ||||
|     outline-offset: -1px; | ||||
| } | ||||
|  | ||||
| .tabulator-row .tabulator-cell.tabulator-editing { | ||||
|     border-color: transparent; | ||||
| } | ||||
|  | ||||
| .tabulator-row:not(.tabulator-moving) .tabulator-cell.tabulator-editing { | ||||
|     outline: calc(var(--cell-editing-border-width) - 1px) solid var(--cell-editing-border-color); | ||||
|     border-color: var(--cell-editing-border-color); | ||||
|     background: var(--cell-editing-background-color); | ||||
| } | ||||
|  | ||||
| .tabulator-row:not(.tabulator-moving) .tabulator-cell.tabulator-editing > * { | ||||
|     color: var(--cell-editing-text-color); | ||||
| } | ||||
|  | ||||
| .tabulator .tree-collapse, | ||||
| .tabulator .tree-expand { | ||||
|     color: var(--row-text-color); | ||||
| } | ||||
|  | ||||
| /* Align items without children/expander to the ones with. */ | ||||
| .tabulator-cell[tabulator-field="title"] > span:first-child,         /* 1st level */ | ||||
| .tabulator-cell[tabulator-field="title"] > div:first-child + span {  /* sub-level */ | ||||
|     padding-left: 21px; | ||||
| } | ||||
|  | ||||
| /* Checkbox cells */ | ||||
|  | ||||
| .tabulator .tabulator-cell:has(svg), | ||||
| .tabulator .tabulator-cell:has(input[type="checkbox"]) { | ||||
|     padding-left: 8px; | ||||
|     display: inline-flex; | ||||
|     flex-direction: column; | ||||
|     justify-content: center; | ||||
|     align-items: flex-start; | ||||
| } | ||||
|  | ||||
| .tabulator .tabulator-cell input[type="checkbox"] { | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| .tabulator .tabulator-footer { | ||||
|     color: var(--main-text-color); | ||||
| } | ||||
|  | ||||
| /* Context menus */ | ||||
|  | ||||
| .tabulator-popup-container { | ||||
|     min-width: 10em; | ||||
|     border-radius: var(--bs-border-radius); | ||||
| } | ||||
|  | ||||
| .tabulator-menu .tabulator-menu-item { | ||||
|     border: 1px solid transparent; | ||||
|     color: var(--menu-text-color); | ||||
|     font-size: 16px; | ||||
| } | ||||
|  | ||||
| /* Footer */ | ||||
|  | ||||
| :root .tabulator .tabulator-footer { | ||||
|     border-top: unset; | ||||
|     padding: 10px 0; | ||||
| } | ||||
| @@ -4,6 +4,7 @@ | ||||
| @import url(./pages.css); | ||||
| @import url(./ribbon.css); | ||||
| @import url(./notes/text.css); | ||||
| @import url(./notes/collections/table.css); | ||||
|  | ||||
| @font-face { | ||||
|     font-family: "Inter"; | ||||
| @@ -183,7 +184,7 @@ html body .dropdown-item[disabled] { | ||||
|  | ||||
| /* Menu item icon */ | ||||
| .dropdown-item .bx { | ||||
|     transform: translateY(var(--menu-item-icon-vert-offset)); | ||||
|     translate: 0 var(--menu-item-icon-vert-offset); | ||||
|     color: var(--menu-item-icon-color) !important; | ||||
|     font-size: 1.1em; | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,13 @@ | ||||
| :root .tabulator { | ||||
|     --col-header-hover-background-color: var(--hover-item-background-color); | ||||
|     --col-header-arrow-active-color: var(--active-item-text-color); | ||||
|     --col-header-arrow-inactive-color: var(--main-border-color); | ||||
|  | ||||
|     --row-moving-background-color: var(--more-accented-background-color); | ||||
|  | ||||
|     --cell-editable-hover-outline-color: var(--input-focus-outline-color); | ||||
|  | ||||
|     --cell-editing-border-color: var(--input-focus-outline-color); | ||||
|     --cell-editing-background-color: var(--input-background-color); | ||||
|     --cell-editing-text-color: var(--input-text-color); | ||||
| } | ||||
| @@ -1431,7 +1431,6 @@ | ||||
|     "move-to": "移动到...", | ||||
|     "paste-into": "粘贴到里面", | ||||
|     "paste-after": "粘贴到后面", | ||||
|     "duplicate-subtree": "复制子树", | ||||
|     "export": "导出", | ||||
|     "import-into-note": "导入到笔记", | ||||
|     "apply-bulk-actions": "应用批量操作", | ||||
|   | ||||
| @@ -1384,7 +1384,7 @@ | ||||
|     "move-to": "Verschieben nach...", | ||||
|     "paste-into": "Als Unternotiz einfügen", | ||||
|     "paste-after": "Danach einfügen", | ||||
|     "duplicate-subtree": "Notizbaum duplizieren", | ||||
|     "duplicate": "Duplizieren", | ||||
|     "export": "Exportieren", | ||||
|     "import-into-note": "In Notiz importieren", | ||||
|     "apply-bulk-actions": "Massenaktionen ausführen", | ||||
|   | ||||
| @@ -1025,7 +1025,7 @@ | ||||
|     "title": "Consistency Checks", | ||||
|     "find_and_fix_button": "Find and fix consistency issues", | ||||
|     "finding_and_fixing_message": "Finding and fixing consistency issues...", | ||||
|     "issues_fixed_message": "Consistency issues should be fixed." | ||||
|     "issues_fixed_message": "Any consistency issue which may have been found is now fixed." | ||||
|   }, | ||||
|   "database_anonymization": { | ||||
|     "title": "Database Anonymization", | ||||
| @@ -1595,12 +1595,13 @@ | ||||
|     "move-to": "Move to...", | ||||
|     "paste-into": "Paste into", | ||||
|     "paste-after": "Paste after", | ||||
|     "duplicate-subtree": "Duplicate subtree", | ||||
|     "duplicate": "Duplicate", | ||||
|     "export": "Export", | ||||
|     "import-into-note": "Import into note", | ||||
|     "apply-bulk-actions": "Apply bulk actions", | ||||
|     "converted-to-attachments": "{{count}} notes have been converted to attachments.", | ||||
|     "convert-to-attachment-confirm": "Are you sure you want to convert note selected notes into attachments of their parent notes?" | ||||
|     "convert-to-attachment-confirm": "Are you sure you want to convert note selected notes into attachments of their parent notes?", | ||||
|     "open-in-popup": "Quick edit" | ||||
|   }, | ||||
|   "shared_info": { | ||||
|     "shared_publicly": "This note is shared publicly on", | ||||
| @@ -1832,7 +1833,8 @@ | ||||
|   "link_context_menu": { | ||||
|     "open_note_in_new_tab": "Open note in a new tab", | ||||
|     "open_note_in_new_split": "Open note in a new split", | ||||
|     "open_note_in_new_window": "Open note in a new window" | ||||
|     "open_note_in_new_window": "Open note in a new window", | ||||
|     "open_note_in_popup": "Quick edit" | ||||
|   }, | ||||
|   "electron_integration": { | ||||
|     "desktop-application": "Desktop Application", | ||||
| @@ -1852,7 +1854,8 @@ | ||||
|     "full-text-search": "Full text search" | ||||
|   }, | ||||
|   "note_tooltip": { | ||||
|     "note-has-been-deleted": "Note has been deleted." | ||||
|     "note-has-been-deleted": "Note has been deleted.", | ||||
|     "quick-edit": "Quick edit" | ||||
|   }, | ||||
|   "geo-map": { | ||||
|     "create-child-note-title": "Create a new child note and add it to the map", | ||||
| @@ -1941,10 +1944,29 @@ | ||||
|   }, | ||||
|   "table_view": { | ||||
|     "new-row": "New row", | ||||
|     "new-column": "New column" | ||||
|     "new-column": "New column", | ||||
|     "sort-column-by": "Sort by \"{{title}}\"", | ||||
|     "sort-column-ascending": "Ascending", | ||||
|     "sort-column-descending": "Descending", | ||||
|     "sort-column-clear": "Clear sorting", | ||||
|     "hide-column": "Hide column \"{{title}}\"", | ||||
|     "show-hide-columns": "Show/hide columns", | ||||
|     "row-insert-above": "Insert row above", | ||||
|     "row-insert-below": "Insert row below", | ||||
|     "row-insert-child": "Insert child note", | ||||
|     "add-column-to-the-left": "Add column to the left", | ||||
|     "add-column-to-the-right": "Add column to the right", | ||||
|     "edit-column": "Edit column", | ||||
|     "delete_column_confirmation": "Are you sure you want to delete this column? The corresponding attribute will be removed from all notes.", | ||||
|     "delete-column": "Delete column", | ||||
|     "new-column-label": "Label", | ||||
|     "new-column-relation": "Relation" | ||||
|   }, | ||||
|   "book_properties_config": { | ||||
|     "hide-weekends": "Hide weekends", | ||||
|     "display-week-numbers": "Display week numbers" | ||||
|   }, | ||||
|   "table_context_menu": { | ||||
|     "delete_row": "Delete row" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1593,7 +1593,7 @@ | ||||
|     "move-to": "Mover a...", | ||||
|     "paste-into": "Pegar en", | ||||
|     "paste-after": "Pegar después de", | ||||
|     "duplicate-subtree": "Duplicar subárbol", | ||||
|     "duplicate": "Duplicar", | ||||
|     "export": "Exportar", | ||||
|     "import-into-note": "Importar a nota", | ||||
|     "apply-bulk-actions": "Aplicar acciones en lote", | ||||
|   | ||||
| @@ -1389,7 +1389,7 @@ | ||||
|     "move-to": "Déplacer vers...", | ||||
|     "paste-into": "Coller dans", | ||||
|     "paste-after": "Coller après", | ||||
|     "duplicate-subtree": "Dupliquer le sous-arbre", | ||||
|     "duplicate": "Dupliquer", | ||||
|     "export": "Exporter", | ||||
|     "import-into-note": "Importer dans la note", | ||||
|     "apply-bulk-actions": "Appliquer des Actions groupées", | ||||
|   | ||||
| @@ -1349,7 +1349,7 @@ | ||||
|     "copy-note-path-to-clipboard": "Copiază calea notiței în clipboard", | ||||
|     "cut": "Decupează", | ||||
|     "delete": "Șterge", | ||||
|     "duplicate-subtree": "Dublifică ierarhia", | ||||
|     "duplicate": "Dublifică", | ||||
|     "edit-branch-prefix": "Editează prefixul ramurii", | ||||
|     "expand-subtree": "Expandează subnotițele", | ||||
|     "export": "Exportă", | ||||
|   | ||||
| @@ -1336,7 +1336,6 @@ | ||||
|     "move-to": "移動到...", | ||||
|     "paste-into": "貼上到裡面", | ||||
|     "paste-after": "貼上到後面", | ||||
|     "duplicate-subtree": "複製子樹", | ||||
|     "export": "匯出", | ||||
|     "import-into-note": "匯入到筆記", | ||||
|     "apply-bulk-actions": "應用批量操作", | ||||
|   | ||||
| @@ -78,7 +78,7 @@ const TPL = /*html*/` | ||||
|         } | ||||
|     </style> | ||||
|  | ||||
|     <div style="display: flex; justify-content: space-between; margin-bottom: 8px;"> | ||||
|     <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;"> | ||||
|         <h5 class="attr-detail-title">${t("attribute_detail.attr_detail_title")}</h5> | ||||
|  | ||||
|         <span class="bx bx-x close-attr-detail-button tn-tool-button" title="${t("attribute_detail.close_button_title")}"></span> | ||||
| @@ -295,6 +295,8 @@ interface AttributeDetailOpts { | ||||
|     x: number; | ||||
|     y: number; | ||||
|     focus?: "name"; | ||||
|     parent?: HTMLElement; | ||||
|     hideMultiplicity?: boolean; | ||||
| } | ||||
|  | ||||
| interface SearchRelatedResponse { | ||||
| @@ -477,7 +479,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async showAttributeDetail({ allAttributes, attribute, isOwned, x, y, focus }: AttributeDetailOpts) { | ||||
|     async showAttributeDetail({ allAttributes, attribute, isOwned, x, y, focus, hideMultiplicity }: AttributeDetailOpts) { | ||||
|         if (!attribute) { | ||||
|             this.hide(); | ||||
|  | ||||
| @@ -528,7 +530,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { | ||||
|         this.$rowPromotedAlias.toggle(!!definition.isPromoted); | ||||
|         this.$inputPromotedAlias.val(definition.promotedAlias || "").attr("disabled", disabledFn); | ||||
|  | ||||
|         this.$rowMultiplicity.toggle(["label-definition", "relation-definition"].includes(this.attrType || "")); | ||||
|         this.$rowMultiplicity.toggle(["label-definition", "relation-definition"].includes(this.attrType || "") && !hideMultiplicity); | ||||
|         this.$inputMultiplicity.val(definition.multiplicity || "").attr("disabled", disabledFn); | ||||
|  | ||||
|         this.$rowLabelType.toggle(this.attrType === "label-definition"); | ||||
| @@ -560,19 +562,22 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { | ||||
|  | ||||
|         this.toggleInt(true); | ||||
|  | ||||
|         const offset = this.parent?.$widget.offset() || { top: 0, left: 0 }; | ||||
|         const offset = this.parent?.$widget?.offset() || { top: 0, left: 0 }; | ||||
|         const detPosition = this.getDetailPosition(x, offset); | ||||
|         const outerHeight = this.$widget.outerHeight(); | ||||
|         const height = $(window).height(); | ||||
|  | ||||
|         if (detPosition && outerHeight && height) { | ||||
|             this.$widget | ||||
|                 .css("left", detPosition.left) | ||||
|                 .css("right", detPosition.right) | ||||
|                 .css("top", y - offset.top + 70) | ||||
|                 .css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000); | ||||
|         if (!detPosition || !outerHeight || !height) { | ||||
|             console.warn("Can't position popup, is it attached?"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.$widget | ||||
|             .css("left", detPosition.left) | ||||
|             .css("right", detPosition.right) | ||||
|             .css("top", y - offset.top + 70) | ||||
|             .css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000); | ||||
|  | ||||
|         if (focus === "name") { | ||||
|             this.$inputName.trigger("focus").trigger("select"); | ||||
|         } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import noteAutocompleteService, { type Suggestion } from "../../services/note_au | ||||
| import server from "../../services/server.js"; | ||||
| import contextMenuService from "../../menus/context_menu.js"; | ||||
| import attributeParser, { type Attribute } from "../../services/attribute_parser.js"; | ||||
| import { AttributeEditor, type EditorConfig, type Element, type MentionFeed, type Node, type Position } from "@triliumnext/ckeditor5"; | ||||
| import { AttributeEditor, type EditorConfig, type ModelElement, type MentionFeed, type ModelNode, type ModelPosition } from "@triliumnext/ckeditor5"; | ||||
| import froca from "../../services/froca.js"; | ||||
| import attributeRenderer from "../../services/attribute_renderer.js"; | ||||
| import noteCreateService from "../../services/note_create.js"; | ||||
| @@ -417,16 +417,16 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem | ||||
|         this.$editor.tooltip("show"); | ||||
|     } | ||||
|  | ||||
|     getClickIndex(pos: Position) { | ||||
|     getClickIndex(pos: ModelPosition) { | ||||
|         let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0); | ||||
|  | ||||
|         let curNode: Node | Text | Element | null = pos.textNode; | ||||
|         let curNode: ModelNode | Text | ModelElement | null = pos.textNode; | ||||
|  | ||||
|         while (curNode?.previousSibling) { | ||||
|             curNode = curNode.previousSibling; | ||||
|  | ||||
|             if ((curNode as Element).name === "reference") { | ||||
|                 clickIndex += (curNode.getAttribute("notePath") as string).length + 1; | ||||
|             if ((curNode as ModelElement).name === "reference") { | ||||
|                 clickIndex += (curNode.getAttribute("href") as string).length + 1; | ||||
|             } else if ("data" in curNode) { | ||||
|                 clickIndex += (curNode.data as string).length; | ||||
|             } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import type { default as Component, TypedComponent } from "../../components/component.js"; | ||||
| import BasicWidget, { TypedBasicWidget } from "../basic_widget.js"; | ||||
| import type { TypedComponent } from "../../components/component.js"; | ||||
| import { TypedBasicWidget } from "../basic_widget.js"; | ||||
|  | ||||
| export default class Container<T extends TypedComponent<any>> extends TypedBasicWidget<T> { | ||||
|     doRender() { | ||||
|   | ||||
							
								
								
									
										157
									
								
								apps/client/src/widgets/dialogs/popup_editor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,157 @@ | ||||
| import type { EventNames, EventData } from "../../components/app_context.js"; | ||||
| import NoteContext from "../../components/note_context.js"; | ||||
| import { openDialog } from "../../services/dialog.js"; | ||||
| import BasicWidget from "../basic_widget.js"; | ||||
| import Container from "../containers/container.js"; | ||||
| import TypeWidget from "../type_widgets/type_widget.js"; | ||||
|  | ||||
| const TPL = /*html*/`\ | ||||
| <div class="popup-editor-dialog modal fade mx-auto" tabindex="-1" role="dialog"> | ||||
|     <style> | ||||
|         body.desktop .modal.popup-editor-dialog .modal-dialog { | ||||
|             max-width: 75vw; | ||||
|         } | ||||
|  | ||||
|         .modal.popup-editor-dialog .modal-header .modal-title { | ||||
|             font-size: 1.1em; | ||||
|         } | ||||
|  | ||||
|         .modal.popup-editor-dialog .modal-body { | ||||
|             padding: 0; | ||||
|             height: 75vh; | ||||
|             overflow: auto; | ||||
|         } | ||||
|  | ||||
|         .modal.popup-editor-dialog .note-detail-editable-text { | ||||
|             padding: 0 1em; | ||||
|         } | ||||
|  | ||||
|         .modal.popup-editor-dialog .title-row, | ||||
|         .modal.popup-editor-dialog .modal-title, | ||||
|         .modal.popup-editor-dialog .note-icon-widget { | ||||
|             height: 32px; | ||||
|         } | ||||
|  | ||||
|         .modal.popup-editor-dialog .note-icon-widget { | ||||
|             width: 32px; | ||||
|             margin: 0; | ||||
|             padding: 0; | ||||
|         } | ||||
|  | ||||
|         .modal.popup-editor-dialog .note-icon-widget button.note-icon, | ||||
|         .modal.popup-editor-dialog .note-title-widget input.note-title { | ||||
|             font-size: 1em; | ||||
|         } | ||||
|  | ||||
|         .modal.popup-editor-dialog .classic-toolbar-widget { | ||||
|             position: sticky; | ||||
|             top: 0; | ||||
|             left: 0; | ||||
|             right: 0; | ||||
|             background: var(--modal-background-color); | ||||
|             z-index: 998; | ||||
|         } | ||||
|  | ||||
|         .modal.popup-editor-dialog .note-detail-file { | ||||
|             padding: 0; | ||||
|         } | ||||
|     </style> | ||||
|  | ||||
|     <div class="modal-dialog modal-lg" role="document"> | ||||
|         <div class="modal-content"> | ||||
|             <div class="modal-header"> | ||||
|                 <div class="modal-title"> | ||||
|                     <!-- This is where the first child will be injected --> | ||||
|                 </div> | ||||
|                 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | ||||
|             </div> | ||||
|  | ||||
|             <div class="modal-body"> | ||||
|                 <!-- This is where all but the first child will be injected. --> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| `; | ||||
|  | ||||
| export default class PopupEditorDialog extends Container<BasicWidget> { | ||||
|  | ||||
|     private noteContext: NoteContext; | ||||
|     private $modalHeader!: JQuery<HTMLElement>; | ||||
|     private $modalBody!: JQuery<HTMLElement>; | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.noteContext = new NoteContext("_popup-editor"); | ||||
|     } | ||||
|  | ||||
|     doRender() { | ||||
|         // This will populate this.$widget with the content of the children. | ||||
|         super.doRender(); | ||||
|  | ||||
|         // Now we wrap it in the modal. | ||||
|         const $newWidget = $(TPL); | ||||
|         this.$modalHeader = $newWidget.find(".modal-title"); | ||||
|         this.$modalBody = $newWidget.find(".modal-body"); | ||||
|  | ||||
|         const children = this.$widget.children(); | ||||
|         this.$modalHeader.append(children[0]); | ||||
|         this.$modalBody.append(children.slice(1)); | ||||
|         this.$widget = $newWidget; | ||||
|         this.setVisibility(false); | ||||
|     } | ||||
|  | ||||
|     async openInPopupEvent({ noteIdOrPath }: EventData<"openInPopup">) { | ||||
|         const $dialog = await openDialog(this.$widget, false, { | ||||
|             focus: false | ||||
|         }); | ||||
|  | ||||
|         await this.noteContext.setNote(noteIdOrPath); | ||||
|  | ||||
|         const activeEl = document.activeElement; | ||||
|         if (activeEl && "blur" in activeEl) { | ||||
|             (activeEl as HTMLElement).blur(); | ||||
|         } | ||||
|  | ||||
|         $dialog.on("shown.bs.modal", async () => { | ||||
|             // Reduce the z-index of modals so that ckeditor popups are properly shown on top of it. | ||||
|             // The backdrop instance is not shared so it's OK to make a one-off modification. | ||||
|             $("body > .modal-backdrop").css("z-index", "998"); | ||||
|             $dialog.css("z-index", "999"); | ||||
|  | ||||
|             await this.handleEventInChildren("activeContextChanged", { noteContext: this.noteContext }); | ||||
|             this.setVisibility(true); | ||||
|             await this.handleEventInChildren("focusOnDetail", { ntxId: this.noteContext.ntxId }); | ||||
|         }); | ||||
|         $dialog.on("hidden.bs.modal", () => { | ||||
|             const $typeWidgetEl = $dialog.find(".note-detail-printable"); | ||||
|             if ($typeWidgetEl.length) { | ||||
|                 const typeWidget = glob.getComponentByEl($typeWidgetEl[0]) as TypeWidget; | ||||
|                 typeWidget.cleanup(); | ||||
|             } | ||||
|  | ||||
|             this.setVisibility(false); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     setVisibility(visible: boolean) { | ||||
|         const $bodyItems = this.$modalBody.find("> div"); | ||||
|         if (visible) { | ||||
|             $bodyItems.fadeIn(); | ||||
|             this.$modalHeader.children().show(); | ||||
|         } else { | ||||
|             $bodyItems.hide(); | ||||
|             this.$modalHeader.children().hide(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null { | ||||
|         // Avoid events related to the current tab interfere with our popup. | ||||
|         if (["noteSwitched", "noteSwitchedAndActivated"].includes(name)) { | ||||
|             return Promise.resolve(); | ||||
|         } | ||||
|  | ||||
|         return super.handleEventInChildren(name, data); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -31,8 +31,8 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = { | ||||
| }; | ||||
|  | ||||
| export const byBookType: Record<ViewTypeOptions, string | null> = { | ||||
|     list: null, | ||||
|     grid: null, | ||||
|     list: "mULW0Q3VojwY", | ||||
|     grid: "8QqnMzx393bx", | ||||
|     calendar: "xWbu3jpNWapp", | ||||
|     table: "2FvYrpmOXm29", | ||||
|     geoMap: "81SGnPGMk7Xc" | ||||
|   | ||||
| @@ -195,7 +195,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
|         // https://github.com/zadam/trilium/issues/2522 | ||||
|         const isBackendNote = this.noteContext?.noteId === "_backendLog"; | ||||
|         const isSqlNote = this.mime === "text/x-sqlite;schema=trilium"; | ||||
|         const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "mermaid"].includes(this.type ?? ""); | ||||
|         const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "mermaid", "file"].includes(this.type ?? ""); | ||||
|         const isFullHeight = (!this.noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote) | ||||
|             || this.noteContext?.viewScope?.viewMode === "attachments" | ||||
|             || isBackendNote; | ||||
|   | ||||
| @@ -3,8 +3,6 @@ import NoteListRenderer from "../services/note_list_renderer.js"; | ||||
| import type FNote from "../entities/fnote.js"; | ||||
| import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData, EventNames } from "../components/app_context.js"; | ||||
| import type ViewMode from "./view_widgets/view_mode.js"; | ||||
| import AttributeDetailWidget from "./attribute_widgets/attribute_detail.js"; | ||||
| import { Attribute } from "../services/attribute_parser.js"; | ||||
|  | ||||
| const TPL = /*html*/` | ||||
| <div class="note-list-widget"> | ||||
| @@ -39,24 +37,36 @@ export default class NoteListWidget extends NoteContextAwareWidget { | ||||
|     private noteIdRefreshed?: string; | ||||
|     private shownNoteId?: string | null; | ||||
|     private viewMode?: ViewMode<any> | null; | ||||
|     private attributeDetailWidget: AttributeDetailWidget; | ||||
|     private displayOnlyCollections: boolean; | ||||
|  | ||||
|     constructor() { | ||||
|     /** | ||||
|      * @param displayOnlyCollections if set to `true` then only collection-type views are displayed such as geo-map and the calendar. The original book types grid and list will be ignored. | ||||
|      */ | ||||
|     constructor(displayOnlyCollections: boolean) { | ||||
|         super(); | ||||
|         this.attributeDetailWidget = new AttributeDetailWidget() | ||||
|                 .contentSized() | ||||
|                 .setParent(this); | ||||
|  | ||||
|         this.displayOnlyCollections = displayOnlyCollections; | ||||
|     } | ||||
|  | ||||
|     isEnabled() { | ||||
|         return super.isEnabled() && this.noteContext?.hasNoteList(); | ||||
|         if (!super.isEnabled()) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (this.displayOnlyCollections && this.note?.type !== "book") { | ||||
|             const viewType = this.note?.getLabelValue("viewType"); | ||||
|             if (!viewType || ["grid", "list"].includes(viewType)) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return this.noteContext?.hasNoteList(); | ||||
|     } | ||||
|  | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|         this.contentSized(); | ||||
|         this.$content = this.$widget.find(".note-list-widget-content"); | ||||
|         this.$widget.append(this.attributeDetailWidget.render()); | ||||
|  | ||||
|         const observer = new IntersectionObserver( | ||||
|             (entries) => { | ||||
| @@ -75,23 +85,6 @@ export default class NoteListWidget extends NoteContextAwareWidget { | ||||
|         setTimeout(() => observer.observe(this.$widget[0]), 10); | ||||
|     } | ||||
|  | ||||
|     addNoteListItemEvent() { | ||||
|         const attr: Attribute = { | ||||
|             type: "label", | ||||
|             name: "label:myLabel", | ||||
|             value: "promoted,single,text" | ||||
|         }; | ||||
|  | ||||
|         this.attributeDetailWidget!.showAttributeDetail({ | ||||
|             attribute: attr, | ||||
|             allAttributes: [ attr ], | ||||
|             isOwned: true, | ||||
|             x: 100, | ||||
|             y: 200, | ||||
|             focus: "name" | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     checkRenderStatus() { | ||||
|         // console.log("this.isIntersecting", this.isIntersecting); | ||||
|         // console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId); | ||||
| @@ -107,8 +100,7 @@ export default class NoteListWidget extends NoteContextAwareWidget { | ||||
|         const noteListRenderer = new NoteListRenderer({ | ||||
|             $parent: this.$content, | ||||
|             parentNote: note, | ||||
|             parentNotePath: this.notePath, | ||||
|             noteIds: note.getChildNoteIds() | ||||
|             parentNotePath: this.notePath | ||||
|         }); | ||||
|         this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight); | ||||
|         await noteListRenderer.renderList(); | ||||
| @@ -153,12 +145,6 @@ export default class NoteListWidget extends NoteContextAwareWidget { | ||||
|             this.refresh(); | ||||
|             this.checkRenderStatus(); | ||||
|         } | ||||
|  | ||||
|         // Inform the view mode of changes and refresh if needed. | ||||
|         if (this.viewMode && this.viewMode.onEntitiesReloaded(e)) { | ||||
|             this.refresh(); | ||||
|             this.checkRenderStatus(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) { | ||||
|   | ||||
| @@ -240,24 +240,25 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | ||||
|         this.$tree.on("mousedown", ".fancytree-title", (e) => { | ||||
|             if (e.which === 2) { | ||||
|                 const node = $.ui.fancytree.getNode(e as unknown as Event); | ||||
|  | ||||
|                 const notePath = treeService.getNotePath(node); | ||||
|  | ||||
|                 if (notePath) { | ||||
|                     e.stopPropagation(); | ||||
|                     e.preventDefault(); | ||||
|  | ||||
|                     appContext.tabManager.openTabWithNoteWithHoisting(notePath, { | ||||
|                         activate: e.shiftKey ? true : false | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|             } | ||||
|         }); | ||||
|         this.$tree.on("mouseup", ".fancytree-title", (e) => { | ||||
|             // Prevent middle click from pasting in the editor. | ||||
|             if (e.which === 2) { | ||||
|                 e.stopPropagation(); | ||||
|                 e.preventDefault(); | ||||
|             } | ||||
|         }); | ||||
|         this.$tree.on("auxclick", (e) => { | ||||
|             // Prevent middle click from pasting in the editor. | ||||
|             e.stopPropagation(); | ||||
|             e.preventDefault(); | ||||
|         }); | ||||
|  | ||||
|         this.$treeSettingsPopup = this.$widget.find(".tree-settings-popup"); | ||||
|         this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find(".hide-archived-notes"); | ||||
| @@ -712,7 +713,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | ||||
|             }); | ||||
|         } else { | ||||
|             this.$tree.on("contextmenu", ".fancytree-node", (e) => { | ||||
|                 this.showContextMenu(e); | ||||
|                 if (!utils.isCtrlKey(e)) { | ||||
|                     this.showContextMenu(e); | ||||
|                 } else { | ||||
|                     const node = $.ui.fancytree.getNode(e as unknown as Event); | ||||
|                     const notePath = treeService.getNotePath(node); | ||||
|                     appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath }); | ||||
|                 } | ||||
|                 return false; // blocks default browser right click menu | ||||
|             }); | ||||
|  | ||||
|   | ||||
| @@ -23,10 +23,15 @@ const TPL = /*html*/` | ||||
|             align-items: center; | ||||
|         } | ||||
|  | ||||
|         .book-properties-container > * { | ||||
|         .book-properties-container > div { | ||||
|             margin-right: 15px; | ||||
|         } | ||||
|  | ||||
|         .book-properties-container > .type-number > label { | ||||
|             display: flex; | ||||
|             align-items: baseline; | ||||
|         } | ||||
|  | ||||
|         .book-properties-container input[type="checkbox"] { | ||||
|             margin-right: 5px; | ||||
|         } | ||||
| @@ -127,6 +132,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget { | ||||
|  | ||||
|     renderBookProperty(property: BookProperty) { | ||||
|         const $container = $("<div>"); | ||||
|         $container.addClass(`type-${property.type}`); | ||||
|         const note = this.note; | ||||
|         if (!note) { | ||||
|             return $container; | ||||
| @@ -168,6 +174,27 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget { | ||||
|                 }); | ||||
|                 $container.append($button); | ||||
|                 break; | ||||
|             case "number": | ||||
|                 const $numberInput = $("<input>", { | ||||
|                     type: "number", | ||||
|                     class: "form-control form-control-sm", | ||||
|                     value: note.getLabelValue(property.bindToLabel) || "", | ||||
|                     width: property.width ?? 100, | ||||
|                     min: property.min ?? 0 | ||||
|                 }); | ||||
|                 $numberInput.on("change", () => { | ||||
|                     const value = $numberInput.val(); | ||||
|                     if (value === "") { | ||||
|                         attributes.removeOwnedLabelByName(note, property.bindToLabel); | ||||
|                     } else { | ||||
|                         attributes.setLabel(note.noteId, property.bindToLabel, String(value)); | ||||
|                     } | ||||
|                 }); | ||||
|                 $container.append($("<label>") | ||||
|                     .text(property.label) | ||||
|                     .append(" ".repeat(2)) | ||||
|                     .append($numberInput)); | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         return $container; | ||||
|   | ||||
| @@ -4,8 +4,6 @@ import attributes from "../../services/attributes"; | ||||
| import { ViewTypeOptions } from "../../services/note_list_renderer" | ||||
| import NoteContextAwareWidget from "../note_context_aware_widget"; | ||||
|  | ||||
| export type BookProperty = CheckBoxProperty | ButtonProperty; | ||||
|  | ||||
| interface BookConfig { | ||||
|     properties: BookProperty[]; | ||||
| } | ||||
| @@ -24,6 +22,16 @@ interface ButtonProperty { | ||||
|     onClick: (context: BookContext) => void; | ||||
| } | ||||
|  | ||||
| interface NumberProperty { | ||||
|     type: "number", | ||||
|     label: string; | ||||
|     bindToLabel: string; | ||||
|     width?: number; | ||||
|     min?: number; | ||||
| } | ||||
|  | ||||
| export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty; | ||||
|  | ||||
| interface BookContext { | ||||
|     note: FNote; | ||||
|     triggerCommand: NoteContextAwareWidget["triggerCommand"]; | ||||
| @@ -85,6 +93,13 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = { | ||||
|         properties: [] | ||||
|     }, | ||||
|     table: { | ||||
|         properties: [] | ||||
|         properties: [ | ||||
|             { | ||||
|                 label: "Max nesting depth:", | ||||
|                 type: "number", | ||||
|                 bindToLabel: "maxNestingDepth", | ||||
|                 width: 65 | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -48,6 +48,18 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget { | ||||
|         this.contentSized(); | ||||
|     } | ||||
|  | ||||
|     isEnabled(): boolean | null | undefined { | ||||
|         if (options.get("textNoteEditorType") !== "ckeditor-classic") { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (!this.note || this.note.type !== "text") { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     async getTitle() { | ||||
|         return { | ||||
|             show: await this.#shouldDisplay(), | ||||
| @@ -58,11 +70,7 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget { | ||||
|     } | ||||
|  | ||||
|     async #shouldDisplay() { | ||||
|         if (options.get("textNoteEditorType") !== "ckeditor-classic") { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (!this.note || this.note.type !== "text") { | ||||
|         if (!this.isEnabled()) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -69,11 +69,6 @@ interface AttributeResult { | ||||
|     attributeId: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This widget is quite special because it's used in the desktop ribbon, but in mobile outside of ribbon. | ||||
|  * This works without many issues (apart from autocomplete), but it should be kept in mind when changing things | ||||
|  * and testing. | ||||
|  */ | ||||
| export default class PromotedAttributesWidget extends NoteContextAwareWidget { | ||||
|  | ||||
|     private $container!: JQuery<HTMLElement>; | ||||
|   | ||||
| @@ -68,7 +68,6 @@ export default class SearchResultWidget extends NoteContextAwareWidget { | ||||
|         const noteListRenderer = new NoteListRenderer({ | ||||
|             $parent: this.$content, | ||||
|             parentNote: note, | ||||
|             noteIds: note.getChildNoteIds(), | ||||
|             showNotePath: true | ||||
|         }); | ||||
|         await noteListRenderer.renderList(); | ||||
|   | ||||
| @@ -59,7 +59,7 @@ async function handleContentUpdate(affectedNoteIds: string[]) { | ||||
|     const templateNoteIds = new Set(templateCache.keys()); | ||||
|     const affectedTemplateNoteIds = templateNoteIds.intersection(updatedNoteIds); | ||||
|  | ||||
|     await froca.getNotes(affectedNoteIds); | ||||
|     await froca.getNotes(affectedNoteIds, true); | ||||
|  | ||||
|     let fullReloadNeeded = false; | ||||
|     for (const affectedTemplateNoteId of affectedTemplateNoteIds) { | ||||
|   | ||||
| @@ -178,13 +178,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|             }); | ||||
|  | ||||
|             if (isClassicEditor) { | ||||
|                 let $classicToolbarWidget; | ||||
|                 if (!utils.isMobile()) { | ||||
|                     const $parentSplit = this.$widget.parents(".note-split.type-text"); | ||||
|                     $classicToolbarWidget = $parentSplit.find("> .ribbon-container .classic-toolbar-widget"); | ||||
|                 } else { | ||||
|                     $classicToolbarWidget = $("body").find(".classic-toolbar-widget"); | ||||
|                 } | ||||
|                 const $classicToolbarWidget = this.findClassicToolbar(); | ||||
|  | ||||
|                 $classicToolbarWidget.empty(); | ||||
|                 if ($classicToolbarWidget.length) { | ||||
| @@ -271,7 +265,12 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|     } | ||||
|  | ||||
|     focus() { | ||||
|         this.$editor.trigger("focus"); | ||||
|         const editor = this.watchdog.editor; | ||||
|         if (editor) { | ||||
|             editor.editing.view.focus(); | ||||
|         } else { | ||||
|             this.$editor.trigger("focus"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     scrollToEnd() { | ||||
| @@ -515,6 +514,22 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     findClassicToolbar(): JQuery<HTMLElement> { | ||||
|         if (!utils.isMobile()) { | ||||
|             const $parentSplit = this.$widget.parents(".note-split.type-text"); | ||||
|  | ||||
|             if ($parentSplit.length) { | ||||
|                 // The editor is in a normal tab. | ||||
|                 return $parentSplit.find("> .ribbon-container .classic-toolbar-widget"); | ||||
|             } else { | ||||
|                 // The editor is in a popup. | ||||
|                 return this.$widget.closest(".modal-body").find(".classic-toolbar-widget"); | ||||
|             } | ||||
|         } else { | ||||
|             return $("body").find(".classic-toolbar-widget"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) { | ||||
|         const { TouchBar, buildIcon } = data; | ||||
|         const { TouchBarSegmentedControl, TouchBarGroup, TouchBarButton } = TouchBar; | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import TypeWidget from "./type_widget.js"; | ||||
| import appContext from "../../components/app_context.js"; | ||||
| import searchService from "../../services/search.js"; | ||||
| import { t } from "../../services/i18n.js"; | ||||
| import type FNote from "../../entities/fnote.js"; | ||||
|  | ||||
| const TPL = /*html*/` | ||||
| <div class="note-detail-empty note-detail-printable"> | ||||
|   | ||||
| @@ -22,7 +22,8 @@ const TPL = /*html*/` | ||||
|             padding: 0; | ||||
|         } | ||||
|  | ||||
|         .note-split.full-content-width .note-detail-file[data-preview-type="video"] { | ||||
|         .note-detail.full-height .note-detail-file[data-preview-type="pdf"], | ||||
|         .note-detail.full-height .note-detail-file[data-preview-type="video"] { | ||||
|             overflow: hidden; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -71,6 +71,17 @@ export default abstract class TypeWidget extends NoteContextAwareWidget { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     activeNoteChangedEvent() { | ||||
|         if (!this.isActiveNoteContext()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Restore focus to the editor when switching tabs, but only if the note tree is not already focused. | ||||
|         if (!document.activeElement?.classList.contains("fancytree-title")) { | ||||
|             this.focus(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * {@inheritdoc} | ||||
|      * | ||||
|   | ||||
| @@ -113,7 +113,6 @@ export default class CalendarView extends ViewMode<{}> { | ||||
|  | ||||
|     private $root: JQuery<HTMLElement>; | ||||
|     private $calendarContainer: JQuery<HTMLElement>; | ||||
|     private noteIds: string[]; | ||||
|     private calendar?: Calendar; | ||||
|     private isCalendarRoot: boolean; | ||||
|     private lastView?: string; | ||||
| @@ -124,15 +123,10 @@ export default class CalendarView extends ViewMode<{}> { | ||||
|  | ||||
|         this.$root = $(TPL); | ||||
|         this.$calendarContainer = this.$root.find(".calendar-container"); | ||||
|         this.noteIds = args.noteIds; | ||||
|         this.isCalendarRoot = false; | ||||
|         args.$parent.append(this.$root); | ||||
|     } | ||||
|  | ||||
|     get isFullHeight(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     async renderList(): Promise<JQuery<HTMLElement> | undefined> { | ||||
|         this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot"); | ||||
|         const isEditable = !this.isCalendarRoot; | ||||
| @@ -225,6 +219,7 @@ export default class CalendarView extends ViewMode<{}> { | ||||
|                     $(mainContainer ?? e.el).append($(promotedAttributesHtml)); | ||||
|                 } | ||||
|             }, | ||||
|             // Called upon when clicking the day number in the calendar, opens or creates the day note but only if in a calendar root. | ||||
|             dateClick: async (e) => { | ||||
|                 if (!this.isCalendarRoot) { | ||||
|                     return; | ||||
| @@ -232,7 +227,8 @@ export default class CalendarView extends ViewMode<{}> { | ||||
|  | ||||
|                 const note = await date_notes.getDayNote(e.dateStr); | ||||
|                 if (note) { | ||||
|                     appContext.tabManager.getActiveContext()?.setNote(note.noteId); | ||||
|                     appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId }); | ||||
|                     appContext.triggerCommand("refreshNoteList", { noteId: this.parentNote.noteId }); | ||||
|                 } | ||||
|             }, | ||||
|             datesSet: (e) => this.#onDatesSet(e), | ||||
| @@ -394,7 +390,7 @@ export default class CalendarView extends ViewMode<{}> { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { | ||||
|     async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { | ||||
|         // Refresh note IDs if they got changed. | ||||
|         if (loadResults.getBranchRows().some((branch) => branch.parentNoteId === this.parentNote.noteId)) { | ||||
|             this.noteIds = this.parentNote.getChildNoteIds(); | ||||
| @@ -405,9 +401,14 @@ export default class CalendarView extends ViewMode<{}> { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         // Refresh on note title change. | ||||
|         if (loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId))) { | ||||
|             this.calendar?.refetchEvents(); | ||||
|         } | ||||
|  | ||||
|         // Refresh dataset on subnote change. | ||||
|         if (this.calendar && loadResults.getAttributeRows().some((a) => this.noteIds.includes(a.noteId ?? ""))) { | ||||
|             this.calendar.refetchEvents(); | ||||
|         if (loadResults.getAttributeRows().some((a) => this.noteIds.includes(a.noteId ?? ""))) { | ||||
|             this.calendar?.refetchEvents(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -436,7 +437,7 @@ export default class CalendarView extends ViewMode<{}> { | ||||
|             events.push(await CalendarView.buildEvent(dateNote, { startDate })); | ||||
|  | ||||
|             if (dateNote.hasChildren()) { | ||||
|                 const childNoteIds = dateNote.getChildNoteIds(); | ||||
|                 const childNoteIds = await dateNote.getSubtreeNoteIds(); | ||||
|                 for (const childNoteId of childNoteIds) { | ||||
|                     childNoteToDateMapping[childNoteId] = startDate; | ||||
|                 } | ||||
| @@ -462,13 +463,6 @@ export default class CalendarView extends ViewMode<{}> { | ||||
|         for (const note of notes) { | ||||
|             const startDate = CalendarView.#getCustomisableLabel(note, "startDate", "calendar:startDate"); | ||||
|  | ||||
|             if (note.hasChildren()) { | ||||
|                 const childrenEventData = await this.buildEvents(note.getChildNoteIds()); | ||||
|                 if (childrenEventData.length > 0) { | ||||
|                     events.push(childrenEventData); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (!startDate) { | ||||
|                 continue; | ||||
|             } | ||||
| @@ -533,7 +527,7 @@ export default class CalendarView extends ViewMode<{}> { | ||||
|             const eventData: EventInput = { | ||||
|                 title: title, | ||||
|                 start: startDate, | ||||
|                 url: `#${note.noteId}`, | ||||
|                 url: `#${note.noteId}?popup`, | ||||
|                 noteId: note.noteId, | ||||
|                 color: color ?? undefined, | ||||
|                 iconClass: note.getLabelValue("iconClass"), | ||||
|   | ||||
| @@ -29,6 +29,11 @@ const TPL = /*html*/` | ||||
|             z-index: 1; | ||||
|         } | ||||
|  | ||||
|         .leaflet-top, | ||||
|         .leaflet-bottom { | ||||
|             z-index: 997; | ||||
|         } | ||||
|  | ||||
|         .geo-map-container.placing-note { | ||||
|             cursor: crosshair; | ||||
|         } | ||||
| @@ -221,7 +226,7 @@ export default class GeoView extends ViewMode<MapData> { | ||||
|  | ||||
|         // Add the new markers. | ||||
|         this.currentMarkerData = {}; | ||||
|         const notes = await this.parentNote.getChildNotes(); | ||||
|         const notes = await this.parentNote.getSubtreeNotes(); | ||||
|         const draggable = !this.isReadOnly; | ||||
|         for (const childNote of notes) { | ||||
|             if (childNote.mime === "application/gpx+xml") { | ||||
| @@ -238,10 +243,6 @@ export default class GeoView extends ViewMode<MapData> { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     get isFullHeight(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     #changeState(newState: State) { | ||||
|         this._state = newState; | ||||
|         this.$container.toggleClass("placing-note", newState === State.NewNote); | ||||
| @@ -250,7 +251,7 @@ export default class GeoView extends ViewMode<MapData> { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { | ||||
|     async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { | ||||
|         // If any of the children branches are altered. | ||||
|         if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.parentNote.noteId)) { | ||||
|             this.#reloadMarkers(); | ||||
|   | ||||
| @@ -36,10 +36,17 @@ export default function processNoteWithMarker(map: Map, note: FNote, location: s | ||||
|             return true; | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     newMarker.on("contextmenu", (e) => { | ||||
|         openContextMenu(note.noteId, e, isEditable); | ||||
|     }); | ||||
|  | ||||
|     if (!isEditable) { | ||||
|         newMarker.on("click", (e) => { | ||||
|             appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     return newMarker; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -161,7 +161,7 @@ const TPL = /*html*/` | ||||
| class ListOrGridView extends ViewMode<{}> { | ||||
|     private $noteList: JQuery<HTMLElement>; | ||||
|  | ||||
|     private noteIds: string[]; | ||||
|     private filteredNoteIds!: string[]; | ||||
|     private page?: number; | ||||
|     private pageSize?: number; | ||||
|     private showNotePath?: boolean; | ||||
| @@ -174,13 +174,6 @@ class ListOrGridView extends ViewMode<{}> { | ||||
|         super(args, viewType); | ||||
|         this.$noteList = $(TPL); | ||||
|  | ||||
|         const includedNoteIds = this.getIncludedNoteIds(); | ||||
|  | ||||
|         this.noteIds = args.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); | ||||
|  | ||||
|         if (this.noteIds.length === 0) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         args.$parent.append(this.$noteList); | ||||
|  | ||||
| @@ -204,8 +197,14 @@ class ListOrGridView extends ViewMode<{}> { | ||||
|         return new Set(includedLinks.map((rel) => rel.value)); | ||||
|     } | ||||
|  | ||||
|     async beforeRender() { | ||||
|         super.beforeRender(); | ||||
|         const includedNoteIds = this.getIncludedNoteIds(); | ||||
|         this.filteredNoteIds = this.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); | ||||
|     } | ||||
|  | ||||
|     async renderList() { | ||||
|         if (this.noteIds.length === 0 || !this.page || !this.pageSize) { | ||||
|         if (this.filteredNoteIds.length === 0 || !this.page || !this.pageSize) { | ||||
|             this.$noteList.hide(); | ||||
|             return; | ||||
|         } | ||||
| @@ -226,7 +225,7 @@ class ListOrGridView extends ViewMode<{}> { | ||||
|         const startIdx = (this.page - 1) * this.pageSize; | ||||
|         const endIdx = startIdx + this.pageSize; | ||||
|  | ||||
|         const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length)); | ||||
|         const pageNoteIds = this.filteredNoteIds.slice(startIdx, Math.min(endIdx, this.filteredNoteIds.length)); | ||||
|         const pageNotes = await froca.getNotes(pageNoteIds); | ||||
|  | ||||
|         for (const note of pageNotes) { | ||||
| @@ -246,7 +245,7 @@ class ListOrGridView extends ViewMode<{}> { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const pageCount = Math.ceil(this.noteIds.length / this.pageSize); | ||||
|         const pageCount = Math.ceil(this.filteredNoteIds.length / this.pageSize); | ||||
|  | ||||
|         $pager.toggle(pageCount > 1); | ||||
|  | ||||
| @@ -257,7 +256,7 @@ class ListOrGridView extends ViewMode<{}> { | ||||
|                 lastPrinted = true; | ||||
|  | ||||
|                 const startIndex = (i - 1) * this.pageSize + 1; | ||||
|                 const endIndex = Math.min(this.noteIds.length, i * this.pageSize); | ||||
|                 const endIndex = Math.min(this.filteredNoteIds.length, i * this.pageSize); | ||||
|  | ||||
|                 $pager.append( | ||||
|                     i === this.page | ||||
| @@ -279,7 +278,7 @@ class ListOrGridView extends ViewMode<{}> { | ||||
|         } | ||||
|  | ||||
|         // no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all | ||||
|         $pager.append(`<span class="note-list-pager-total-count">(${this.noteIds.length} notes)</span>`); | ||||
|         $pager.append(`<span class="note-list-pager-total-count">(${this.filteredNoteIds.length} notes)</span>`); | ||||
|     } | ||||
|  | ||||
|     async renderNote(note: FNote, expand: boolean = false) { | ||||
|   | ||||
| @@ -0,0 +1,31 @@ | ||||
| import { executeBulkActions } from "../../../services/bulk_action.js"; | ||||
|  | ||||
| export async function renameColumn(parentNoteId: string, type: "label" | "relation", originalName: string, newName: string) { | ||||
|     if (type === "label") { | ||||
|         return executeBulkActions(parentNoteId, [{ | ||||
|             name: "renameLabel", | ||||
|             oldLabelName: originalName, | ||||
|             newLabelName: newName | ||||
|         }]); | ||||
|     } else { | ||||
|         return executeBulkActions(parentNoteId, [{ | ||||
|             name: "renameRelation", | ||||
|             oldRelationName: originalName, | ||||
|             newRelationName: newName | ||||
|         }]); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export async function deleteColumn(parentNoteId: string, type: "label" | "relation", columnName: string) { | ||||
|     if (type === "label") { | ||||
|         return executeBulkActions(parentNoteId, [{ | ||||
|             name: "deleteLabel", | ||||
|             labelName: columnName | ||||
|         }]); | ||||
|     } else { | ||||
|         return executeBulkActions(parentNoteId, [{ | ||||
|             name: "deleteRelation", | ||||
|             relationName: columnName | ||||
|         }]); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										152
									
								
								apps/client/src/widgets/view_widgets/table_view/col_editing.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,152 @@ | ||||
| import { Tabulator } from "tabulator-tables"; | ||||
| import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; | ||||
| import { Attribute } from "../../../services/attribute_parser"; | ||||
| import Component from "../../../components/component"; | ||||
| import { CommandListenerData, EventData } from "../../../components/app_context"; | ||||
| import attributes from "../../../services/attributes"; | ||||
| import FNote from "../../../entities/fnote"; | ||||
| import { deleteColumn, renameColumn } from "./bulk_actions"; | ||||
| import dialog from "../../../services/dialog"; | ||||
| import { t } from "../../../services/i18n"; | ||||
|  | ||||
| export default class TableColumnEditing extends Component { | ||||
|  | ||||
|     private attributeDetailWidget: AttributeDetailWidget; | ||||
|     private api: Tabulator; | ||||
|     private parentNote: FNote; | ||||
|  | ||||
|     private newAttribute?: Attribute; | ||||
|     private newAttributePosition?: number; | ||||
|     private existingAttributeToEdit?: Attribute; | ||||
|  | ||||
|     constructor($parent: JQuery<HTMLElement>, parentNote: FNote, api: Tabulator) { | ||||
|         super(); | ||||
|         const parentComponent = glob.getComponentByEl($parent[0]); | ||||
|         this.attributeDetailWidget = new AttributeDetailWidget() | ||||
|                 .contentSized() | ||||
|                 .setParent(parentComponent); | ||||
|         $parent.append(this.attributeDetailWidget.render()); | ||||
|         this.api = api; | ||||
|         this.parentNote = parentNote; | ||||
|     } | ||||
|  | ||||
|     addNewTableColumnCommand({ referenceColumn, columnToEdit, direction, type }: EventData<"addNewTableColumn">) { | ||||
|         let attr: Attribute | undefined; | ||||
|  | ||||
|         this.existingAttributeToEdit = undefined; | ||||
|         if (columnToEdit) { | ||||
|             attr = this.getAttributeFromField(columnToEdit.getField()); | ||||
|             if (attr) { | ||||
|                 this.existingAttributeToEdit = { ...attr }; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (!attr) { | ||||
|             attr = { | ||||
|                 type: "label", | ||||
|                 name: `${type ?? "label"}:myLabel`, | ||||
|                 value: "promoted,single,text", | ||||
|                 isInheritable: true | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         if (referenceColumn && this.api) { | ||||
|             this.newAttributePosition = this.api.getColumns().indexOf(referenceColumn); | ||||
|  | ||||
|             if (direction === "after") { | ||||
|                 this.newAttributePosition++; | ||||
|             } | ||||
|         } else { | ||||
|             this.newAttributePosition = undefined; | ||||
|         } | ||||
|  | ||||
|         this.attributeDetailWidget!.showAttributeDetail({ | ||||
|             attribute: attr, | ||||
|             allAttributes: [ attr ], | ||||
|             isOwned: true, | ||||
|             x: 0, | ||||
|             y: 150, | ||||
|             focus: "name", | ||||
|             hideMultiplicity: true | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) { | ||||
|         this.newAttribute = attributes[0]; | ||||
|     } | ||||
|  | ||||
|     async saveAttributesCommand() { | ||||
|         if (!this.newAttribute) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const { name, value, isInheritable } = this.newAttribute; | ||||
|  | ||||
|         this.api.blockRedraw(); | ||||
|         const isRename = (this.existingAttributeToEdit && this.existingAttributeToEdit.name !== name); | ||||
|         try { | ||||
|             if (isRename) { | ||||
|                 const oldName = this.existingAttributeToEdit!.name.split(":")[1]; | ||||
|                 const [ type, newName ] = name.split(":"); | ||||
|                 await renameColumn(this.parentNote.noteId, type as "label" | "relation", oldName, newName); | ||||
|             } | ||||
|  | ||||
|             if (this.existingAttributeToEdit && (isRename || this.existingAttributeToEdit.isInheritable !== isInheritable)) { | ||||
|                 attributes.removeOwnedLabelByName(this.parentNote, this.existingAttributeToEdit.name); | ||||
|             } | ||||
|             attributes.setLabel(this.parentNote.noteId, name, value, isInheritable); | ||||
|         } finally { | ||||
|             this.api.restoreRedraw(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async deleteTableColumnCommand({ columnToDelete }: CommandListenerData<"deleteTableColumn">) { | ||||
|         if (!columnToDelete || !await dialog.confirm(t("table_view.delete_column_confirmation"))) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let [ type, name ] = columnToDelete.getField()?.split(".", 2); | ||||
|         if (!type || !name) { | ||||
|             return; | ||||
|         } | ||||
|         type = type.replace("s", ""); | ||||
|  | ||||
|         this.api.blockRedraw(); | ||||
|         try { | ||||
|             await deleteColumn(this.parentNote.noteId, type as "label" | "relation", name); | ||||
|             attributes.removeOwnedLabelByName(this.parentNote, `${type}:${name}`); | ||||
|         } finally { | ||||
|             this.api.restoreRedraw(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     getNewAttributePosition() { | ||||
|         return this.newAttributePosition; | ||||
|     } | ||||
|  | ||||
|     resetNewAttributePosition() { | ||||
|         this.newAttribute = undefined; | ||||
|         this.newAttributePosition = undefined; | ||||
|         this.existingAttributeToEdit = undefined; | ||||
|     } | ||||
|  | ||||
|     getFAttributeFromField(field: string) { | ||||
|         const [ type, name ] = field.split(".", 2); | ||||
|         const attrName = `${type.replace("s", "")}:${name}`; | ||||
|         return this.parentNote.getLabel(attrName); | ||||
|     } | ||||
|  | ||||
|     getAttributeFromField(field: string): Attribute | undefined { | ||||
|         const fAttribute = this.getFAttributeFromField(field); | ||||
|         if (fAttribute) { | ||||
|             return { | ||||
|                 name: fAttribute.name, | ||||
|                 value: fAttribute.value, | ||||
|                 type: fAttribute.type, | ||||
|                 isInheritable: fAttribute.isInheritable | ||||
|             }; | ||||
|         } | ||||
|         return undefined; | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										133
									
								
								apps/client/src/widgets/view_widgets/table_view/columns.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,133 @@ | ||||
| import { describe, expect, it } from "vitest"; | ||||
| import { restoreExistingData } from "./columns"; | ||||
| import type { ColumnDefinition } from "tabulator-tables"; | ||||
|  | ||||
| describe("restoreExistingData", () => { | ||||
|     it("maintains important columns properties", () => { | ||||
|         const newDefs: ColumnDefinition[] = [ | ||||
|             { field: "title", title: "Title", editor: "input" }, | ||||
|             { field: "noteId", title: "Note ID", formatter: "color", visible: false } | ||||
|         ]; | ||||
|         const oldDefs: ColumnDefinition[] = [ | ||||
|             { field: "title", title: "Title", width: 300, visible: true }, | ||||
|             { field: "noteId", title: "Note ID", width: 200, visible: true } | ||||
|         ]; | ||||
|         const restored = restoreExistingData(newDefs, oldDefs); | ||||
|         expect(restored[0].editor).toBe("input"); | ||||
|         expect(restored[1].formatter).toBe("color"); | ||||
|     }); | ||||
|  | ||||
|     it("should restore existing column data", () => { | ||||
|         const newDefs: ColumnDefinition[] = [ | ||||
|             { field: "title", title: "Title", editor: "input" }, | ||||
|             { field: "noteId", title: "Note ID", visible: false } | ||||
|         ]; | ||||
|         const oldDefs: ColumnDefinition[] = [ | ||||
|             { field: "title", title: "Title", width: 300, visible: true }, | ||||
|             { field: "noteId", title: "Note ID", width: 200, visible: true } | ||||
|         ]; | ||||
|         const restored = restoreExistingData(newDefs, oldDefs); | ||||
|         expect(restored[0].width).toBe(300); | ||||
|         expect(restored[1].width).toBe(200); | ||||
|     }); | ||||
|  | ||||
|     it("restores order of columns", () => { | ||||
|         const newDefs: ColumnDefinition[] = [ | ||||
|             { field: "title", title: "Title", editor: "input" }, | ||||
|             { field: "noteId", title: "Note ID", visible: false } | ||||
|         ]; | ||||
|         const oldDefs: ColumnDefinition[] = [ | ||||
|             { field: "noteId", title: "Note ID", width: 200, visible: true }, | ||||
|             { field: "title", title: "Title", width: 300, visible: true } | ||||
|         ]; | ||||
|         const restored = restoreExistingData(newDefs, oldDefs); | ||||
|         expect(restored[0].field).toBe("noteId"); | ||||
|         expect(restored[1].field).toBe("title"); | ||||
|     }); | ||||
|  | ||||
|     it("inserts new columns at given position", () => { | ||||
|         const newDefs: ColumnDefinition[] = [ | ||||
|             { field: "title", title: "Title", editor: "input" }, | ||||
|             { field: "noteId", title: "Note ID", visible: false }, | ||||
|             { field: "newColumn", title: "New Column", editor: "input" } | ||||
|         ]; | ||||
|         const oldDefs: ColumnDefinition[] = [ | ||||
|             { field: "title", title: "Title", width: 300, visible: true }, | ||||
|             { field: "noteId", title: "Note ID", width: 200, visible: true } | ||||
|         ]; | ||||
|         const restored = restoreExistingData(newDefs, oldDefs, 0); | ||||
|         expect(restored.length).toBe(3); | ||||
|         expect(restored[0].field).toBe("newColumn"); | ||||
|         expect(restored[1].field).toBe("title"); | ||||
|         expect(restored[2].field).toBe("noteId"); | ||||
|     }); | ||||
|  | ||||
|     it("inserts new columns at the end if no position is specified", () => { | ||||
|         const newDefs: ColumnDefinition[] = [ | ||||
|             { field: "title", title: "Title", editor: "input" }, | ||||
|             { field: "noteId", title: "Note ID", visible: false }, | ||||
|             { field: "newColumn", title: "New Column", editor: "input" } | ||||
|         ]; | ||||
|         const oldDefs: ColumnDefinition[] = [ | ||||
|             { field: "title", title: "Title", width: 300, visible: true }, | ||||
|             { field: "noteId", title: "Note ID", width: 200, visible: true } | ||||
|         ]; | ||||
|         const restored = restoreExistingData(newDefs, oldDefs); | ||||
|         expect(restored.length).toBe(3); | ||||
|         expect(restored[0].field).toBe("title"); | ||||
|         expect(restored[1].field).toBe("noteId"); | ||||
|         expect(restored[2].field).toBe("newColumn"); | ||||
|     }); | ||||
|  | ||||
|     it("supports a rename", () => { | ||||
|         const newDefs: ColumnDefinition[] = [ | ||||
|             { field: "title", title: "Title", editor: "input" }, | ||||
|             { field: "noteId", title: "Note ID", visible: false }, | ||||
|             { field: "newColumn", title: "New Column", editor: "input" } | ||||
|         ]; | ||||
|         const oldDefs: ColumnDefinition[] = [ | ||||
|             { field: "title", title: "Title", width: 300, visible: true }, | ||||
|             { field: "noteId", title: "Note ID", width: 200, visible: true }, | ||||
|             { field: "oldColumn", title: "New Column", editor: "input" } | ||||
|         ]; | ||||
|         const restored = restoreExistingData(newDefs, oldDefs); | ||||
|         expect(restored.length).toBe(3); | ||||
|     }); | ||||
|  | ||||
|     it("doesn't alter the existing order", () => { | ||||
|         const newDefs: ColumnDefinition[] = [ | ||||
|             { title: "#", headerSort: false, hozAlign: "center", resizable: false, frozen: true, rowHandle: false }, | ||||
|             { field: "noteId", title: "Note ID", visible: false }, | ||||
|             { field: "title", title: "Title", editor: "input", width: 400 } | ||||
|         ] | ||||
|         const oldDefs: ColumnDefinition[] = [ | ||||
|             { title: "#", headerSort: false, hozAlign: "center", resizable: false, rowHandle: false }, | ||||
|             { field: "noteId", title: "Note ID", visible: false }, | ||||
|             { field: "title", title: "Title", editor: "input", width: 400 } | ||||
|         ]; | ||||
|         const restored = restoreExistingData(newDefs, oldDefs); | ||||
|         expect(restored).toStrictEqual(newDefs); | ||||
|     }); | ||||
|  | ||||
|     it("allows hiding the row number column", () => { | ||||
|         const newDefs: ColumnDefinition[] = [ | ||||
|             { title: "#", headerSort: false, hozAlign: "center", resizable: false, frozen: true, rowHandle: false }, | ||||
|         ] | ||||
|         const oldDefs: ColumnDefinition[] = [ | ||||
|             { title: "#", headerSort: false, hozAlign: "center", resizable: false, rowHandle: false, visible: false }, | ||||
|         ]; | ||||
|         const restored = restoreExistingData(newDefs, oldDefs); | ||||
|         expect(restored[0].visible).toStrictEqual(false); | ||||
|     }); | ||||
|  | ||||
|     it("enforces size for non-resizable columns", () => { | ||||
|         const newDefs: ColumnDefinition[] = [ | ||||
|             { title: "#", resizable: false, width: "100px" }, | ||||
|         ] | ||||
|         const oldDefs: ColumnDefinition[] = [ | ||||
|             { title: "#", resizable: false, width: "120px" }, | ||||
|         ]; | ||||
|         const restored = restoreExistingData(newDefs, oldDefs); | ||||
|         expect(restored[0].width).toStrictEqual("100px"); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,12 +1,11 @@ | ||||
| import { RelationEditor } from "./relation_editor.js"; | ||||
| import { NoteFormatter, NoteTitleFormatter } from "./formatters.js"; | ||||
| import { applyHeaderMenu } from "./header-menu.js"; | ||||
| import { MonospaceFormatter, NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js"; | ||||
| import type { ColumnDefinition } from "tabulator-tables"; | ||||
| import { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; | ||||
|  | ||||
| type ColumnType = LabelType | "relation"; | ||||
|  | ||||
| export interface PromotedAttributeInformation { | ||||
| export interface AttributeDefinitionInformation { | ||||
|     name: string; | ||||
|     title?: string; | ||||
|     type?: ColumnType; | ||||
| @@ -42,19 +41,30 @@ const labelTypeMappings: Record<ColumnType, Partial<ColumnDefinition>> = { | ||||
|     } | ||||
| }; | ||||
|  | ||||
| export function buildColumnDefinitions(info: PromotedAttributeInformation[], existingColumnData?: ColumnDefinition[]) { | ||||
|     const columnDefs: ColumnDefinition[] = [ | ||||
| interface BuildColumnArgs { | ||||
|     info: AttributeDefinitionInformation[]; | ||||
|     movableRows: boolean; | ||||
|     existingColumnData: ColumnDefinition[] | undefined; | ||||
|     rowNumberHint: number; | ||||
|     position?: number; | ||||
| } | ||||
|  | ||||
| export function buildColumnDefinitions({ info, movableRows, existingColumnData, rowNumberHint, position }: BuildColumnArgs) { | ||||
|     let columnDefs: ColumnDefinition[] = [ | ||||
|         { | ||||
|             title: "#", | ||||
|             formatter: "rownum", | ||||
|             headerSort: false, | ||||
|             hozAlign: "center", | ||||
|             resizable: false, | ||||
|             frozen: true | ||||
|             frozen: true, | ||||
|             rowHandle: movableRows, | ||||
|             width: calculateIndexColumnWidth(rowNumberHint, movableRows), | ||||
|             formatter: RowNumberFormatter(movableRows) | ||||
|         }, | ||||
|         { | ||||
|             field: "noteId", | ||||
|             title: "Note ID", | ||||
|             formatter: MonospaceFormatter, | ||||
|             visible: false | ||||
|         }, | ||||
|         { | ||||
| @@ -79,32 +89,59 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[], exi | ||||
|             field, | ||||
|             title: title ?? name, | ||||
|             editor: "input", | ||||
|             rowHandle: false, | ||||
|             ...labelTypeMappings[type ?? "text"], | ||||
|         }); | ||||
|         seenFields.add(field); | ||||
|     } | ||||
|  | ||||
|     applyHeaderMenu(columnDefs); | ||||
|     if (existingColumnData) { | ||||
|         restoreExistingData(columnDefs, existingColumnData); | ||||
|         columnDefs = restoreExistingData(columnDefs, existingColumnData, position); | ||||
|     } | ||||
|  | ||||
|     return columnDefs; | ||||
| } | ||||
|  | ||||
| function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[]) { | ||||
|     const byField = new Map<string, ColumnDefinition>; | ||||
|     for (const def of oldDefs) { | ||||
|         byField.set(def.field ?? "", def); | ||||
|     } | ||||
| export function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[], position?: number) { | ||||
|     // 1. Keep existing columns, but restore their properties like width, visibility and order. | ||||
|     const newItemsByField = new Map<string, ColumnDefinition>( | ||||
|         newDefs.map(def => [def.field!, def]) | ||||
|     ); | ||||
|     const existingColumns = oldDefs | ||||
|         .filter(item => (item.field && newItemsByField.has(item.field!)) || item.title === "#") | ||||
|         .map(oldItem => { | ||||
|             const data = newItemsByField.get(oldItem.field!)!; | ||||
|             if (oldItem.resizable !== false && oldItem.width !== undefined) { | ||||
|                 data.width = oldItem.width; | ||||
|             } | ||||
|             if (oldItem.visible !== undefined) { | ||||
|                 data.visible = oldItem.visible; | ||||
|             } | ||||
|             return data; | ||||
|         }) as ColumnDefinition[]; | ||||
|  | ||||
|     for (const newDef of newDefs) { | ||||
|         const oldDef = byField.get(newDef.field ?? ""); | ||||
|         if (!oldDef) { | ||||
|             continue; | ||||
|         } | ||||
|     // 2. Determine new columns. | ||||
|     const existingFields = new Set(existingColumns.map(item => item.field)); | ||||
|     const newColumns = newDefs | ||||
|         .filter(item => !existingFields.has(item.field!)); | ||||
|  | ||||
|         newDef.width = oldDef.width; | ||||
|         newDef.visible = oldDef.visible; | ||||
|     } | ||||
|     // Clamp position to a valid range | ||||
|     const insertPos = position !== undefined | ||||
|         ? Math.min(Math.max(position, 0), existingColumns.length) | ||||
|         : existingColumns.length; | ||||
|  | ||||
|     // 3. Insert new columns at the specified position | ||||
|     return [ | ||||
|         ...existingColumns.slice(0, insertPos), | ||||
|         ...newColumns, | ||||
|         ...existingColumns.slice(insertPos) | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| function calculateIndexColumnWidth(rowNumberHint: number, movableRows: boolean): number { | ||||
|     let columnWidth = 16 * (rowNumberHint.toString().length || 1); | ||||
|     if (movableRows) { | ||||
|         columnWidth += 32; | ||||
|     } | ||||
|     return columnWidth; | ||||
| } | ||||
|   | ||||
							
								
								
									
										277
									
								
								apps/client/src/widgets/view_widgets/table_view/context_menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,277 @@ | ||||
| import { ColumnComponent, RowComponent, Tabulator } from "tabulator-tables"; | ||||
| import contextMenu, { MenuItem } from "../../../menus/context_menu.js"; | ||||
| import { TableData } from "./rows.js"; | ||||
| import branches from "../../../services/branches.js"; | ||||
| import { t } from "../../../services/i18n.js"; | ||||
| import link_context_menu from "../../../menus/link_context_menu.js"; | ||||
| import type FNote from "../../../entities/fnote.js"; | ||||
| import froca from "../../../services/froca.js"; | ||||
| import type Component from "../../../components/component.js"; | ||||
|  | ||||
| export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) { | ||||
|     tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote, tabulator)); | ||||
|     tabulator.on("headerContext", (e, col) => showColumnContextMenu(e, col, parentNote, tabulator)); | ||||
|     tabulator.on("renderComplete", () => { | ||||
|         const headerRow = tabulator.element.querySelector(".tabulator-header-contents"); | ||||
|         headerRow?.addEventListener("contextmenu", (e) => showHeaderContextMenu(e, tabulator)); | ||||
|     }); | ||||
|  | ||||
|     // Pressing the expand button prevents bubbling and the context menu remains menu when it shouldn't. | ||||
|     if (tabulator.options.dataTree) { | ||||
|         const dismissContextMenu = () => contextMenu.hide(); | ||||
|         tabulator.on("dataTreeRowExpanded", dismissContextMenu); | ||||
|         tabulator.on("dataTreeRowCollapsed", dismissContextMenu); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: FNote, tabulator: Tabulator) { | ||||
|     const e = _e as MouseEvent; | ||||
|     const { title, field } = column.getDefinition(); | ||||
|  | ||||
|     const sorters = tabulator.getSorters(); | ||||
|     const sorter = sorters.find(sorter => sorter.field === field); | ||||
|     const isUserDefinedColumn = (!!field && (field?.startsWith("labels.") || field?.startsWith("relations."))); | ||||
|  | ||||
|     contextMenu.show({ | ||||
|         items: [ | ||||
|             { | ||||
|                 title: t("table_view.sort-column-by", { title }), | ||||
|                 enabled: !!field, | ||||
|                 uiIcon: "bx bx-sort-alt-2", | ||||
|                 items: [ | ||||
|                     { | ||||
|                         title: t("table_view.sort-column-ascending"), | ||||
|                         checked: (sorter?.dir === "asc"), | ||||
|                         uiIcon: "bx bx-empty", | ||||
|                         handler: () => tabulator.setSort([ | ||||
|                             { | ||||
|                                 column: field!, | ||||
|                                 dir: "asc", | ||||
|                             } | ||||
|                         ]) | ||||
|                     }, | ||||
|                     { | ||||
|                         title: t("table_view.sort-column-descending"), | ||||
|                         checked: (sorter?.dir === "desc"), | ||||
|                         uiIcon: "bx bx-empty", | ||||
|                         handler: () => tabulator.setSort([ | ||||
|                             { | ||||
|                                 column: field!, | ||||
|                                 dir: "desc" | ||||
|                             } | ||||
|                         ]) | ||||
|                     } | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 title: t("table_view.sort-column-clear"), | ||||
|                 enabled: sorters.length > 0, | ||||
|                 uiIcon: "bx bx-x-circle", | ||||
|                 handler: () => tabulator.clearSort() | ||||
|             }, | ||||
|             { | ||||
|                 title: "----" | ||||
|             }, | ||||
|             { | ||||
|                 title: t("table_view.hide-column", { title }), | ||||
|                 uiIcon: "bx bx-hide", | ||||
|                 handler: () => column.hide() | ||||
|             }, | ||||
|             { | ||||
|                 title: t("table_view.show-hide-columns"), | ||||
|                 uiIcon: "bx bx-columns", | ||||
|                 items: buildColumnItems(tabulator) | ||||
|             }, | ||||
|             { title: "----" }, | ||||
|             { | ||||
|                 title: t("table_view.add-column-to-the-left"), | ||||
|                 uiIcon: "bx bx-horizontal-left", | ||||
|                 enabled: !column.getDefinition().frozen, | ||||
|                 items: buildInsertSubmenu(e, column, "before"), | ||||
|                 handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { | ||||
|                     referenceColumn: column | ||||
|                 }) | ||||
|             }, | ||||
|             { | ||||
|                 title: t("table_view.add-column-to-the-right"), | ||||
|                 uiIcon: "bx bx-horizontal-right", | ||||
|                 items: buildInsertSubmenu(e, column, "after"), | ||||
|                 handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { | ||||
|                     referenceColumn: column, | ||||
|                     direction: "after" | ||||
|                 }) | ||||
|             }, | ||||
|             { title: "----" }, | ||||
|             { | ||||
|                 title: t("table_view.edit-column"), | ||||
|                 uiIcon: "bx bxs-edit-alt", | ||||
|                 enabled: isUserDefinedColumn, | ||||
|                 handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { | ||||
|                     referenceColumn: column, | ||||
|                     columnToEdit: column | ||||
|                 }) | ||||
|             }, | ||||
|             { | ||||
|                 title: t("table_view.delete-column"), | ||||
|                 uiIcon: "bx bx-trash", | ||||
|                 enabled: isUserDefinedColumn, | ||||
|                 handler: () => getParentComponent(e)?.triggerCommand("deleteTableColumn", { | ||||
|                     columnToDelete: column | ||||
|                 }) | ||||
|             } | ||||
|         ], | ||||
|         selectMenuItemHandler() {}, | ||||
|         x: e.pageX, | ||||
|         y: e.pageY | ||||
|     }); | ||||
|     e.preventDefault(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Shows a context menu which has options dedicated to the header area (the part where the columns are, but in the empty space). | ||||
|  * Provides generic options such as toggling columns. | ||||
|  */ | ||||
| function showHeaderContextMenu(_e: Event, tabulator: Tabulator) { | ||||
|     const e = _e as MouseEvent; | ||||
|     contextMenu.show({ | ||||
|         items: [ | ||||
|             { | ||||
|                 title: t("table_view.show-hide-columns"), | ||||
|                 uiIcon: "bx bx-columns", | ||||
|                 items: buildColumnItems(tabulator) | ||||
|             }, | ||||
|             { title: "----" }, | ||||
|             { | ||||
|                 title: t("table_view.new-column"), | ||||
|                 uiIcon: "bx bx-empty", | ||||
|                 enabled: false | ||||
|             }, | ||||
|             ...buildInsertSubmenu(e) | ||||
|         ], | ||||
|         selectMenuItemHandler() {}, | ||||
|         x: e.pageX, | ||||
|         y: e.pageY | ||||
|     }); | ||||
|     e.preventDefault(); | ||||
| } | ||||
|  | ||||
| export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: FNote, tabulator: Tabulator) { | ||||
|     const e = _e as MouseEvent; | ||||
|     const rowData = row.getData() as TableData; | ||||
|  | ||||
|     let parentNoteId: string = parentNote.noteId; | ||||
|  | ||||
|     if (tabulator.options.dataTree) { | ||||
|         const parentRow = row.getTreeParent(); | ||||
|         if (parentRow) { | ||||
|             parentNoteId = parentRow.getData().noteId as string; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     contextMenu.show({ | ||||
|         items: [ | ||||
|             ...link_context_menu.getItems(), | ||||
|             { title: "----" }, | ||||
|             { | ||||
|                 title: t("table_view.row-insert-above"), | ||||
|                 uiIcon: "bx bx-horizontal-left bx-rotate-90", | ||||
|                 handler: () => getParentComponent(e)?.triggerCommand("addNewRow", { | ||||
|                     parentNotePath: parentNoteId, | ||||
|                     customOpts: { | ||||
|                         target: "before", | ||||
|                         targetBranchId: rowData.branchId, | ||||
|                     } | ||||
|                 }) | ||||
|             }, | ||||
|             { | ||||
|                 title: t("table_view.row-insert-child"), | ||||
|                 uiIcon: "bx bx-subdirectory-right", | ||||
|                 handler: async () => { | ||||
|                     const branchId = row.getData().branchId; | ||||
|                     const note = await froca.getBranch(branchId)?.getNote(); | ||||
|                     getParentComponent(e)?.triggerCommand("addNewRow", { | ||||
|                         parentNotePath: note?.noteId, | ||||
|                         customOpts: { | ||||
|                             target: "after", | ||||
|                             targetBranchId: branchId, | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 title: t("table_view.row-insert-below"), | ||||
|                 uiIcon: "bx bx-horizontal-left bx-rotate-270", | ||||
|                 handler: () => getParentComponent(e)?.triggerCommand("addNewRow", { | ||||
|                     parentNotePath: parentNoteId, | ||||
|                     customOpts: { | ||||
|                         target: "after", | ||||
|                         targetBranchId: rowData.branchId, | ||||
|                     } | ||||
|                 }) | ||||
|             }, | ||||
|             { title: "----" }, | ||||
|             { | ||||
|                 title: t("table_context_menu.delete_row"), | ||||
|                 uiIcon: "bx bx-trash", | ||||
|                 handler: () => branches.deleteNotes([ rowData.branchId ], false, false) | ||||
|             } | ||||
|         ], | ||||
|         selectMenuItemHandler: ({ command }) =>  link_context_menu.handleLinkContextMenuItem(command, rowData.noteId), | ||||
|         x: e.pageX, | ||||
|         y: e.pageY | ||||
|     }); | ||||
|     e.preventDefault(); | ||||
| } | ||||
|  | ||||
| function getParentComponent(e: MouseEvent) { | ||||
|     if (!e.target) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     return $(e.target) | ||||
|         .closest(".component") | ||||
|         .prop("component") as Component; | ||||
| } | ||||
|  | ||||
| function buildColumnItems(tabulator: Tabulator) { | ||||
|     const items: MenuItem<unknown>[] = []; | ||||
|     for (const column of tabulator.getColumns()) { | ||||
|         const { title } = column.getDefinition(); | ||||
|  | ||||
|         items.push({ | ||||
|             title, | ||||
|             checked: column.isVisible(), | ||||
|             uiIcon: "bx bx-empty", | ||||
|             handler: () => column.toggle() | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     return items; | ||||
| } | ||||
|  | ||||
| function buildInsertSubmenu(e: MouseEvent, referenceColumn?: ColumnComponent, direction?: "before" | "after"): MenuItem<unknown>[] { | ||||
|     return [ | ||||
|         { | ||||
|             title: t("table_view.new-column-label"), | ||||
|             uiIcon: "bx bx-hash", | ||||
|             handler: () => { | ||||
|                 getParentComponent(e)?.triggerCommand("addNewTableColumn", { | ||||
|                     referenceColumn, | ||||
|                     type: "label", | ||||
|                     direction | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: t("table_view.new-column-relation"), | ||||
|             uiIcon: "bx bx-transfer", | ||||
|             handler: () => { | ||||
|                 getParentComponent(e)?.triggerCommand("addNewTableColumn", { | ||||
|                     referenceColumn, | ||||
|                     type: "relation", | ||||
|                     direction | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| @@ -11,12 +11,12 @@ export default function buildFooter(parentNote: FNote) { | ||||
|     } | ||||
|  | ||||
|     return /*html*/`\ | ||||
|         <button class="btn btn-sm" style="padding: 0px 10px 0px 10px;" data-trigger-command="addNewRow"> | ||||
|         <button class="btn btn-sm" data-trigger-command="addNewRow"> | ||||
|             <span class="bx bx-plus"></span> ${t("table_view.new-row")} | ||||
|         </button> | ||||
|  | ||||
|         <button class="btn btn-sm" style="padding: 0px 10px 0px 10px;" data-trigger-command="addNoteListItem"> | ||||
|             <span class="bx bx-columns"></span> ${t("table_view.new-column")} | ||||
|         <button class="btn btn-sm" data-trigger-command="addNewTableColumn"> | ||||
|             <span class="bx bx-carousel"></span> ${t("table_view.new-column")} | ||||
|         </button> | ||||
|     `.trimStart(); | ||||
| } | ||||
|   | ||||
| @@ -1,45 +1,89 @@ | ||||
| import { CellComponent } from "tabulator-tables"; | ||||
| import { loadReferenceLinkTitle } from "../../../services/link.js"; | ||||
| import froca from "../../../services/froca.js"; | ||||
| import FNote from "../../../entities/fnote.js"; | ||||
|  | ||||
| /** | ||||
|  * Custom formatter to represent a note, with the icon and note title being rendered. | ||||
|  * | ||||
|  * The value of the cell must be the note ID. | ||||
|  */ | ||||
| export function NoteFormatter(cell: CellComponent, _formatterParams, onRendered) { | ||||
| export function NoteFormatter(cell: CellComponent, _formatterParams, onRendered): string { | ||||
|     let noteId = cell.getValue(); | ||||
|     if (!noteId) { | ||||
|         return ""; | ||||
|     } | ||||
|  | ||||
|     onRendered(async () => { | ||||
|         const { $noteRef, href } = buildNoteLink(noteId); | ||||
|         await loadReferenceLinkTitle($noteRef, href); | ||||
|         cell.getElement().appendChild($noteRef[0]); | ||||
|     }); | ||||
|     return ""; | ||||
|     function buildLink(note: FNote | undefined) { | ||||
|         if (!note) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const iconClass = note.getIcon(); | ||||
|         const title = note.title; | ||||
|         const { $noteRef } = buildNoteLink(noteId, title, iconClass, note.getColorClass()); | ||||
|         return $noteRef[0]; | ||||
|     } | ||||
|  | ||||
|     const cachedNote = froca.getNoteFromCache(noteId); | ||||
|     if (cachedNote) { | ||||
|         // Cache hit, build the link immediately | ||||
|         const el = buildLink(cachedNote); | ||||
|         return el?.outerHTML ?? ""; | ||||
|     } else { | ||||
|         // Cache miss, load the note asynchronously | ||||
|         onRendered(async () => { | ||||
|             const note = await froca.getNote(noteId); | ||||
|             if (!note) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const el = buildLink(note); | ||||
|             if (el) { | ||||
|                 cell.getElement().appendChild(el); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return ""; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Custom formatter for the note title that is quite similar to {@link NoteFormatter}, but where the title and icons are read from separate fields. | ||||
|  */ | ||||
| export function NoteTitleFormatter(cell: CellComponent) { | ||||
|     const { noteId, iconClass } = cell.getRow().getData(); | ||||
|     const { noteId, iconClass, colorClass } = cell.getRow().getData(); | ||||
|     if (!noteId) { | ||||
|         return ""; | ||||
|     } | ||||
|  | ||||
|     const { $noteRef } = buildNoteLink(noteId); | ||||
|     $noteRef.text(cell.getValue()); | ||||
|     $noteRef.prepend($("<span>").addClass(iconClass)); | ||||
|  | ||||
|     const { $noteRef } = buildNoteLink(noteId, cell.getValue(), iconClass, colorClass); | ||||
|     return $noteRef[0].outerHTML; | ||||
| } | ||||
|  | ||||
| function buildNoteLink(noteId: string) { | ||||
| export function RowNumberFormatter(draggableRows: boolean) { | ||||
|     return (cell: CellComponent) => { | ||||
|         let html = ""; | ||||
|         if (draggableRows) { | ||||
|             html += `<span class="bx bx-dots-vertical-rounded"></span> `; | ||||
|         } | ||||
|         html += cell.getRow().getPosition(true); | ||||
|         return html; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export function MonospaceFormatter(cell: CellComponent) { | ||||
|     return `<code>${cell.getValue()}</code>`; | ||||
| } | ||||
|  | ||||
| function buildNoteLink(noteId: string, title: string, iconClass: string, colorClass?: string) { | ||||
|     const $noteRef = $("<span>"); | ||||
|     const href = `#root/${noteId}`; | ||||
|     $noteRef.addClass("reference-link"); | ||||
|     $noteRef.attr("data-href", href); | ||||
|     $noteRef.text(title); | ||||
|     $noteRef.prepend($("<span>").addClass(iconClass)); | ||||
|     if (colorClass) { | ||||
|         $noteRef.addClass(colorClass); | ||||
|     } | ||||
|     return { $noteRef, href }; | ||||
| } | ||||
|   | ||||
| @@ -1,53 +0,0 @@ | ||||
| import type { ColumnComponent, ColumnDefinition, MenuObject, Tabulator } from "tabulator-tables"; | ||||
|  | ||||
| export function applyHeaderMenu(columns: ColumnDefinition[]) { | ||||
|     for (let column of columns) { | ||||
|         if (column.headerSort !== false) { | ||||
|             column.headerMenu = headerMenu; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| function headerMenu(this: Tabulator) { | ||||
|     const menu: MenuObject<ColumnComponent>[] = []; | ||||
|     const columns = this.getColumns(); | ||||
|  | ||||
|     for (let column of columns) { | ||||
|         //create checkbox element using font awesome icons | ||||
|         let icon = document.createElement("i"); | ||||
|         icon.classList.add("bx"); | ||||
|         icon.classList.add(column.isVisible() ? "bx-check" : "bx-empty"); | ||||
|  | ||||
|         //build label | ||||
|         let label = document.createElement("span"); | ||||
|         let title = document.createElement("span"); | ||||
|  | ||||
|         title.textContent = " " + column.getDefinition().title; | ||||
|  | ||||
|         label.appendChild(icon); | ||||
|         label.appendChild(title); | ||||
|  | ||||
|         //create menu item | ||||
|         menu.push({ | ||||
|             label: label, | ||||
|             action: function (e) { | ||||
|                 //prevent menu closing | ||||
|                 e.stopPropagation(); | ||||
|  | ||||
|                 //toggle current column visibility | ||||
|                 column.toggle(); | ||||
|  | ||||
|                 //change menu item icon | ||||
|                 if (column.isVisible()) { | ||||
|                     icon.classList.remove("bx-empty"); | ||||
|                     icon.classList.add("bx-check"); | ||||
|                 } else { | ||||
|                     icon.classList.remove("bx-check"); | ||||
|                     icon.classList.add("bx-empty"); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     return menu; | ||||
| }; | ||||
| @@ -1,17 +1,17 @@ | ||||
| import froca from "../../../services/froca.js"; | ||||
| import ViewMode, { type ViewModeArgs } from "../view_mode.js"; | ||||
| import attributes, { setAttribute, setLabel } from "../../../services/attributes.js"; | ||||
| import server from "../../../services/server.js"; | ||||
| import attributes from "../../../services/attributes.js"; | ||||
| import SpacedUpdate from "../../../services/spaced_update.js"; | ||||
| import type { CommandListenerData, EventData } from "../../../components/app_context.js"; | ||||
| import type { Attribute } from "../../../services/attribute_parser.js"; | ||||
| import note_create from "../../../services/note_create.js"; | ||||
| import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MenuModule, MoveRowsModule, ColumnDefinition} from 'tabulator-tables'; | ||||
| import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; | ||||
| import type { EventData } from "../../../components/app_context.js"; | ||||
| import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent, ColumnComponent} from 'tabulator-tables'; | ||||
| import "tabulator-tables/dist/css/tabulator.css"; | ||||
| import "../../../../src/stylesheets/table.css"; | ||||
| import { canReorderRows, configureReorderingRows } from "./dragging.js"; | ||||
| import buildFooter from "./footer.js"; | ||||
| import getPromotedAttributeInformation, { buildRowDefinitions } from "./rows.js"; | ||||
| import { buildColumnDefinitions } from "./columns.js"; | ||||
| import getAttributeDefinitionInformation, { buildRowDefinitions } from "./rows.js"; | ||||
| import { AttributeDefinitionInformation, buildColumnDefinitions } from "./columns.js"; | ||||
| import { setupContextMenu } from "./context_menu.js"; | ||||
| import TableColumnEditing from "./col_editing.js"; | ||||
| import TableRowEditing from "./row_editing.js"; | ||||
|  | ||||
| const TPL = /*html*/` | ||||
| <div class="table-view"> | ||||
| @@ -63,6 +63,26 @@ const TPL = /*html*/` | ||||
|         justify-content: left; | ||||
|         gap: 0.5em; | ||||
|     } | ||||
|  | ||||
|     .tabulator button.tree-expand, | ||||
|     .tabulator button.tree-collapse { | ||||
|         display: inline-block; | ||||
|         appearance: none; | ||||
|         border: 0; | ||||
|         background: transparent; | ||||
|         width: 1.5em; | ||||
|         position: relative; | ||||
|         vertical-align: middle; | ||||
|     } | ||||
|  | ||||
|     .tabulator button.tree-expand span, | ||||
|     .tabulator button.tree-collapse span { | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|         font-size: 1.5em; | ||||
|         transform: translateY(-50%); | ||||
|     } | ||||
|     </style> | ||||
|  | ||||
|     <div class="table-view-container"></div> | ||||
| @@ -79,29 +99,24 @@ export default class TableView extends ViewMode<StateInfo> { | ||||
|  | ||||
|     private $root: JQuery<HTMLElement>; | ||||
|     private $container: JQuery<HTMLElement>; | ||||
|     private args: ViewModeArgs; | ||||
|     private spacedUpdate: SpacedUpdate; | ||||
|     private api?: Tabulator; | ||||
|     private newAttribute?: Attribute; | ||||
|     private persistentData: StateInfo["tableData"]; | ||||
|     /** If set to a note ID, whenever the rows will be updated, the title of the note will be automatically focused for editing. */ | ||||
|     private noteIdToEdit?: string; | ||||
|     private colEditing?: TableColumnEditing; | ||||
|     private rowEditing?: TableRowEditing; | ||||
|     private maxDepth: number = -1; | ||||
|     private rowNumberHint: number = 1; | ||||
|  | ||||
|     constructor(args: ViewModeArgs) { | ||||
|         super(args, "table"); | ||||
|  | ||||
|         this.$root = $(TPL); | ||||
|         this.$container = this.$root.find(".table-view-container"); | ||||
|         this.args = args; | ||||
|         this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000); | ||||
|         this.persistentData = {}; | ||||
|         args.$parent.append(this.$root); | ||||
|     } | ||||
|  | ||||
|     get isFullHeight(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     async renderList() { | ||||
|         this.$container.empty(); | ||||
|         this.renderTable(this.$container[0]); | ||||
| @@ -109,29 +124,34 @@ export default class TableView extends ViewMode<StateInfo> { | ||||
|     } | ||||
|  | ||||
|     private async renderTable(el: HTMLElement) { | ||||
|         const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, MenuModule]; | ||||
|         const info = getAttributeDefinitionInformation(this.parentNote); | ||||
|         const modules = [ SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule ]; | ||||
|         for (const module of modules) { | ||||
|             Tabulator.registerModule(module); | ||||
|         } | ||||
|  | ||||
|         this.initialize(el); | ||||
|         this.initialize(el, info); | ||||
|     } | ||||
|  | ||||
|     private async initialize(el: HTMLElement) { | ||||
|         const notes = await froca.getNotes(this.args.noteIds); | ||||
|         const info = getPromotedAttributeInformation(this.parentNote); | ||||
|  | ||||
|     private async initialize(el: HTMLElement, info: AttributeDefinitionInformation[]) { | ||||
|         const viewStorage = await this.viewStorage.restore(); | ||||
|         this.persistentData = viewStorage?.tableData || {}; | ||||
|  | ||||
|         const columnDefs = buildColumnDefinitions(info); | ||||
|         const movableRows = canReorderRows(this.parentNote); | ||||
|  | ||||
|         this.api = new Tabulator(el, { | ||||
|         this.maxDepth = parseInt(this.parentNote.getLabelValue("maxNestingDepth") ?? "-1", 10); | ||||
|         const { definitions: rowData, hasSubtree: hasChildren, rowNumber } = await buildRowDefinitions(this.parentNote, info, this.maxDepth); | ||||
|         this.rowNumberHint = rowNumber; | ||||
|         const movableRows = canReorderRows(this.parentNote) && !hasChildren; | ||||
|         const columnDefs = buildColumnDefinitions({ | ||||
|             info, | ||||
|             movableRows, | ||||
|             existingColumnData: this.persistentData.columns, | ||||
|             rowNumberHint: this.rowNumberHint | ||||
|         }); | ||||
|         let opts: Options = { | ||||
|             layout: "fitDataFill", | ||||
|             index: "noteId", | ||||
|             index: "branchId", | ||||
|             columns: columnDefs, | ||||
|             data: await buildRowDefinitions(this.parentNote, notes, info), | ||||
|             data: rowData, | ||||
|             persistence: true, | ||||
|             movableColumns: true, | ||||
|             movableRows, | ||||
| @@ -141,9 +161,30 @@ export default class TableView extends ViewMode<StateInfo> { | ||||
|                 this.spacedUpdate.scheduleUpdate(); | ||||
|             }, | ||||
|             persistenceReaderFunc: (_id, type: string) => this.persistentData?.[type], | ||||
|         }); | ||||
|         configureReorderingRows(this.api); | ||||
|         this.setupEditing(); | ||||
|         }; | ||||
|  | ||||
|         if (hasChildren) { | ||||
|             opts = { | ||||
|                 ...opts, | ||||
|                 dataTree: hasChildren, | ||||
|                 dataTreeStartExpanded: true, | ||||
|                 dataTreeBranchElement: false, | ||||
|                 dataTreeElementColumn: "title", | ||||
|                 dataTreeChildIndent: 20, | ||||
|                 dataTreeExpandElement: `<button class="tree-expand"><span class="bx bx-chevron-right"></span></button>`, | ||||
|                 dataTreeCollapseElement: `<button class="tree-collapse"><span class="bx bx-chevron-down"></span></button>` | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.api = new Tabulator(el, opts); | ||||
|  | ||||
|         this.colEditing = new TableColumnEditing(this.args.$parent, this.args.parentNote, this.api); | ||||
|         this.rowEditing = new TableRowEditing(this.api, this.args.parentNotePath!); | ||||
|  | ||||
|         if (movableRows) { | ||||
|             configureReorderingRows(this.api); | ||||
|         } | ||||
|         setupContextMenu(this.api, this.parentNote); | ||||
|     } | ||||
|  | ||||
|     private onSave() { | ||||
| @@ -152,82 +193,35 @@ export default class TableView extends ViewMode<StateInfo> { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private setupEditing() { | ||||
|         this.api!.on("cellEdited", async (cell) => { | ||||
|             const noteId = cell.getRow().getData().noteId; | ||||
|             const field = cell.getField(); | ||||
|             const newValue = cell.getValue(); | ||||
|  | ||||
|             if (field === "title") { | ||||
|                 server.put(`notes/${noteId}/title`, { title: newValue }); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (field.includes(".")) { | ||||
|                 const [ type, name ] = field.split(".", 2); | ||||
|                 if (type === "labels") { | ||||
|                     setLabel(noteId, name, newValue); | ||||
|                 } else if (type === "relations") { | ||||
|                     const note = await froca.getNote(noteId); | ||||
|                     if (note) { | ||||
|                         setAttribute(note, "relation", name, newValue); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async reloadAttributesCommand() { | ||||
|         console.log("Reload attributes"); | ||||
|     } | ||||
|  | ||||
|     async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) { | ||||
|         this.newAttribute = attributes[0]; | ||||
|     } | ||||
|  | ||||
|     async saveAttributesCommand() { | ||||
|         if (!this.newAttribute) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const { name, value } = this.newAttribute; | ||||
|         attributes.addLabel(this.parentNote.noteId, name, value, true); | ||||
|         console.log("Save attributes", this.newAttribute); | ||||
|     } | ||||
|  | ||||
|     addNewRowCommand() { | ||||
|         const parentNotePath = this.args.parentNotePath; | ||||
|         if (parentNotePath) { | ||||
|             note_create.createNote(parentNotePath, { | ||||
|                 activate: false | ||||
|             }).then(({ note }) => { | ||||
|                 if (!note) { | ||||
|                     return; | ||||
|                 } | ||||
|                 this.noteIdToEdit = note.noteId; | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { | ||||
|     async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { | ||||
|         if (!this.api) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Force a refresh if sorted is changed since we need to disable reordering. | ||||
|         if (loadResults.getAttributeRows().find(a => a.name === "sorted" && attributes.isAffecting(a, this.parentNote))) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         // Refresh if promoted attributes get changed. | ||||
|         if (loadResults.getAttributeRows().find(attr => | ||||
|             attr.type === "label" && | ||||
|             (attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) && | ||||
|             attributes.isAffecting(attr, this.parentNote))) { | ||||
|             this.#manageColumnUpdate(); | ||||
|             return await this.#manageRowsUpdate(); | ||||
|         } | ||||
|  | ||||
|         if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId)) { | ||||
|             this.#manageRowsUpdate(); | ||||
|         // Refresh max depth | ||||
|         if (loadResults.getAttributeRows().find(attr => attr.type === "label" && attr.name === "maxNestingDepth" && attributes.isAffecting(attr, this.parentNote))) { | ||||
|             this.maxDepth = parseInt(this.parentNote.getLabelValue("maxNestingDepth") ?? "-1", 10); | ||||
|             return await this.#manageRowsUpdate(); | ||||
|         } | ||||
|  | ||||
|         if (loadResults.getAttributeRows().some(attr => this.args.noteIds.includes(attr.noteId!))) { | ||||
|             this.#manageRowsUpdate(); | ||||
|         if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? "")) | ||||
|             || loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId)) | ||||
|             || loadResults.getAttributeRows().some(attr => this.noteIds.includes(attr.noteId!))) { | ||||
|             return await this.#manageRowsUpdate(); | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
| @@ -238,27 +232,40 @@ export default class TableView extends ViewMode<StateInfo> { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const info = getPromotedAttributeInformation(this.parentNote); | ||||
|         const columnDefs = buildColumnDefinitions(info, this.persistentData?.columns); | ||||
|         const info = getAttributeDefinitionInformation(this.parentNote); | ||||
|         const columnDefs = buildColumnDefinitions({ | ||||
|             info, | ||||
|             movableRows: !!this.api.options.movableRows, | ||||
|             existingColumnData: this.persistentData?.columns, | ||||
|             rowNumberHint: this.rowNumberHint, | ||||
|             position: this.colEditing?.getNewAttributePosition() | ||||
|         }); | ||||
|         this.api.setColumns(columnDefs); | ||||
|         this.colEditing?.resetNewAttributePosition(); | ||||
|     } | ||||
|  | ||||
|     addNewRowCommand(e) { this.rowEditing?.addNewRowCommand(e); } | ||||
|     addNewTableColumnCommand(e) { this.colEditing?.addNewTableColumnCommand(e); } | ||||
|     deleteTableColumnCommand(e) { this.colEditing?.deleteTableColumnCommand(e); } | ||||
|     updateAttributeListCommand(e) { this.colEditing?.updateAttributeListCommand(e); } | ||||
|     saveAttributesCommand() { this.colEditing?.saveAttributesCommand(); } | ||||
|  | ||||
|     async #manageRowsUpdate() { | ||||
|         if (!this.api) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const notes = await froca.getNotes(this.args.noteIds); | ||||
|         const info = getPromotedAttributeInformation(this.parentNote); | ||||
|         this.api.replaceData(await buildRowDefinitions(this.parentNote, notes, info)); | ||||
|         const info = getAttributeDefinitionInformation(this.parentNote); | ||||
|         const { definitions, hasSubtree, rowNumber } = await buildRowDefinitions(this.parentNote, info, this.maxDepth); | ||||
|         this.rowNumberHint = rowNumber; | ||||
|  | ||||
|         if (this.noteIdToEdit) { | ||||
|             const row = this.api?.getRows().find(r => r.getData().noteId === this.noteIdToEdit); | ||||
|             if (row) { | ||||
|                 row.getCell("title").edit(); | ||||
|             } | ||||
|             this.noteIdToEdit = undefined; | ||||
|         // Force a refresh if the data tree needs enabling/disabling. | ||||
|         if (this.api.options.dataTree !== hasSubtree) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         await this.api.replaceData(definitions); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -21,24 +21,29 @@ export function RelationEditor(cell: CellComponent, onRendered, success, cancel, | ||||
|     editor.style.boxSizing = "border-box"; | ||||
|  | ||||
|     //Set value of editor to the current value of the cell | ||||
|     const noteId = cell.getValue(); | ||||
|     if (noteId) { | ||||
|         const note = froca.getNoteFromCache(noteId); | ||||
|     const originalNoteId = cell.getValue(); | ||||
|     if (originalNoteId) { | ||||
|         const note = froca.getNoteFromCache(originalNoteId); | ||||
|         editor.value = note.title; | ||||
|     } else { | ||||
|         editor.value = ""; | ||||
|     } | ||||
|  | ||||
|     //set focus on the select box when the editor is selected | ||||
|     onRendered(function(){ | ||||
|         let newNoteId = originalNoteId; | ||||
|  | ||||
|         note_autocomplete.initNoteAutocomplete($editor, { | ||||
|             allowCreatingNotes: true | ||||
|             allowCreatingNotes: true, | ||||
|             hideAllButtons: true | ||||
|         }).on("autocomplete:noteselected", (event, suggestion, dataset) => { | ||||
|             const notePath = suggestion.notePath; | ||||
|             if (!notePath) { | ||||
|                 return; | ||||
|             newNoteId = (notePath ?? "").split("/").at(-1); | ||||
|         }).on("blur", () => { | ||||
|             if (!editor.value) { | ||||
|                 newNoteId = ""; | ||||
|             } | ||||
|  | ||||
|             const noteId = notePath.split("/").at(-1); | ||||
|             success(noteId); | ||||
|             success(newNoteId); | ||||
|         }); | ||||
|         editor.focus(); | ||||
|     }); | ||||
|   | ||||
| @@ -0,0 +1,97 @@ | ||||
| import { RowComponent, Tabulator } from "tabulator-tables"; | ||||
| import Component from "../../../components/component.js"; | ||||
| import { setAttribute, setLabel } from "../../../services/attributes.js"; | ||||
| import server from "../../../services/server.js"; | ||||
| import froca from "../../../services/froca.js"; | ||||
| import note_create, { CreateNoteOpts } from "../../../services/note_create.js"; | ||||
| import { CommandListenerData } from "../../../components/app_context.js"; | ||||
|  | ||||
| export default class TableRowEditing extends Component { | ||||
|  | ||||
|     private parentNotePath: string; | ||||
|     private api: Tabulator; | ||||
|  | ||||
|     constructor(api: Tabulator, parentNotePath: string) { | ||||
|         super(); | ||||
|         this.api = api; | ||||
|         this.parentNotePath = parentNotePath; | ||||
|         api.on("cellEdited", async (cell) => { | ||||
|             const noteId = cell.getRow().getData().noteId; | ||||
|             const field = cell.getField(); | ||||
|             let newValue = cell.getValue(); | ||||
|  | ||||
|             if (field === "title") { | ||||
|                 server.put(`notes/${noteId}/title`, { title: newValue }); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (field.includes(".")) { | ||||
|                 const [ type, name ] = field.split(".", 2); | ||||
|                 if (type === "labels") { | ||||
|                     if (typeof newValue === "boolean") { | ||||
|                         newValue = newValue ? "true" : "false"; | ||||
|                     } | ||||
|                     setLabel(noteId, name, newValue); | ||||
|                 } else if (type === "relations") { | ||||
|                     const note = await froca.getNote(noteId); | ||||
|                     if (note) { | ||||
|                         setAttribute(note, "relation", name, newValue); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) { | ||||
|         const parentNotePath = customNotePath ?? this.parentNotePath; | ||||
|         if (parentNotePath) { | ||||
|             const opts: CreateNoteOpts = { | ||||
|                 activate: false, | ||||
|                 ...customOpts | ||||
|             } | ||||
|             note_create.createNote(parentNotePath, opts).then(({ branch }) => { | ||||
|                 if (branch) { | ||||
|                     setTimeout(() => { | ||||
|                         this.focusOnBranch(branch?.branchId); | ||||
|                     }); | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     focusOnBranch(branchId: string) { | ||||
|         if (!this.api) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const row = findRowDataById(this.api.getRows(), branchId); | ||||
|         if (!row) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Expand the parent tree if any. | ||||
|         if (this.api.options.dataTree) { | ||||
|             const parent = row.getTreeParent(); | ||||
|             if (parent) { | ||||
|                 parent.treeExpand(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         row.getCell("title").edit(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| function findRowDataById(rows: RowComponent[], branchId: string): RowComponent | null { | ||||
|     for (let row of rows) { | ||||
|         const item = row.getIndex() as string; | ||||
|  | ||||
|         if (item === branchId) { | ||||
|             return row; | ||||
|         } | ||||
|  | ||||
|         let found = findRowDataById(row.getTreeChildren(), branchId); | ||||
|         if (found) return found; | ||||
|     } | ||||
|     return null; | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| import FNote from "../../../entities/fnote.js"; | ||||
| import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; | ||||
| import type { PromotedAttributeInformation } from "./columns.js"; | ||||
| import type { AttributeDefinitionInformation } from "./columns.js"; | ||||
|  | ||||
| export type TableData = { | ||||
|     iconClass: string; | ||||
| @@ -9,11 +9,17 @@ export type TableData = { | ||||
|     labels: Record<string, boolean | string | null>; | ||||
|     relations: Record<string, boolean | string | null>; | ||||
|     branchId: string; | ||||
|     colorClass: string | undefined; | ||||
|     _children?: TableData[]; | ||||
| }; | ||||
|  | ||||
| export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], infos: PromotedAttributeInformation[]) { | ||||
| export async function buildRowDefinitions(parentNote: FNote, infos: AttributeDefinitionInformation[], maxDepth = -1, currentDepth = 0) { | ||||
|     const definitions: TableData[] = []; | ||||
|     for (const branch of parentNote.getChildBranches()) { | ||||
|     const childBranches = parentNote.getChildBranches(); | ||||
|     let hasSubtree = false; | ||||
|     let rowNumber = childBranches.length; | ||||
|  | ||||
|     for (const branch of childBranches) { | ||||
|         const note = await branch.getNote(); | ||||
|         if (!note) { | ||||
|             continue; // Skip if the note is not found | ||||
| @@ -24,36 +30,51 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf | ||||
|         for (const { name, type } of infos) { | ||||
|             if (type === "relation") { | ||||
|                 relations[name] = note.getRelationValue(name); | ||||
|             } else if (type === "boolean") { | ||||
|                 labels[name] = note.hasLabel(name); | ||||
|             } else { | ||||
|                 labels[name] = note.getLabelValue(name); | ||||
|             } | ||||
|         } | ||||
|         definitions.push({ | ||||
|  | ||||
|         const def: TableData = { | ||||
|             iconClass: note.getIcon(), | ||||
|             noteId: note.noteId, | ||||
|             title: note.title, | ||||
|             labels, | ||||
|             relations, | ||||
|             branchId: branch.branchId | ||||
|         }); | ||||
|             branchId: branch.branchId, | ||||
|             colorClass: note.getColorClass() | ||||
|         } | ||||
|  | ||||
|         if (note.hasChildren() && (maxDepth < 0 || currentDepth < maxDepth)) { | ||||
|             const { definitions, rowNumber: subRowNumber } = (await buildRowDefinitions(note, infos, maxDepth, currentDepth + 1)); | ||||
|             def._children = definitions; | ||||
|             hasSubtree = true; | ||||
|             rowNumber += subRowNumber; | ||||
|         } | ||||
|  | ||||
|         definitions.push(def); | ||||
|     } | ||||
|  | ||||
|     return definitions; | ||||
|     return { | ||||
|         definitions, | ||||
|         hasSubtree, | ||||
|         rowNumber | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export default function getPromotedAttributeInformation(parentNote: FNote) { | ||||
|     const info: PromotedAttributeInformation[] = []; | ||||
|     for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) { | ||||
|         const def = promotedAttribute.getDefinition(); | ||||
| export default function getAttributeDefinitionInformation(parentNote: FNote) { | ||||
|     const info: AttributeDefinitionInformation[] = []; | ||||
|     const attrDefs = parentNote.getAttributes() | ||||
|         .filter(attr => attr.isDefinition()); | ||||
|     for (const attrDef of attrDefs) { | ||||
|         const def = attrDef.getDefinition(); | ||||
|         if (def.multiplicity !== "single") { | ||||
|             console.warn("Multiple values are not supported for now"); | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         const [ labelType, name ] = promotedAttribute.name.split(":", 2); | ||||
|         if (promotedAttribute.type !== "label") { | ||||
|         const [ labelType, name ] = attrDef.name.split(":", 2); | ||||
|         if (attrDef.type !== "label") { | ||||
|             console.warn("Relations are not supported for now"); | ||||
|             continue; | ||||
|         } | ||||
| @@ -69,6 +90,5 @@ export default function getPromotedAttributeInformation(parentNote: FNote) { | ||||
|             type | ||||
|         }); | ||||
|     } | ||||
|     console.log("Promoted attribute information", info); | ||||
|     return info; | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import type { EventData } from "../../components/app_context.js"; | ||||
| import appContext from "../../components/app_context.js"; | ||||
| import Component from "../../components/component.js"; | ||||
| import type FNote from "../../entities/fnote.js"; | ||||
| import type { ViewTypeOptions } from "../../services/note_list_renderer.js"; | ||||
| @@ -8,7 +9,6 @@ export interface ViewModeArgs { | ||||
|     $parent: JQuery<HTMLElement>; | ||||
|     parentNote: FNote; | ||||
|     parentNotePath?: string | null; | ||||
|     noteIds: string[]; | ||||
|     showNotePath?: boolean; | ||||
| } | ||||
|  | ||||
| @@ -17,6 +17,8 @@ export default abstract class ViewMode<T extends object> extends Component { | ||||
|     private _viewStorage: ViewModeStorage<T> | null; | ||||
|     protected parentNote: FNote; | ||||
|     protected viewType: ViewTypeOptions; | ||||
|     protected noteIds: string[]; | ||||
|     protected args: ViewModeArgs; | ||||
|  | ||||
|     constructor(args: ViewModeArgs, viewType: ViewTypeOptions) { | ||||
|         super(); | ||||
| @@ -25,6 +27,12 @@ export default abstract class ViewMode<T extends object> extends Component { | ||||
|         // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work | ||||
|         args.$parent.empty(); | ||||
|         this.viewType = viewType; | ||||
|         this.args = args; | ||||
|         this.noteIds = []; | ||||
|     } | ||||
|  | ||||
|     async beforeRender() { | ||||
|         await this.#refreshNoteIds(); | ||||
|     } | ||||
|  | ||||
|     abstract renderList(): Promise<JQuery<HTMLElement> | undefined>; | ||||
| @@ -35,13 +43,18 @@ export default abstract class ViewMode<T extends object> extends Component { | ||||
|      * @param e the event data. | ||||
|      * @return {@code true} if the view should be re-rendered, a falsy value otherwise. | ||||
|      */ | ||||
|     onEntitiesReloaded(e: EventData<"entitiesReloaded">): boolean | void { | ||||
|     async onEntitiesReloaded(e: EventData<"entitiesReloaded">): Promise<boolean | void> { | ||||
|         // Do nothing by default. | ||||
|     } | ||||
|  | ||||
|     get isFullHeight() { | ||||
|         // Override to change its value. | ||||
|         return false; | ||||
|     async entitiesReloadedEvent(e: EventData<"entitiesReloaded">) { | ||||
|         if (e.loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? ""))) { | ||||
|             this.#refreshNoteIds(); | ||||
|         } | ||||
|  | ||||
|         if (await this.onEntitiesReloaded(e)) { | ||||
|             appContext.triggerEvent("refreshNoteList", { noteId: this.parentNote.noteId }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     get isReadOnly() { | ||||
| @@ -57,4 +70,14 @@ export default abstract class ViewMode<T extends object> extends Component { | ||||
|         return this._viewStorage; | ||||
|     } | ||||
|  | ||||
|     async #refreshNoteIds() { | ||||
|         let noteIds: string[]; | ||||
|         if (this.viewType === "list" || this.viewType === "grid") { | ||||
|             noteIds = this.args.parentNote.getChildNoteIds(); | ||||
|         } else { | ||||
|             noteIds = await this.args.parentNote.getSubtreeNoteIds(); | ||||
|         } | ||||
|         this.noteIds = noteIds; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
|     } | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "dotenv": "17.1.0", | ||||
|     "electron": "37.2.0" | ||||
|     "dotenv": "17.2.0", | ||||
|     "electron": "37.2.3" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "@triliumnext/desktop", | ||||
|   "version": "0.96.0", | ||||
|   "version": "0.97.1", | ||||
|   "description": "Build your personal knowledge base with Trilium Notes", | ||||
|   "private": true, | ||||
|   "main": "main.cjs", | ||||
| @@ -17,7 +17,7 @@ | ||||
|     "@types/electron-squirrel-startup": "1.0.2", | ||||
|     "@triliumnext/server": "workspace:*", | ||||
|     "copy-webpack-plugin": "13.0.0", | ||||
|     "electron": "37.2.0", | ||||
|     "electron": "37.2.3", | ||||
|     "@electron-forge/cli": "7.8.1", | ||||
|     "@electron-forge/maker-deb": "7.8.1", | ||||
|     "@electron-forge/maker-dmg": "7.8.1", | ||||
|   | ||||
| @@ -12,7 +12,7 @@ | ||||
|     "@triliumnext/desktop": "workspace:*", | ||||
|     "@types/fs-extra": "11.0.4", | ||||
|     "copy-webpack-plugin": "13.0.0", | ||||
|     "electron": "37.2.0", | ||||
|     "electron": "37.2.3", | ||||
|     "fs-extra": "11.3.0" | ||||
|   }, | ||||
|   "nx": { | ||||
|   | ||||
| @@ -17,6 +17,6 @@ | ||||
|     } | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "dotenv": "17.1.0" | ||||
|     "dotenv": "17.2.0" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| FROM node:22.17.0-bullseye-slim AS builder | ||||
| FROM node:22.17.1-bullseye-slim AS builder | ||||
| RUN corepack enable | ||||
|  | ||||
| # Install native dependencies since we might be building cross-platform. | ||||
| @@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/ | ||||
| # We have to use --no-frozen-lockfile due to CKEditor patches | ||||
| RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild | ||||
|  | ||||
| FROM node:22.17.0-bullseye-slim | ||||
| FROM node:22.17.1-bullseye-slim | ||||
| # Install only runtime dependencies | ||||
| RUN apt-get update && \ | ||||
|     apt-get install -y --no-install-recommends \ | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| FROM node:22.17.0-alpine AS builder | ||||
| FROM node:22.17.1-alpine AS builder | ||||
| RUN corepack enable | ||||
|  | ||||
| # Install native dependencies since we might be building cross-platform. | ||||
| @@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/ | ||||
| # We have to use --no-frozen-lockfile due to CKEditor patches | ||||
| RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild | ||||
|  | ||||
| FROM node:22.17.0-alpine | ||||
| FROM node:22.17.1-alpine | ||||
| # Install runtime dependencies | ||||
| RUN apk add --no-cache su-exec shadow | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| FROM node:22.17.0-alpine AS builder | ||||
| FROM node:22.17.1-alpine AS builder | ||||
| RUN corepack enable | ||||
|  | ||||
| # Install native dependencies since we might be building cross-platform. | ||||
| @@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/ | ||||
| # We have to use --no-frozen-lockfile due to CKEditor patches | ||||
| RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild | ||||
|  | ||||
| FROM node:22.17.0-alpine | ||||
| FROM node:22.17.1-alpine | ||||
| # Create a non-root user with configurable UID/GID | ||||
| ARG USER=trilium | ||||
| ARG UID=1001 | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| FROM node:22.17.0-bullseye-slim AS builder | ||||
| FROM node:22.17.1-bullseye-slim AS builder | ||||
| RUN corepack enable | ||||
|  | ||||
| # Install native dependencies since we might be building cross-platform. | ||||
| @@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/ | ||||
| # We have to use --no-frozen-lockfile due to CKEditor patches | ||||
| RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild | ||||
|  | ||||
| FROM node:22.17.0-bullseye-slim | ||||
| FROM node:22.17.1-bullseye-slim | ||||
| # Create a non-root user with configurable UID/GID | ||||
| ARG USER=trilium | ||||
| ARG UID=1001 | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "@triliumnext/server", | ||||
|   "version": "0.96.0", | ||||
|   "version": "0.97.1", | ||||
|   "description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.", | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
| @@ -52,21 +52,21 @@ | ||||
|     "cheerio": "1.1.0", | ||||
|     "chokidar": "4.0.3", | ||||
|     "cls-hooked": "4.2.2", | ||||
|     "compression": "1.8.0", | ||||
|     "compression": "1.8.1", | ||||
|     "cookie-parser": "1.4.7", | ||||
|     "csrf-csrf": "3.2.2", | ||||
|     "dayjs": "1.11.13", | ||||
|     "debounce": "2.2.0", | ||||
|     "debug": "4.4.1", | ||||
|     "ejs": "3.1.10", | ||||
|     "electron": "37.2.0", | ||||
|     "electron": "37.2.3", | ||||
|     "electron-debug": "4.1.0", | ||||
|     "electron-window-state": "5.0.3", | ||||
|     "escape-html": "1.0.3", | ||||
|     "express": "5.1.0", | ||||
|     "express-openid-connect": "^2.17.1", | ||||
|     "express-rate-limit": "7.5.1", | ||||
|     "express-session": "1.18.1", | ||||
|     "express-rate-limit": "8.0.1", | ||||
|     "express-session": "1.18.2", | ||||
|     "file-uri-to-path": "2.0.0", | ||||
|     "fs-extra": "11.3.0", | ||||
|     "helmet": "8.1.0", | ||||
| @@ -83,12 +83,12 @@ | ||||
|     "jimp": "1.6.0", | ||||
|     "js-yaml": "4.1.0", | ||||
|     "jsdom": "26.1.0", | ||||
|     "marked": "16.0.0", | ||||
|     "marked": "16.1.1", | ||||
|     "mime-types": "3.0.1", | ||||
|     "multer": "2.0.1", | ||||
|     "multer": "2.0.2", | ||||
|     "normalize-strings": "1.1.1", | ||||
|     "ollama": "0.5.16", | ||||
|     "openai": "5.8.3", | ||||
|     "openai": "5.10.1", | ||||
|     "rand-token": "1.0.1", | ||||
|     "safe-compare": "1.1.4", | ||||
|     "sanitize-filename": "1.6.3", | ||||
|   | ||||
							
								
								
									
										2
									
								
								apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										10
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						| @@ -70,24 +70,28 @@ class="image"> | ||||
|           <th><a class="reference-link" href="#root/_help_m523cpzocqaD">Saved Search</a> | ||||
|           </th> | ||||
|           <td>Not supported.</td> | ||||
|           <td> </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th><a class="reference-link" href="#root/_help_iRwzGnHPzonm">Relation Map</a> | ||||
|           </th> | ||||
|           <td>Not supported.</td> | ||||
|           <td> </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th><a class="reference-link" href="#root/_help_bdUJEHsAPYQR">Note Map</a> | ||||
|           </th> | ||||
|           <td>Not supported.</td> | ||||
|           <td> </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th><a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a> | ||||
|           </th> | ||||
|           <td>Not supported.</td> | ||||
|           <td> </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th><a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Book</a> | ||||
|           <th><a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a> | ||||
|           </th> | ||||
|           <td> | ||||
|             <ul> | ||||
| @@ -132,6 +136,7 @@ class="image"> | ||||
|           <th><a class="reference-link" href="#root/_help_1vHRoWCEjj0L">Web View</a> | ||||
|           </th> | ||||
|           <td>Not supported.</td> | ||||
|           <td> </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th><a class="reference-link" href="#root/_help_gBbsAeiuUxI5">Mind Map</a> | ||||
| @@ -144,9 +149,10 @@ class="image"> | ||||
|           </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th><a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map</a> | ||||
|           <th><a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map View</a> | ||||
|           </th> | ||||
|           <td>Not supported.</td> | ||||
|           <td> </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th><a class="reference-link" href="#root/_help_W8vYD3Q1zjCR">File</a> | ||||
|   | ||||
							
								
								
									
										1
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Quick edit.clone.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <p>This is a clone of a note. Go to its <a href="../UI%20Elements/Quick%20edit.html">primary location</a>.</p> | ||||
| Before Width: | Height: | Size: 70 KiB | 
| Before Width: | Height: | Size: 27 KiB | 
| @@ -1,3 +1,7 @@ | ||||
| <figure class="image"> | ||||
|   <img style="aspect-ratio:990/590;" src="Note List_image.png" width="990" | ||||
|   height="590"> | ||||
| </figure> | ||||
| <p>When a note has one or more child notes, they will be listed at the end | ||||
|   of the note for easy navigation.</p> | ||||
| <h2>Configuration</h2> | ||||
| @@ -11,47 +15,11 @@ | ||||
|     the desired number.</li> | ||||
| </ul> | ||||
| <h2>View types</h2> | ||||
| <p>The view types dictate how the child notes are represented.</p> | ||||
| <p>By default, the notes will be displayed in a grid, however there are also | ||||
|   some other view types available.</p> | ||||
| <aside class="admonition tip"> | ||||
|   <p>Generally the view type can only be changed in a <a class="reference-link" | ||||
|     href="#root/_help_GTwFsgaA0lCt">Book</a> note from the <a class="reference-link" | ||||
|     href="#root/_help_BlN9DFI679QC">Ribbon</a>, but it can also be changed | ||||
|     manually on any type of note using the <code>#viewType</code> attribute.</p> | ||||
| </aside> | ||||
| <h3>Grid view</h3> | ||||
| <figure class="image image-style-align-center"> | ||||
|   <img style="aspect-ratio:1025/655;" src="1_Note List_image.png" width="1025" | ||||
|   height="655"> | ||||
| </figure> | ||||
| <p>This view presents the child notes in a grid format, allowing for a more | ||||
|   visual navigation experience.</p> | ||||
| <ul> | ||||
|   <li>For <a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a> notes, | ||||
|     the text can be slighly scrollable via the mouse wheel to reveal more context.</li> | ||||
|   <li>For <a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a> notes, | ||||
|     syntax highlighting is applied.</li> | ||||
|   <li>For <a class="reference-link" href="#root/_help_W8vYD3Q1zjCR">File</a> notes, | ||||
|     a preview is made available for audio, video and PDF notes.</li> | ||||
|   <li>If the note does not have a content, a list of its child notes will be | ||||
|     displayed instead.</li> | ||||
| </ul> | ||||
| <p>This is the default view type.</p> | ||||
| <h3>List view</h3> | ||||
| <figure class="image image-style-align-center"> | ||||
|   <img style="aspect-ratio:1013/526;" src="Note List_image.png" width="1013" | ||||
|   height="526"> | ||||
| </figure> | ||||
| <p>In the list view mode, each note is displayed in a single row with only | ||||
|   the title and the icon of the note being visible by the default. By pressing | ||||
|   the expand button it's possible to view the content of the note, as well | ||||
|   as the children of the note (recursively).</p> | ||||
| <h3>Calendar view</h3> | ||||
| <figure class="image image-style-align-center"> | ||||
|   <img style="aspect-ratio:1090/598;" src="2_Note List_image.png" width="1090" | ||||
|   height="598"> | ||||
| </figure> | ||||
| <p>In the calendar view, child notes are represented as events, with a start | ||||
|   date and optionally an end date. The view also has interaction support | ||||
|   such as moving or creating new events. See <a class="reference-link" | ||||
|   href="#root/_help_xWbu3jpNWapp">Calendar View</a> for more information.</p> | ||||
| <p>Generally the view type can only be changed in a <a class="reference-link" | ||||
|   href="#root/_help_GTwFsgaA0lCt">Collections</a> note from the  | ||||
|   <a | ||||
|   class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>, but it can also be changed manually on any type of note using | ||||
|     the <code>#viewType</code> attribute.</p> | ||||
| @@ -2,8 +2,8 @@ | ||||
|   <img style="aspect-ratio:767/606;" src="4_Calendar View_image.png" width="767" | ||||
|   height="606"> | ||||
| </figure> | ||||
| <p>The Calendar view of Book notes will display each child note in a calendar | ||||
|   that has a start date and optionally an end date, as an event.</p> | ||||
| <p>The Calendar view will display each child note in a calendar that has | ||||
|   a start date and optionally an end date, as an event.</p> | ||||
| <p>The Calendar view has multiple display modes:</p> | ||||
| <ul> | ||||
|   <li>Week view, where all the 7 days of the week (or 5 if the weekends are | ||||
| @@ -14,8 +14,9 @@ | ||||
|   <li>Year view, which displays the entire year for quick reference.</li> | ||||
|   <li>List view, which displays all the events of a given month in sequence.</li> | ||||
| </ul> | ||||
| <p>Unlike other Book view types, the Calendar view also allows some kind | ||||
|   of interaction, such as moving events around as well as creating new ones.</p> | ||||
| <p>Unlike other Collection view types, the Calendar view also allows some | ||||
|   kind of interaction, such as moving events around as well as creating new | ||||
|   ones.</p> | ||||
| <h2>Creating a calendar</h2> | ||||
| <figure class="table"> | ||||
|   <table> | ||||
| @@ -32,17 +33,17 @@ | ||||
|         <td> | ||||
|           <img src="2_Calendar View_image.png"> | ||||
|         </td> | ||||
|         <td>The Calendar View works only for Book note types. To create a new note, | ||||
|           right click on the note tree on the left and select Insert note after, | ||||
|           or Insert child note and then select <em>Book</em>.</td> | ||||
|         <td>The Calendar View works only for Collection note types. To create a new | ||||
|           note, right click on the note tree on the left and select Insert note after, | ||||
|           or Insert child note and then select <em>Collection</em>.</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td>2</td> | ||||
|         <td> | ||||
|           <img src="3_Calendar View_image.png"> | ||||
|         </td> | ||||
|         <td>Once created, the “View type” of the Book needs changed to “Calendar”, | ||||
|           by selecting the “Book Properties” tab in the ribbon.</td> | ||||
|         <td>Once created, the “View type” of the Collection needs changed to “Calendar”, | ||||
|           by selecting the “Collection Properties” tab in the ribbon.</td> | ||||
|       </tr> | ||||
|     </tbody> | ||||
|   </table> | ||||
| @@ -63,7 +64,7 @@ | ||||
|     <img src="Calendar View_image.png"> | ||||
|   </li> | ||||
|   <li>Creating new notes from the calendar will respect the <code>~child:template</code> relation | ||||
|     if set on the book note.</li> | ||||
|     if set on the Collection note.</li> | ||||
| </ul> | ||||
| <h2>Interacting with events</h2> | ||||
| <ul> | ||||
| @@ -71,16 +72,30 @@ | ||||
|     <br> | ||||
|     <img src="7_Calendar View_image.png"> | ||||
|   </li> | ||||
|   <li>Left clicking the event will go to that note. Middle clicking will open | ||||
|     the note in a new tab and right click will offer more options including | ||||
|     opening the note in a new split or window.</li> | ||||
|   <li>Left clicking the event will open a <a class="reference-link" href="#root/_help_ZjLYv08Rp3qC">Quick edit</a> to | ||||
|     edit the note in a popup while allowing easy return to the calendar by | ||||
|     just dismissing the popup. | ||||
|     <ul> | ||||
|       <li>Middle clicking will open the note in a new tab.</li> | ||||
|       <li>Right click will offer more options including opening the note in a new | ||||
|         split or window.</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li>Drag and drop an event on the calendar to move it to another day.</li> | ||||
|   <li>The length of an event can be changed by placing the mouse to the right | ||||
|     edge of the event and dragging the mouse around.</li> | ||||
| </ul> | ||||
| <h2>Configuring the calendar</h2> | ||||
| <p>The following attributes can be added to the book type:</p> | ||||
| <figure class="table"> | ||||
| <h2>Configuring the calendar view</h2> | ||||
| <p>In the <em>Collections</em> tab in the <a class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>, | ||||
|   it's possible to adjust the following:</p> | ||||
| <ul> | ||||
|   <li>Hide weekends from the week view.</li> | ||||
|   <li>Display week numbers on the calendar.</li> | ||||
| </ul> | ||||
| <h2>Configuring the calendar using attributes</h2> | ||||
| <p>The following attributes can be added to the Collection type:</p> | ||||
| <figure | ||||
| class="table"> | ||||
|   <table> | ||||
|     <thead> | ||||
|       <tr> | ||||
| @@ -126,200 +141,169 @@ | ||||
|       </tr> | ||||
|     </tbody> | ||||
|   </table> | ||||
| </figure> | ||||
| <p>In addition, the first day of the week can be either Sunday or Monday | ||||
|   and can be adjusted from the application settings.</p> | ||||
| <h2>Configuring the calendar events</h2> | ||||
| <p>For each note of the calendar, the following attributes can be used:</p> | ||||
| <figure | ||||
| class="table"> | ||||
|   <table> | ||||
|     <thead> | ||||
|       <tr> | ||||
|         <th>Name</th> | ||||
|         <th>Description</th> | ||||
|       </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|       <tr> | ||||
|         <td><code>#startDate</code> | ||||
|         </td> | ||||
|         <td>The date the event starts, which will display it in the calendar. The | ||||
|           format is <code>YYYY-MM-DD</code> (year, month and day separated by a minus | ||||
|           sign).</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#endDate</code> | ||||
|         </td> | ||||
|         <td>Similar to <code>startDate</code>, mentions the end date if the event spans | ||||
|           across multiple days. The date is inclusive, so the end day is also considered. | ||||
|           The attribute can be missing for single-day events.</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#startTime</code> | ||||
|         </td> | ||||
|         <td>The time the event starts at. If this value is missing, then the event | ||||
|           is considered a full-day event. The format is <code>HH:MM</code> (hours in | ||||
|           24-hour format and minutes).</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#endTime</code> | ||||
|         </td> | ||||
|         <td>Similar to <code>startTime</code>, it mentions the time at which the event | ||||
|           ends (in relation with <code>endDate</code> if present, or <code>startDate</code>).</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#color</code> | ||||
|         </td> | ||||
|         <td>Displays the event with a specified color (named such as <code>red</code>, <code>gray</code> or | ||||
|           hex such as <code>#FF0000</code>). This will also change the color of the | ||||
|           note in other places such as the note tree.</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#calendar:color</code> | ||||
|         </td> | ||||
|         <td>Similar to <code>#color</code>, but applies the color only for the event | ||||
|           in the calendar and not for other places such as the note tree.</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#iconClass</code> | ||||
|         </td> | ||||
|         <td>If present, the icon of the note will be displayed to the left of the | ||||
|           event title.</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#calendar:title</code> | ||||
|         </td> | ||||
|         <td>Changes the title of an event to point to an attribute of the note other | ||||
|           than the title, can either a label or a relation (without the <code>#</code> or <code>~</code> symbol). | ||||
|           See <em>Use-cases</em> for more information.</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#calendar:displayedAttributes</code> | ||||
|         </td> | ||||
|         <td>Allows displaying the value of one or more attributes in the calendar | ||||
|           like this:     | ||||
|           <br> | ||||
|           <br> | ||||
|           <img src="9_Calendar View_image.png">   | ||||
|           <br> | ||||
|           <br><code>#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"</code>   | ||||
|           <br> | ||||
|           <br>It can also be used with relations, case in which it will display the | ||||
|           title of the target note:    | ||||
|           <br> | ||||
|           <br><code>~assignee=@My assignee #calendar:displayedAttributes="assignee"</code> | ||||
|         </td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#calendar:startDate</code> | ||||
|         </td> | ||||
|         <td>Allows using a different label to represent the start date, other than <code>startDate</code> (e.g. <code>expiryDate</code>). | ||||
|           The label name <strong>must not be</strong> prefixed with <code>#</code>. | ||||
|           If the label is not defined for a note, the default will be used instead.</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#calendar:endDate</code> | ||||
|         </td> | ||||
|         <td>Similar to <code>#calendar:startDate</code>, allows changing the attribute | ||||
|           which is being used to read the end date.</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#calendar:startTime</code> | ||||
|         </td> | ||||
|         <td>Similar to <code>#calendar:startDate</code>, allows changing the attribute | ||||
|           which is being used to read the start time.</td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>#calendar:endTime</code> | ||||
|         </td> | ||||
|         <td>Similar to <code>#calendar:startDate</code>, allows changing the attribute | ||||
|           which is being used to read the end time.</td> | ||||
|       </tr> | ||||
|     </tbody> | ||||
|   </table> | ||||
|   </figure> | ||||
|    | ||||
| <h2>How the calendar works</h2> | ||||
|   <p> | ||||
|     <img src="11_Calendar View_image.png"> | ||||
|   </p> | ||||
|   <p>The calendar displays all the child notes of the book that have a <code>#startDate</code>. | ||||
|     An <code>#endDate</code> can optionally be added.</p> | ||||
|   <p>If editing the start date and end date from the note itself is desirable, | ||||
|     the following attributes can be added to the book note:</p><pre><code class="language-text-x-trilium-auto">#viewType=calendar #label:startDate(inheritable)="promoted,alias=Start Date,single,date" | ||||
| #label:endDate(inheritable)="promoted,alias=End Date,single,date" | ||||
| #hidePromotedAttributes </code></pre> | ||||
|   <p>This will result in:</p> | ||||
|   <p> | ||||
|     <img src="10_Calendar View_image.png"> | ||||
|   </p> | ||||
|   <p>When not used in a Journal, the calendar is recursive. That is, it will | ||||
|     look for events not just in its child notes but also in the children of | ||||
|     these child notes.</p> | ||||
|   <h2>Use-cases</h2> | ||||
|   <h3>Using with the Journal / calendar</h3> | ||||
|   <p>It is possible to integrate the calendar view into the Journal with day | ||||
|     notes. In order to do so change the note type of the Journal note (calendar | ||||
|     root) to Book and then select the Calendar View.</p> | ||||
|   <p>Based on the <code>#calendarRoot</code> (or <code>#workspaceCalendarRoot</code>) | ||||
|     attribute, the calendar will know that it's in a calendar and apply the | ||||
|     following:</p> | ||||
|   <ul> | ||||
|     <li>The calendar events are now rendered based on their <code>dateNote</code> attribute | ||||
|       rather than <code>startDate</code>.</li> | ||||
|     <li>Interactive editing such as dragging over an empty era or resizing an | ||||
|       event is no longer possible.</li> | ||||
|     <li>Clicking on the empty space on a date will automatically open that day's | ||||
|       note or create it if it does not exist.</li> | ||||
|     <li>Direct children of a day note will be displayed on the calendar despite | ||||
|       not having a <code>dateNote</code> attribute. Children of the child notes | ||||
|       will not be displayed.</li> | ||||
|   </ul> | ||||
|   <img src="8_Calendar View_image.png" width="1217" height="724"> | ||||
|    | ||||
| <h3>Using a different attribute as event title</h3> | ||||
|   <p>By default, events are displayed on the calendar by their note title. | ||||
|     However, it is possible to configure a different attribute to be displayed | ||||
|     instead.</p> | ||||
|   <p>To do so, assign <code>#calendar:title</code> to the child note (not the | ||||
|     calendar/book note), with the value being <code>name</code> where <code>name</code> can | ||||
|     be any label (make not to add the <code>#</code> prefix). The attribute can | ||||
|     also come through inheritance such as a template attribute. If the note | ||||
|     does not have the requested label, the title of the note will be used instead.</p> | ||||
|   <p>In addition, the first day of the week can be either Sunday or Monday | ||||
|     and can be adjusted from the application settings.</p> | ||||
|   <h2>Configuring the calendar events using attributes</h2> | ||||
|   <p>For each note of the calendar, the following attributes can be used:</p> | ||||
|   <figure | ||||
|   class="table" style="width:100%;"> | ||||
|   class="table"> | ||||
|     <table> | ||||
|       <thead> | ||||
|         <tr> | ||||
|           <th> </th> | ||||
|           <th> </th> | ||||
|           <th>Name</th> | ||||
|           <th>Description</th> | ||||
|         </tr> | ||||
|       </thead> | ||||
|       <tbody> | ||||
|         <tr> | ||||
|           <td><pre><code class="language-text-x-trilium-auto">#startDate=2025-02-11 #endDate=2025-02-13 #name="My vacation" #calendar:title="name"</code></pre> | ||||
|           <td><code>#startDate</code> | ||||
|           </td> | ||||
|           <td> | ||||
|             <p> </p> | ||||
|             <figure class="image image-style-align-center"> | ||||
|               <img style="aspect-ratio:445/124;" src="5_Calendar View_image.png" width="445" | ||||
|               height="124"> | ||||
|             </figure> | ||||
|           <td>The date the event starts, which will display it in the calendar. The | ||||
|             format is <code>YYYY-MM-DD</code> (year, month and day separated by a minus | ||||
|             sign).</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#endDate</code> | ||||
|           </td> | ||||
|           <td>Similar to <code>startDate</code>, mentions the end date if the event spans | ||||
|             across multiple days. The date is inclusive, so the end day is also considered. | ||||
|             The attribute can be missing for single-day events.</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#startTime</code> | ||||
|           </td> | ||||
|           <td>The time the event starts at. If this value is missing, then the event | ||||
|             is considered a full-day event. The format is <code>HH:MM</code> (hours in | ||||
|             24-hour format and minutes).</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#endTime</code> | ||||
|           </td> | ||||
|           <td>Similar to <code>startTime</code>, it mentions the time at which the event | ||||
|             ends (in relation with <code>endDate</code> if present, or <code>startDate</code>).</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#color</code> | ||||
|           </td> | ||||
|           <td>Displays the event with a specified color (named such as <code>red</code>, <code>gray</code> or | ||||
|             hex such as <code>#FF0000</code>). This will also change the color of the | ||||
|             note in other places such as the note tree.</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#calendar:color</code> | ||||
|           </td> | ||||
|           <td>Similar to <code>#color</code>, but applies the color only for the event | ||||
|             in the calendar and not for other places such as the note tree.</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#iconClass</code> | ||||
|           </td> | ||||
|           <td>If present, the icon of the note will be displayed to the left of the | ||||
|             event title.</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#calendar:title</code> | ||||
|           </td> | ||||
|           <td>Changes the title of an event to point to an attribute of the note other | ||||
|             than the title, can either a label or a relation (without the <code>#</code> or <code>~</code> symbol). | ||||
|             See <em>Use-cases</em> for more information.</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#calendar:displayedAttributes</code> | ||||
|           </td> | ||||
|           <td>Allows displaying the value of one or more attributes in the calendar | ||||
|             like this:     | ||||
|             <br> | ||||
|             <br> | ||||
|             <img src="9_Calendar View_image.png">   | ||||
|             <br> | ||||
|             <br><code>#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"</code>   | ||||
|             <br> | ||||
|             <br>It can also be used with relations, case in which it will display the | ||||
|             title of the target note:    | ||||
|             <br> | ||||
|             <br><code>~assignee=@My assignee #calendar:displayedAttributes="assignee"</code> | ||||
|           </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#calendar:startDate</code> | ||||
|           </td> | ||||
|           <td>Allows using a different label to represent the start date, other than <code>startDate</code> (e.g. <code>expiryDate</code>). | ||||
|             The label name <strong>must not be</strong> prefixed with <code>#</code>. | ||||
|             If the label is not defined for a note, the default will be used instead.</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#calendar:endDate</code> | ||||
|           </td> | ||||
|           <td>Similar to <code>#calendar:startDate</code>, allows changing the attribute | ||||
|             which is being used to read the end date.</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#calendar:startTime</code> | ||||
|           </td> | ||||
|           <td>Similar to <code>#calendar:startDate</code>, allows changing the attribute | ||||
|             which is being used to read the start time.</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td><code>#calendar:endTime</code> | ||||
|           </td> | ||||
|           <td>Similar to <code>#calendar:startDate</code>, allows changing the attribute | ||||
|             which is being used to read the end time.</td> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|     </table> | ||||
|     </figure> | ||||
|      | ||||
| <h3>Using a relation attribute as event title</h3> | ||||
|     <p>Similarly to using an attribute, use <code>#calendar:title</code> and set | ||||
|       it to <code>name</code> where <code>name</code> is the name of the relation | ||||
|       to use.</p> | ||||
|     <p>Moreover, if there are more relations of the same name, they will be displayed | ||||
|       as multiple events coming from the same note.</p> | ||||
|     <figure class="table" | ||||
|     style="width:100%;"> | ||||
| <h2>How the calendar works</h2> | ||||
|     <p> | ||||
|       <img src="11_Calendar View_image.png"> | ||||
|     </p> | ||||
|     <p>The calendar displays all the child notes of the Collection that have | ||||
|       a <code>#startDate</code>. An <code>#endDate</code> can optionally be added.</p> | ||||
|     <p>If editing the start date and end date from the note itself is desirable, | ||||
|       the following attributes can be added to the Collection note:</p><pre><code class="language-text-x-trilium-auto">#viewType=calendar #label:startDate(inheritable)="promoted,alias=Start Date,single,date" | ||||
| #label:endDate(inheritable)="promoted,alias=End Date,single,date" | ||||
| #hidePromotedAttributes </code></pre> | ||||
|     <p>This will result in:</p> | ||||
|     <p> | ||||
|       <img src="10_Calendar View_image.png"> | ||||
|     </p> | ||||
|     <p>When not used in a Journal, the calendar is recursive. That is, it will | ||||
|       look for events not just in its child notes but also in the children of | ||||
|       these child notes.</p> | ||||
|     <h2>Use-cases</h2> | ||||
|     <h3>Using with the Journal / calendar</h3> | ||||
|     <p>It is possible to integrate the calendar view into the Journal with day | ||||
|       notes. In order to do so change the note type of the Journal note (calendar | ||||
|       root) to Collection and then select the Calendar View.</p> | ||||
|     <p>Based on the <code>#calendarRoot</code> (or <code>#workspaceCalendarRoot</code>) | ||||
|       attribute, the calendar will know that it's in a calendar and apply the | ||||
|       following:</p> | ||||
|     <ul> | ||||
|       <li>The calendar events are now rendered based on their <code>dateNote</code> attribute | ||||
|         rather than <code>startDate</code>.</li> | ||||
|       <li>Interactive editing such as dragging over an empty era or resizing an | ||||
|         event is no longer possible.</li> | ||||
|       <li>Clicking on the empty space on a date will automatically open that day's | ||||
|         note or create it if it does not exist.</li> | ||||
|       <li>Direct children of a day note will be displayed on the calendar despite | ||||
|         not having a <code>dateNote</code> attribute. Children of the child notes | ||||
|         will not be displayed.</li> | ||||
|     </ul> | ||||
|     <img src="8_Calendar View_image.png" width="1217" height="724"> | ||||
|      | ||||
| <h3>Using a different attribute as event title</h3> | ||||
|     <p>By default, events are displayed on the calendar by their note title. | ||||
|       However, it is possible to configure a different attribute to be displayed | ||||
|       instead.</p> | ||||
|     <p>To do so, assign <code>#calendar:title</code> to the child note (not the | ||||
|       calendar/Collection note), with the value being <code>name</code> where <code>name</code> can | ||||
|       be any label (make not to add the <code>#</code> prefix). The attribute can | ||||
|       also come through inheritance such as a template attribute. If the note | ||||
|       does not have the requested label, the title of the note will be used instead.</p> | ||||
|     <figure | ||||
|     class="table" style="width:100%;"> | ||||
|       <table> | ||||
|         <thead> | ||||
|           <tr> | ||||
| @@ -329,39 +313,70 @@ class="table"> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           <tr> | ||||
|             <td><pre><code class="language-text-x-trilium-auto">#startDate=2025-02-14 #endDate=2025-02-15 ~for=@John Smith ~for=@Jane Doe #calendar:title="for"</code></pre> | ||||
|             </td> | ||||
|             <td> | ||||
|               <img src="6_Calendar View_image.png" width="294" height="151"> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </figure> | ||||
|     <p>Note that it's even possible to have a <code>#calendar:title</code> on the | ||||
|       target note (e.g. “John Smith”) which will try to render an attribute of | ||||
|       it. Note that it's not possible to use a relation here as well for safety | ||||
|       reasons (an accidental recursion  of attributes could cause the application | ||||
|       to loop infinitely).</p> | ||||
|     <figure class="table" style="width:100%;"> | ||||
|       <table> | ||||
|         <thead> | ||||
|           <tr> | ||||
|             <th> </th> | ||||
|             <th> </th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           <tr> | ||||
|             <td><pre><code class="language-text-x-trilium-auto">#calendar:title="shortName" #shortName="John S."</code></pre> | ||||
|             <td><pre><code class="language-text-x-trilium-auto">#startDate=2025-02-11 #endDate=2025-02-13 #name="My vacation" #calendar:title="name"</code></pre> | ||||
|             </td> | ||||
|             <td> | ||||
|               <p> </p> | ||||
|               <figure class="image image-style-align-center"> | ||||
|                 <img style="aspect-ratio:296/150;" src="1_Calendar View_image.png" width="296" | ||||
|                 height="150"> | ||||
|                 <img style="aspect-ratio:445/124;" src="5_Calendar View_image.png" width="445" | ||||
|                 height="124"> | ||||
|               </figure> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </figure> | ||||
|       </figure> | ||||
|        | ||||
| <h3>Using a relation attribute as event title</h3> | ||||
|       <p>Similarly to using an attribute, use <code>#calendar:title</code> and set | ||||
|         it to <code>name</code> where <code>name</code> is the name of the relation | ||||
|         to use.</p> | ||||
|       <p>Moreover, if there are more relations of the same name, they will be displayed | ||||
|         as multiple events coming from the same note.</p> | ||||
|       <figure class="table" | ||||
|       style="width:100%;"> | ||||
|         <table> | ||||
|           <thead> | ||||
|             <tr> | ||||
|               <th> </th> | ||||
|               <th> </th> | ||||
|             </tr> | ||||
|           </thead> | ||||
|           <tbody> | ||||
|             <tr> | ||||
|               <td><pre><code class="language-text-x-trilium-auto">#startDate=2025-02-14 #endDate=2025-02-15 ~for=@John Smith ~for=@Jane Doe #calendar:title="for"</code></pre> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <img src="6_Calendar View_image.png" width="294" height="151"> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </figure> | ||||
|       <p>Note that it's even possible to have a <code>#calendar:title</code> on the | ||||
|         target note (e.g. “John Smith”) which will try to render an attribute of | ||||
|         it. Note that it's not possible to use a relation here as well for safety | ||||
|         reasons (an accidental recursion  of attributes could cause the application | ||||
|         to loop infinitely).</p> | ||||
|       <figure class="table" style="width:100%;"> | ||||
|         <table> | ||||
|           <thead> | ||||
|             <tr> | ||||
|               <th> </th> | ||||
|               <th> </th> | ||||
|             </tr> | ||||
|           </thead> | ||||
|           <tbody> | ||||
|             <tr> | ||||
|               <td><pre><code class="language-text-x-trilium-auto">#calendar:title="shortName" #shortName="John S."</code></pre> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <figure class="image image-style-align-center"> | ||||
|                   <img style="aspect-ratio:296/150;" src="1_Calendar View_image.png" width="296" | ||||
|                   height="150"> | ||||
|                 </figure> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </figure> | ||||
| @@ -1,8 +1,8 @@ | ||||
| <aside class="admonition important"> | ||||
|   <p>Starting with Trilium v0.97.0, the geo map has been converted from a standalone | ||||
|     <a | ||||
|     href="#root/pOsGYCXsbNQG/_help_KSZ04uQ2D1St">note type</a>to a type of view for the <a class="reference-link" | ||||
|       href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_0ESUbbAxVnoK">Note List</a>. </p> | ||||
|     href="#root/_help_KSZ04uQ2D1St">note type</a>to a type of view for the <a class="reference-link" | ||||
|       href="#root/_help_0ESUbbAxVnoK">Note List</a>. </p> | ||||
| </aside> | ||||
| <figure class="image image-style-align-center"> | ||||
|   <img style="aspect-ratio:892/675;" src="9_Geo Map View_image.png" width="892" | ||||
| @@ -45,6 +45,7 @@ | ||||
|     </tbody> | ||||
|   </table> | ||||
| </figure> | ||||
|  | ||||
| <h2>Repositioning the map</h2> | ||||
| <ul> | ||||
|   <li>Click and drag the map in order to move across the map.</li> | ||||
| @@ -109,6 +110,7 @@ | ||||
|     </tbody> | ||||
|   </table> | ||||
| </figure> | ||||
|  | ||||
| <h3>Adding a new note using the contextual menu</h3> | ||||
| <ol> | ||||
|   <li>Right click anywhere on the map, where to place the newly created marker | ||||
| @@ -119,13 +121,13 @@ | ||||
| </ol> | ||||
| <h3>Adding an existing note on note from the note tree</h3> | ||||
| <ol> | ||||
|   <li>Select the desired note in the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>.</li> | ||||
|   <li>Select the desired note in the <a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</li> | ||||
|   <li>Hold the mouse on the note and drag it to the map to the desired location.</li> | ||||
|   <li>The map should be updated with the new marker.</li> | ||||
| </ol> | ||||
| <p>This works for:</p> | ||||
| <ul> | ||||
|   <li>Notes that are not part of the geo map, case in which a <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_IakOLONlIfGI">clone</a> will | ||||
|   <li>Notes that are not part of the geo map, case in which a <a href="#root/_help_IakOLONlIfGI">clone</a> will | ||||
|     be created.</li> | ||||
|   <li>Notes that are a child of the geo map but not yet positioned on the map.</li> | ||||
|   <li>Notes that are a child of the geo map and also positioned, case in which | ||||
| @@ -134,9 +136,8 @@ | ||||
| <h2>How the location of the markers is stored</h2> | ||||
| <p>The location of a marker is stored in the <code>#geolocation</code> attribute | ||||
|   of the child notes:</p> | ||||
| <p> | ||||
|   <img src="18_Geo Map View_image.png" width="1288" height="278"> | ||||
| </p> | ||||
| <img src="18_Geo Map View_image.png" width="1288" | ||||
| height="278"> | ||||
| <p>This value can be added manually if needed. The value of the attribute | ||||
|   is made up of the latitude and longitude separated by a comma.</p> | ||||
| <h2>Repositioning markers</h2> | ||||
| @@ -148,19 +149,18 @@ | ||||
|   page (<kbd>Ctrl</kbd>+<kbd>R</kbd> ) to cancel it.</p> | ||||
| <h2>Interaction with the markers</h2> | ||||
| <ul> | ||||
|   <li>Hovering over a marker will display the content of the note it belongs | ||||
|     to. | ||||
|   <li>Hovering over a marker will display a <a class="reference-link" href="#root/_help_lgKX7r3aL30x">Note Tooltip</a> with | ||||
|     the content of the note it belongs to. | ||||
|     <ul> | ||||
|       <li>Clicking on the note title in the tooltip will navigate to the note in | ||||
|         the current view.</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li>Middle-clicking the marker will open the note in a new tab.</li> | ||||
|   <li>Right-clicking the marker will open a contextual menu allowing: | ||||
|     <ul> | ||||
|       <li> </li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li>Right-clicking the marker will open a contextual menu (as described below).</li> | ||||
|   <li>If the map is in read-only mode, clicking on a marker will open a  | ||||
|     <a | ||||
|     class="reference-link" href="#root/_help_ZjLYv08Rp3qC">Quick edit</a> popup for the corresponding note.</li> | ||||
| </ul> | ||||
| <h2>Contextual menu</h2> | ||||
| <p>It's possible to press the right mouse button to display a contextual | ||||
| @@ -261,6 +261,7 @@ | ||||
|     </tbody> | ||||
|   </table> | ||||
| </figure> | ||||
|  | ||||
| <h3>Adding from OpenStreetMap</h3> | ||||
| <p>Similarly to the Google Maps approach:</p> | ||||
| <figure class="table" style="width:100%;"> | ||||
| @@ -310,6 +311,7 @@ | ||||
|     </tbody> | ||||
|   </table> | ||||
| </figure> | ||||
|  | ||||
| <h2>Adding GPS tracks (.gpx)</h2> | ||||
| <p>Trilium has basic support for displaying GPS tracks on the geo map.</p> | ||||
| <figure | ||||
| @@ -377,19 +379,20 @@ class="table" style="width:100%;"> | ||||
|   <p>When a map is in read-only all editing features will be disabled such | ||||
|     as:</p> | ||||
|   <ul> | ||||
|     <li>The add button in the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_XpOYSgsLkTJy">Floating buttons</a>.</li> | ||||
|     <li>The add button in the <a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>.</li> | ||||
|     <li>Dragging markers.</li> | ||||
|     <li>Editing from the contextual menu (removing locations or adding new items).</li> | ||||
|   </ul> | ||||
|   <p>To enable read-only mode simply press the <em>Lock</em> icon from the  | ||||
|     <a | ||||
|     class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_XpOYSgsLkTJy">Floating buttons</a>. To disable it, press the button again.</p> | ||||
|     class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>. To disable it, press the button again.</p> | ||||
|   <h2>Troubleshooting</h2> | ||||
|   <figure class="image image-style-align-right image_resized" style="width:34.06%;"> | ||||
|     <img style="aspect-ratio:678/499;" src="13_Geo Map View_image.png" width="678" | ||||
|     height="499"> | ||||
|   </figure> | ||||
|   <h3>Grid-like artifacts on the map</h3> | ||||
|    | ||||
| <h3>Grid-like artifacts on the map</h3> | ||||
|   <p>This occurs if the application is not at 100% zoom which causes the pixels | ||||
|     of the map to not render correctly due to fractional scaling. The only | ||||
|     possible solution is to set the UI zoom at 100% (default keyboard shortcut | ||||
|   | ||||
							
								
								
									
										30
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Grid View.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| <figure class="image"> | ||||
|   <img style="aspect-ratio:990/590;" src="Grid View_image.png" width="990" | ||||
|   height="590"> | ||||
| </figure> | ||||
| <p>This view presents the child notes in a grid format, allowing for a more | ||||
|   visual navigation experience.</p> | ||||
| <p>Each tile contains:</p> | ||||
| <ul> | ||||
|   <li>The title of a note.</li> | ||||
|   <li>A snippet of the content.</li> | ||||
|   <li>For empty notes, the sub-children are also displayed, allowing for quick | ||||
|     navigation.</li> | ||||
| </ul> | ||||
| <p>Depending on the type of note:</p> | ||||
| <ul> | ||||
|   <li>For <a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a> notes, | ||||
|     the text can be slightly scrollable via the mouse wheel to reveal more | ||||
|     context.</li> | ||||
|   <li>For <a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a> notes, | ||||
|     syntax highlighting is applied.</li> | ||||
|   <li>For <a class="reference-link" href="#root/_help_W8vYD3Q1zjCR">File</a> notes, | ||||
|     a preview is made available for audio, video and PDF notes.</li> | ||||
|   <li>If the note does not have a content, a list of its child notes will be | ||||
|     displayed instead.</li> | ||||
| </ul> | ||||
| <p>The grid view is also used by default in the <a class="reference-link" | ||||
|   href="#root/_help_0ESUbbAxVnoK">Note List</a> of every note, making | ||||
|   it easy to navigate to children notes.</p> | ||||
| <h2>Configuration</h2> | ||||
| <p>Unlike most other view types, the grid view is not actually configurable.</p> | ||||
							
								
								
									
										
											BIN
										
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Grid View_image.png
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 78 KiB | 
							
								
								
									
										20
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/List View.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | ||||
| <figure class="image"> | ||||
|   <img style="aspect-ratio:1387/758;" src="List View_image.png" width="1387" | ||||
|   height="758"> | ||||
| </figure> | ||||
| <p>List view is similar to <a class="reference-link" href="#root/_help_8QqnMzx393bx">Grid View</a>, | ||||
|   but in the list view mode, each note is displayed in a single row with | ||||
|   only the title and the icon of the note being visible by the default. By | ||||
|   pressing the expand button it's possible to view the content of the note, | ||||
|   as well as the children of the note (recursively).</p> | ||||
| <p>In the example above, the "Node.js" note on the left panel contains several | ||||
|   child notes. The right panel displays the content of these child notes | ||||
|   as a single continuous document.</p> | ||||
| <h2>Interaction</h2> | ||||
| <ul> | ||||
|   <li>Each note can be expanded or collapsed by clicking on the arrow to the | ||||
|     left of the title.</li> | ||||
|   <li>In the <a class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>, | ||||
|     in the <em>Collection</em> tab there are options to expand and to collapse | ||||
|     all notes easily.</li> | ||||
| </ul> | ||||
							
								
								
									
										
											BIN
										
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/List View_image.png
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 119 KiB | 
| @@ -5,30 +5,86 @@ | ||||
| <p>The table view displays information in a grid, where the rows are individual | ||||
|   notes and the columns are <a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>. | ||||
|   In addition, values are editable.</p> | ||||
| <h2>How it works</h2> | ||||
| <p>The tabular structure is represented as such:</p> | ||||
| <ul> | ||||
|   <li>Each child note is a row in the table.</li> | ||||
|   <li>If child rows also have children, they will be displayed under an expander | ||||
|     (nested notes).</li> | ||||
|   <li>Each column is a <a href="#root/_help_OFXdgB2nNk1F">promoted attribute</a> that | ||||
|     is defined on the Collection note. | ||||
|     <ul> | ||||
|       <li>Actually, both promoted and unpromoted attributes are supported, but it's | ||||
|         a requirement to use a label/relation definition.</li> | ||||
|       <li>The promoted attributes are usually defined as inheritable in order to | ||||
|         show up in the child notes, but it's not a requirement.</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li>If there are multiple attribute definitions with the same <code>name</code>, | ||||
|     only one will be displayed.</li> | ||||
| </ul> | ||||
| <p>There are also a few predefined columns:</p> | ||||
| <ul> | ||||
|   <li>The current item number, identified by the <code>#</code> symbol. | ||||
|     <ul> | ||||
|       <li>This simply counts the note and is affected by sorting.</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li><a class="reference-link" href="#root/_help_m1lbrzyKDaRB">Note ID</a>, | ||||
|     representing the unique ID used internally by Trilium</li> | ||||
|   <li>The title of the note.</li> | ||||
| </ul> | ||||
| <h2>Interaction</h2> | ||||
| <h3>Creating a new table</h3> | ||||
| <p>Right click the <a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> and | ||||
|   select <em>Insert child note</em> and look for the <em>Table item</em>.</p> | ||||
| <h3>Adding columns</h3> | ||||
| <p>Each column is a <a href="#root/_help_OFXdgB2nNk1F">promoted attribute</a> that | ||||
|   is defined on the Book note. Ideally, the promoted attributes need to be | ||||
|   inheritable in order to show up in the child notes.</p> | ||||
| <p>To create a new column, simply press <em>Add new column</em> at the bottom | ||||
|   of the table.</p> | ||||
| <p>There are also a few predefined columns:</p> | ||||
| <p>Each column is a <a href="#root/_help_OFXdgB2nNk1F">promoted or unpromoted attribute</a> that | ||||
|   is defined on the Collection note.</p> | ||||
| <p>To create a new column, either:</p> | ||||
| <ul> | ||||
|   <li>The current item number, identified by the <code>#</code> symbol. This simply | ||||
|     counts the note and is affected by sorting.</li> | ||||
|   <li><a class="reference-link" href="#root/_help_m1lbrzyKDaRB">Note ID</a>, | ||||
|     representing the unique ID used internally by Trilium</li> | ||||
|   <li>The title of the note.</li> | ||||
|   <li>Press <em>Add new column</em> at the bottom of the table.</li> | ||||
|   <li>Right click on an existing column and select Add column to the left/right.</li> | ||||
|   <li>Right click on the empty space of the column header and select <em>Label</em> or <em>Relation</em> in | ||||
|     the <em>New column</em> section.</li> | ||||
| </ul> | ||||
| <h3>Adding new rows</h3> | ||||
| <p>Each row is actually a note that is a child of the book note.</p> | ||||
| <p>To create a new note, press <em>Add new row</em> at the bottom of the table. | ||||
|   By default it will try to edit the title of the newly created note.</p> | ||||
| <p>Alternatively, the note can be created from the<a class="reference-link" | ||||
| <p>Each row is actually a note that is a child of the Collection note.</p> | ||||
| <p>To create a new note, either:</p> | ||||
| <ul> | ||||
|   <li>Press <em>Add new row</em> at the bottom of the table.</li> | ||||
|   <li>Right click on an existing row and select <em>Insert row above, Insert child note</em> or <em>Insert row below</em>.</li> | ||||
| </ul> | ||||
| <p>By default it will try to edit the title of the newly created note.</p> | ||||
| <p>Alternatively, the note can be created from the <a class="reference-link" | ||||
|   href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> or <a href="#root/_help_CdNpE2pqjmI6">scripting</a>.</p> | ||||
| <h3>Context menu</h3> | ||||
| <p>There are multiple menus:</p> | ||||
| <ul> | ||||
|   <li>Right clicking on a column, allows: | ||||
|     <ul> | ||||
|       <li>Sorting by the selected column and resetting the sort.</li> | ||||
|       <li>Hiding the selected column or adjusting the visibility of every column.</li> | ||||
|       <li>Adding new columns to the left or the right of the column.</li> | ||||
|       <li>Editing the current column.</li> | ||||
|       <li>Deleting the current column.</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li>Right clicking on the space to the right of the columns, allows: | ||||
|     <ul> | ||||
|       <li>Adjusting the visibility of every column.</li> | ||||
|       <li>Adding new columns.</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li>Right clicking on a row, allows: | ||||
|     <ul> | ||||
|       <li>Opening the corresponding note of the row in a new tab, split, window | ||||
|         or quick editing it.</li> | ||||
|       <li>Inserting rows above, below or as a child note.</li> | ||||
|       <li>Deleting the row.</li> | ||||
|     </ul> | ||||
|   </li> | ||||
| </ul> | ||||
| <h3>Editing data</h3> | ||||
| <p>Simply click on a cell within a row to change its value. The change will | ||||
|   not only reflect in the table, but also as an attribute of the corresponding | ||||
| @@ -37,16 +93,34 @@ | ||||
|   <li>The editing will respect the type of the promoted attribute, by presenting | ||||
|     a normal text box, a number selector or a date selector for example.</li> | ||||
|   <li>It also possible to change the title of a note.</li> | ||||
|   <li>Editing relations is also possible, by using the note autocomplete.</li> | ||||
|   <li>Editing relations is also possible | ||||
|     <ul> | ||||
|       <li>Simply click on a relation and it will become editable. Enter the text | ||||
|         to look for a note and click on it.</li> | ||||
|       <li>To remove a relation, remove the title of the note from the text box and | ||||
|         click outside the cell.</li> | ||||
|     </ul> | ||||
|   </li> | ||||
| </ul> | ||||
| <h3>Editing columns</h3> | ||||
| <p>It is possible to edit a column by right clicking it and selecting <em>Edit column.</em> This | ||||
|   will basically change the label/relation definition at the collection level.</p> | ||||
| <p>If the <em>Name</em> field of a column is changed, this will trigger a batch | ||||
|   operation in which the corresponding label/relation will be renamed in | ||||
|   all the children.</p> | ||||
| <h2>Working with the data</h2> | ||||
| <h3>Sorting</h3> | ||||
| <p>It is possible to sort the data by the values of a column:</p> | ||||
| <h3>Sorting by column</h3> | ||||
| <p>By default, the order of the notes matches the order in the <a class="reference-link" | ||||
|   href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>. However, it is possible | ||||
|   to sort the data by the values of a column:</p> | ||||
| <ul> | ||||
|   <li>To do so, simply click on a column.</li> | ||||
|   <li>To switch between ascending or descending sort, simply click again on | ||||
|     the same column. The arrow next to the column will indicate the direction | ||||
|     of the sort.</li> | ||||
|   <li>To disable sorting and fall back to the original order, right click any | ||||
|     column on the header and select <em>Clear sorting.</em> | ||||
|   </li> | ||||
| </ul> | ||||
| <h3>Reordering and hiding columns</h3> | ||||
| <ul> | ||||
| @@ -55,36 +129,52 @@ | ||||
|     the item corresponding to the column.</li> | ||||
| </ul> | ||||
| <h3>Reordering rows</h3> | ||||
| <p>Notes can be dragged around to change their order. This will also change | ||||
|   the order of the note in the <a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</p> | ||||
| <p>Currently, it's possible to reorder notes even if sorting is used, but | ||||
|   the result might be inconsistent.</p> | ||||
| <h2>Limitations</h2> | ||||
| <p>The table functionality is still in its early stages, as such it faces | ||||
|   quite a few important limitations:</p> | ||||
| <ol> | ||||
|   <li>As mentioned previously, the columns of the table are defined as  | ||||
|     <a | ||||
|     class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>. | ||||
|       <ol> | ||||
|         <li>But only the promoted attributes that are defined at the level of the | ||||
|           Book note are actually taken into consideration.</li> | ||||
|         <li>There are plans to recursively look for columns across the sub-hierarchy.</li> | ||||
|       </ol> | ||||
| <p>Notes can be dragged around to change their order. To do so, move the | ||||
|   mouse over the three vertical dots near the number row and drag the mouse | ||||
|   to the desired position.</p> | ||||
| <p>This will also change the order of the note in the <a class="reference-link" | ||||
|   href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</p> | ||||
| <p>Reordering does have some limitations:</p> | ||||
| <ul> | ||||
|   <li>If the parent note has <code>#sorted</code>, reordering will be disabled.</li> | ||||
|   <li>If using nested tables, then reordering will also be disabled.</li> | ||||
|   <li>Currently, it's possible to reorder notes even if column sorting is used, | ||||
|     but the result might be inconsistent.</li> | ||||
| </ul> | ||||
| <h3>Nested trees</h3> | ||||
| <p>If the child notes of the collection also have their own child notes, | ||||
|   then they will be displayed in a hierarchy.</p> | ||||
| <p>Next to the title of each element there will be a button to expand or | ||||
|   collapse. By default, all items are expanded.</p> | ||||
| <p>Since nesting is not always desirable, it is possible to limit the nesting | ||||
|   to a certain number of levels or even disable it completely. To do so, | ||||
|   either:</p> | ||||
| <ul> | ||||
|   <li>Go to <em>Collection Properties</em> in the <a class="reference-link" | ||||
|     href="#root/_help_BlN9DFI679QC">Ribbon</a> and look for the <em>Max nesting depth</em> section. | ||||
|     <ul> | ||||
|       <li>To disable nesting, type 0 and press Enter.</li> | ||||
|       <li>To limit to a certain depth, type in the desired number (e.g. 2 to only | ||||
|         display children and sub-children).</li> | ||||
|       <li>To re-enable unlimited nesting, remove the number and press Enter.</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li>Hierarchy is not yet supported, so the table will only show the items | ||||
|     that are direct children of the <em>Book</em> note.</li> | ||||
|   <li>Multiple labels and relations are not supported. If a <a class="reference-link" | ||||
|     href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> is defined | ||||
|     with a <em>Multi value</em> specificity, they will be ignored.</li> | ||||
| </ol> | ||||
|   <li>Manually set <code>maxNestingDepth</code> to the desired value.</li> | ||||
| </ul> | ||||
| <p>Limitations:</p> | ||||
| <ul> | ||||
|   <li>While in this mode, it's not possible to reorder notes.</li> | ||||
| </ul> | ||||
| <h2>Limitations</h2> | ||||
| <p>Multi-value labels and relations are not supported. If a <a class="reference-link" | ||||
|   href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> is defined | ||||
|   with a <em>Multi value</em> specificity, they will be ignored.</p> | ||||
| <h2>Use in search</h2> | ||||
| <p>The table view can be used in a <a class="reference-link" href="#root/_help_m523cpzocqaD">Saved Search</a> by | ||||
|   adding the <code>#viewType=table</code> attribute.</p> | ||||
| <p>Unlike when used in a book, saved searches are not limited to the sub-hierarchy | ||||
|   of a note and allows for advanced queries thanks to the power of the  | ||||
|   <a | ||||
|   class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a>.</p> | ||||
| <p>Unlike when used in a Collection, saved searches are not limited to the | ||||
|   sub-hierarchy of a note and allows for advanced queries thanks to the power | ||||
|   of the <a class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a>.</p> | ||||
| <p>However, there are also some limitations:</p> | ||||
| <ul> | ||||
|   <li>It's not possible to reorder notes.</li> | ||||
|   | ||||
| Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 78 KiB | 
| @@ -54,7 +54,7 @@ | ||||
|     hide the Mermaid source code and display the diagram preview in full-size. | ||||
|     In this case, the read-only mode can be easily toggled on or off via a | ||||
|     dedicated button in the <a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a> area.</li> | ||||
|   <li><a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/0ESUbbAxVnoK/_help_81SGnPGMk7Xc">Geo Map View</a> will | ||||
|   <li><a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map View</a> will | ||||
|     disallow all interaction that would otherwise change the map (dragging | ||||
|     notes, adding new items).</li> | ||||
| </ul> | ||||
							
								
								
									
										36
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tooltip.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| <figure class="image image-style-align-right"> | ||||
|   <img style="aspect-ratio:505/261;" src="Note Tooltip_image.png" width="505" | ||||
|   height="261"> | ||||
| </figure> | ||||
| <p>The note tooltip is a convenience feature which displays a popup when | ||||
|   hovering over an <a href="#root/_help_hrZ1D00cLbal">internal link</a> to | ||||
|   another note.</p> | ||||
| <p>The following information is displayed:</p> | ||||
| <ul> | ||||
|   <li>The note path, at the top of the popup.</li> | ||||
|   <li>The title of the note. | ||||
|     <ul> | ||||
|       <li>Clicking on the title will open the note in the current tab.</li> | ||||
|       <li>Holding <kbd>Ctrl</kbd> pressed while clicking the title will open in a | ||||
|         new tab instead of the current one.</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li>A snippet of the content will be displayed as well.</li> | ||||
|   <li>A button to <a href="#root/_help_ZjLYv08Rp3qC">quickly edit</a> the note | ||||
|     in a popup.</li> | ||||
| </ul> | ||||
| <p>The tooltip can be found in multiple places, including:</p> | ||||
| <ul> | ||||
|   <li>In <a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a> notes, | ||||
|     when hovering over <a class="reference-link" href="#root/_help_hrZ1D00cLbal">Internal (reference) links</a> .</li> | ||||
|   <li><a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a>:  | ||||
|     <ul> | ||||
|       <li><a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map View</a>, | ||||
|         when hovering over a marker.</li> | ||||
|       <li><a class="reference-link" href="#root/_help_xWbu3jpNWapp">Calendar View</a>, | ||||
|         when hovering over an event.</li> | ||||
|       <li><a class="reference-link" href="#root/_help_2FvYrpmOXm29">Table View</a>, | ||||
|         when hovering over a note title, or over a <a href="#root/_help_Cq5X6iKQop6R">relation</a>.</li> | ||||
|     </ul> | ||||
|   </li> | ||||
| </ul> | ||||
							
								
								
									
										
											BIN
										
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tooltip_image.png
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 18 KiB | 
| @@ -30,5 +30,8 @@ | ||||
|   in the context menu, or with the associated keyboard <a href="#root/_help_A9Oc6YKKc65v">shortcuts</a>: <code>CTRL-C</code> ( | ||||
|   <a | ||||
|   href="#root/_help_IakOLONlIfGI">copy</a>), <kbd>Ctrl</kbd> + <kbd>X</kbd> (cut) and <kbd>Ctrl</kbd> + <kbd>V</kbd> (paste).</p> | ||||
| <p>See <a class="reference-link" href="#root/_help_YtSN43OrfzaA">Note Tree Menu</a> for | ||||
|   more information.</p> | ||||
| <p>See <a class="reference-link" href="#root/_help_YtSN43OrfzaA">Note tree contextual menu</a> for | ||||
|   more information.</p> | ||||
| <h2>Keyboard shortcuts</h2> | ||||
| <p>The note tree comes with multiple keyboard shortcuts to make editing faster, | ||||
|   consult the dedicated <a class="reference-link" href="#root/_help_DvdZhoQZY9Yd">Keyboard shortcuts</a> section.</p> | ||||
| Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 43 KiB |