Compare commits
	
		
			332 Commits
		
	
	
		
			fix/resolv
			...
			v0.97.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 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] | github: [eliandoran] | ||||||
| custom: ["https://paypal.me/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> | zadam <adam.zivner@gmail.com> | ||||||
| Adam Zivner <zadam.apps@gmail.com> | zadam <zadam.apps@gmail.com> | ||||||
							
								
								
									
										9
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -28,5 +28,12 @@ | |||||||
|     "typescript.validate.enable": true, |     "typescript.validate.enable": true, | ||||||
|     "typescript.tsserver.experimental.enableProjectDiagnostics": true, |     "typescript.tsserver.experimental.enableProjectDiagnostics": true, | ||||||
|     "typescript.tsdk": "node_modules/typescript/lib", |     "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 | # Trilium Notes | ||||||
|  |  | ||||||
|  | Donate:   | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| [](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) | [](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): | Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080): | ||||||
| ```shell | ```shell | ||||||
| git clone https://github.com/TriliumNext/Notes.git | git clone https://github.com/TriliumNext/Trilium.git | ||||||
| cd Notes | cd Trilium | ||||||
| pnpm install | pnpm install | ||||||
| pnpm run server:start | 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: | Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation: | ||||||
| ```shell | ```shell | ||||||
| git clone https://github.com/TriliumNext/Notes.git | git clone https://github.com/TriliumNext/Trilium.git | ||||||
| cd Notes | cd Trilium | ||||||
| pnpm install | pnpm install | ||||||
| pnpm nx run edit-docs:edit-docs | pnpm nx run edit-docs:edit-docs | ||||||
| ``` | ``` | ||||||
| @@ -138,8 +139,8 @@ pnpm nx run edit-docs:edit-docs | |||||||
| ### Building the Executable | ### Building the Executable | ||||||
| Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows: | Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows: | ||||||
| ```shell | ```shell | ||||||
| git clone https://github.com/TriliumNext/Notes.git | git clone https://github.com/TriliumNext/Trilium.git | ||||||
| cd Notes | cd Trilium | ||||||
| pnpm install | pnpm install | ||||||
| pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32 | pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32 | ||||||
| ``` | ``` | ||||||
|   | |||||||
| @@ -35,13 +35,13 @@ | |||||||
|     "chore:generate-openapi": "tsx bin/generate-openapi.js" |     "chore:generate-openapi": "tsx bin/generate-openapi.js" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": {     |   "devDependencies": {     | ||||||
|     "@playwright/test": "1.53.2", |     "@playwright/test": "1.54.1", | ||||||
|     "@stylistic/eslint-plugin": "5.1.0",         |     "@stylistic/eslint-plugin": "5.2.0",         | ||||||
|     "@types/express": "5.0.3",     |     "@types/express": "5.0.3",     | ||||||
|     "@types/node": "22.16.2",     |     "@types/node": "22.16.5",     | ||||||
|     "@types/yargs": "17.0.33", |     "@types/yargs": "17.0.33", | ||||||
|     "@vitest/coverage-v8": "3.2.4", |     "@vitest/coverage-v8": "3.2.4", | ||||||
|     "eslint": "9.30.1", |     "eslint": "9.31.0", | ||||||
|     "eslint-plugin-simple-import-sort": "12.1.1", |     "eslint-plugin-simple-import-sort": "12.1.1", | ||||||
|     "esm": "3.2.25", |     "esm": "3.2.25", | ||||||
|     "jsdoc": "4.0.4", |     "jsdoc": "4.0.4", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "@triliumnext/client", |   "name": "@triliumnext/client", | ||||||
|   "version": "0.96.0", |   "version": "0.97.0", | ||||||
|   "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", |   "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "license": "AGPL-3.0-only", |   "license": "AGPL-3.0-only", | ||||||
| @@ -10,7 +10,7 @@ | |||||||
|     "url": "https://github.com/TriliumNext/Notes" |     "url": "https://github.com/TriliumNext/Notes" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@eslint/js": "9.30.1", |     "@eslint/js": "9.31.0", | ||||||
|     "@excalidraw/excalidraw": "0.18.0", |     "@excalidraw/excalidraw": "0.18.0", | ||||||
|     "@fullcalendar/core": "6.1.18", |     "@fullcalendar/core": "6.1.18", | ||||||
|     "@fullcalendar/daygrid": "6.1.18", |     "@fullcalendar/daygrid": "6.1.18", | ||||||
| @@ -46,9 +46,9 @@ | |||||||
|     "leaflet": "1.9.4", |     "leaflet": "1.9.4", | ||||||
|     "leaflet-gpx": "2.2.0", |     "leaflet-gpx": "2.2.0", | ||||||
|     "mark.js": "8.11.1", |     "mark.js": "8.11.1", | ||||||
|     "marked": "16.0.0", |     "marked": "16.1.1", | ||||||
|     "mermaid": "11.8.1", |     "mermaid": "11.9.0", | ||||||
|     "mind-elixir": "5.0.1", |     "mind-elixir": "5.0.2", | ||||||
|     "normalize.css": "8.0.1", |     "normalize.css": "8.0.1", | ||||||
|     "panzoom": "9.4.3", |     "panzoom": "9.4.3", | ||||||
|     "preact": "10.26.9", |     "preact": "10.26.9", | ||||||
| @@ -58,7 +58,7 @@ | |||||||
|     "vanilla-js-wheel-zoom": "9.0.4" |     "vanilla-js-wheel-zoom": "9.0.4" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@ckeditor/ckeditor5-inspector": "4.1.0", |     "@ckeditor/ckeditor5-inspector": "5.0.0", | ||||||
|     "@types/bootstrap": "5.2.10", |     "@types/bootstrap": "5.2.10", | ||||||
|     "@types/jquery": "3.5.32", |     "@types/jquery": "3.5.32", | ||||||
|     "@types/leaflet": "1.9.20", |     "@types/leaflet": "1.9.20", | ||||||
| @@ -68,7 +68,7 @@ | |||||||
|     "copy-webpack-plugin": "13.0.0", |     "copy-webpack-plugin": "13.0.0", | ||||||
|     "happy-dom": "18.0.1", |     "happy-dom": "18.0.1", | ||||||
|     "script-loader": "0.7.2", |     "script-loader": "0.7.2", | ||||||
|     "vite-plugin-static-copy": "3.1.0" |     "vite-plugin-static-copy": "3.1.1" | ||||||
|   }, |   }, | ||||||
|   "nx": { |   "nx": { | ||||||
|     "name": "client", |     "name": "client", | ||||||
|   | |||||||
| @@ -28,6 +28,8 @@ import TouchBarComponent from "./touch_bar.js"; | |||||||
| import type { CKTextEditor } from "@triliumnext/ckeditor5"; | import type { CKTextEditor } from "@triliumnext/ckeditor5"; | ||||||
| import type CodeMirror from "@triliumnext/codemirror"; | import type CodeMirror from "@triliumnext/codemirror"; | ||||||
| import { StartupChecks } from "./startup_checks.js"; | import { StartupChecks } from "./startup_checks.js"; | ||||||
|  | import type { CreateNoteOpts } from "../services/note_create.js"; | ||||||
|  | import { ColumnComponent } from "tabulator-tables"; | ||||||
|  |  | ||||||
| interface Layout { | interface Layout { | ||||||
|     getRootWidget: (appContext: AppContext) => RootWidget; |     getRootWidget: (appContext: AppContext) => RootWidget; | ||||||
| @@ -122,6 +124,7 @@ export type CommandMappings = { | |||||||
|     showImportDialog: CommandData & { noteId: string }; |     showImportDialog: CommandData & { noteId: string }; | ||||||
|     openNewNoteSplit: NoteCommandData; |     openNewNoteSplit: NoteCommandData; | ||||||
|     openInWindow: NoteCommandData; |     openInWindow: NoteCommandData; | ||||||
|  |     openInPopup: CommandData & { noteIdOrPath: string; }; | ||||||
|     openNoteInNewTab: CommandData; |     openNoteInNewTab: CommandData; | ||||||
|     openNoteInNewSplit: CommandData; |     openNoteInNewSplit: CommandData; | ||||||
|     openNoteInNewWindow: CommandData; |     openNoteInNewWindow: CommandData; | ||||||
| @@ -140,6 +143,7 @@ export type CommandMappings = { | |||||||
|     }; |     }; | ||||||
|     openInTab: ContextMenuCommandData; |     openInTab: ContextMenuCommandData; | ||||||
|     openNoteInSplit: ContextMenuCommandData; |     openNoteInSplit: ContextMenuCommandData; | ||||||
|  |     openNoteInPopup: ContextMenuCommandData; | ||||||
|     toggleNoteHoisting: ContextMenuCommandData; |     toggleNoteHoisting: ContextMenuCommandData; | ||||||
|     insertNoteAfter: ContextMenuCommandData; |     insertNoteAfter: ContextMenuCommandData; | ||||||
|     insertChildNote: ContextMenuCommandData; |     insertChildNote: ContextMenuCommandData; | ||||||
| @@ -274,6 +278,21 @@ export type CommandMappings = { | |||||||
|  |  | ||||||
|     geoMapCreateChildNote: CommandData; |     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 & { |     buildTouchBar: CommandData & { | ||||||
|         TouchBar: typeof TouchBar; |         TouchBar: typeof TouchBar; | ||||||
|         buildIcon(name: string): NativeImage; |         buildIcon(name: string): NativeImage; | ||||||
|   | |||||||
| @@ -256,6 +256,20 @@ class FNote { | |||||||
|         return this.children; |         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() { |     async getChildNotes() { | ||||||
|         return await this.froca.getNotes(this.children); |         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 FindWidget from "../widgets/find.js"; | ||||||
| import TocWidget from "../widgets/toc.js"; | import TocWidget from "../widgets/toc.js"; | ||||||
| import HighlightsListWidget from "../widgets/highlights_list.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 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 FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; | ||||||
| import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; | import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; | ||||||
| import SvgExportButton from "../widgets/floating_buttons/svg_export_button.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 ScrollPaddingWidget from "../widgets/scroll_padding.js"; | ||||||
| import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; | import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; | ||||||
| import options from "../services/options.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 GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js"; | ||||||
| import ContextualHelpButton from "../widgets/floating_buttons/help_button.js"; | import ContextualHelpButton from "../widgets/floating_buttons/help_button.js"; | ||||||
| import CloseZenButton from "../widgets/close_zen_button.js"; | import CloseZenButton from "../widgets/close_zen_button.js"; | ||||||
| @@ -229,7 +208,7 @@ export default class DesktopLayout { | |||||||
|                                                                 .child(new PromotedAttributesWidget()) |                                                                 .child(new PromotedAttributesWidget()) | ||||||
|                                                                 .child(new SqlTableSchemasWidget()) |                                                                 .child(new SqlTableSchemasWidget()) | ||||||
|                                                                 .child(new NoteDetailWidget()) |                                                                 .child(new NoteDetailWidget()) | ||||||
|                                                                 .child(new NoteListWidget()) |                                                                 .child(new NoteListWidget(false)) | ||||||
|                                                                 .child(new SearchResultWidget()) |                                                                 .child(new SearchResultWidget()) | ||||||
|                                                                 .child(new SqlResultWidget()) |                                                                 .child(new SqlResultWidget()) | ||||||
|                                                                 .child(new ScrollPaddingWidget()) |                                                                 .child(new ScrollPaddingWidget()) | ||||||
|   | |||||||
| @@ -22,6 +22,14 @@ import RevisionsDialog from "../widgets/dialogs/revisions.js"; | |||||||
| import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js"; | import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js"; | ||||||
| import InfoDialog from "../widgets/dialogs/info.js"; | import InfoDialog from "../widgets/dialogs/info.js"; | ||||||
| import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.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) { | export function applyModals(rootContainer: RootContainer) { | ||||||
|     rootContainer |     rootContainer | ||||||
| @@ -47,4 +55,15 @@ export function applyModals(rootContainer: RootContainer) { | |||||||
|         .child(new ConfirmDialog()) |         .child(new ConfirmDialog()) | ||||||
|         .child(new PromptDialog()) |         .child(new PromptDialog()) | ||||||
|         .child(new IncorrectCpuArchDialog()) |         .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() |                                     .filling() | ||||||
|                                     .contentSized() |                                     .contentSized() | ||||||
|                                     .child(new NoteDetailWidget()) |                                     .child(new NoteDetailWidget()) | ||||||
|                                     .child(new NoteListWidget()) |                                     .child(new NoteListWidget(false)) | ||||||
|                                     .child(new FilePropertiesWidget().css("font-size", "smaller")) |                                     .child(new FilePropertiesWidget().css("font-size", "smaller")) | ||||||
|                             ) |                             ) | ||||||
|                             .child(new MobileEditorToolbar()) |                             .child(new MobileEditorToolbar()) | ||||||
|   | |||||||
| @@ -26,6 +26,11 @@ export interface MenuCommandItem<T> { | |||||||
|     title: string; |     title: string; | ||||||
|     command?: T; |     command?: T; | ||||||
|     type?: string; |     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; |     uiIcon?: string; | ||||||
|     badges?: MenuItemBadge[]; |     badges?: MenuItemBadge[]; | ||||||
|     templateNoteId?: string; |     templateNoteId?: string; | ||||||
|   | |||||||
| @@ -16,7 +16,8 @@ function getItems(): MenuItem<CommandNames>[] { | |||||||
|     return [ |     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_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_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 }); |         appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope }); | ||||||
|     } else if (command === "openNoteInNewWindow") { |     } else if (command === "openNoteInNewWindow") { | ||||||
|         appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); |         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)[] = [ |         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-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-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 |             isHoisted | ||||||
|                 ? null |                 ? null | ||||||
| @@ -129,12 +129,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree | |||||||
|                         enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp |                         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.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: "----" }, | ||||||
|  |  | ||||||
| @@ -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.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>`, |                 title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, | ||||||
|                 command: "deleteNotes", |                 command: "deleteNotes", | ||||||
| @@ -246,6 +247,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree | |||||||
|             const { ntxId } = subContexts?.[subContexts.length - 1] ?? {}; |             const { ntxId } = subContexts?.[subContexts.length - 1] ?? {}; | ||||||
|  |  | ||||||
|             this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath }); |             this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath }); | ||||||
|  |         } else if (command === "openNoteInPopup") { | ||||||
|  |             appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath }) | ||||||
|         } else if (command === "convertNoteToAttachment") { |         } else if (command === "convertNoteToAttachment") { | ||||||
|             if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) { |             if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) { | ||||||
|                 return; |                 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`, { |     await server.put(`notes/${noteId}/set-attribute`, { | ||||||
|         type: "label", |         type: "label", | ||||||
|         name: name, |         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); |     branchIdsToDelete = filterRootNote(branchIdsToDelete); | ||||||
|  |  | ||||||
|     if (branchIdsToDelete.length === 0) { |     if (branchIdsToDelete.length === 0) { | ||||||
| @@ -110,11 +118,13 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f | |||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (moveToParent) { | ||||||
|         try { |         try { | ||||||
|             await activateParentNotePath(); |             await activateParentNotePath(); | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error(e); |             console.error(e); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const taskId = utils.randomString(10); |     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 RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; | ||||||
| import { t } from "./i18n.js"; | import { t } from "./i18n.js"; | ||||||
| import type FNote from "../entities/fnote.js"; | import type FNote from "../entities/fnote.js"; | ||||||
|  | import toast from "./toast.js"; | ||||||
|  | import { BulkAction } from "@triliumnext/commons"; | ||||||
|  |  | ||||||
| const ACTION_GROUPS = [ | const ACTION_GROUPS = [ | ||||||
|     { |     { | ||||||
| @@ -89,6 +91,17 @@ function parseActions(note: FNote) { | |||||||
|         .filter((action) => !!action); |         .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 { | export default { | ||||||
|     addAction, |     addAction, | ||||||
|     parseActions, |     parseActions, | ||||||
|   | |||||||
| @@ -4,14 +4,14 @@ import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptio | |||||||
| import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; | import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; | ||||||
| import { focusSavedElement, saveFocusedElement } from "./focus.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) { |     if (closeActDialog) { | ||||||
|         closeActiveDialog(); |         closeActiveDialog(); | ||||||
|         glob.activeDialog = $dialog; |         glob.activeDialog = $dialog; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     saveFocusedElement(); |     saveFocusedElement(); | ||||||
|     Modal.getOrCreateInstance($dialog[0]).show(); |     Modal.getOrCreateInstance($dialog[0], config).show(); | ||||||
|  |  | ||||||
|     $dialog.on("hidden.bs.modal", () => { |     $dialog.on("hidden.bs.modal", () => { | ||||||
|         const $autocompleteEl = $(".aa-input"); |         const $autocompleteEl = $(".aa-input"); | ||||||
| @@ -41,8 +41,14 @@ async function info(message: string) { | |||||||
|     return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); |     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) { | async function confirm(message: string) { | ||||||
|     return new Promise((res) => |     return new Promise<boolean>((res) => | ||||||
|         appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{ |         appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{ | ||||||
|             message, |             message, | ||||||
|             callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed) |             callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed) | ||||||
|   | |||||||
| @@ -231,6 +231,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) { | |||||||
|     let ntxId: string | null = null; |     let ntxId: string | null = null; | ||||||
|     let hoistedNoteId: string | null = null; |     let hoistedNoteId: string | null = null; | ||||||
|     let searchString: string | null = null; |     let searchString: string | null = null; | ||||||
|  |     let openInPopup = false; | ||||||
|  |  | ||||||
|     if (paramString) { |     if (paramString) { | ||||||
|         for (const pair of paramString.split("&")) { |         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 |                 searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla | ||||||
|             } else if (["viewMode", "attachmentId"].includes(name)) { |             } else if (["viewMode", "attachmentId"].includes(name)) { | ||||||
|                 (viewScope as any)[name] = value; |                 (viewScope as any)[name] = value; | ||||||
|  |             } else if (name === "popup") { | ||||||
|  |                 openInPopup = true; | ||||||
|             } else { |             } else { | ||||||
|                 console.warn(`Unrecognized hash parameter '${name}'.`); |                 console.warn(`Unrecognized hash parameter '${name}'.`); | ||||||
|             } |             } | ||||||
| @@ -266,7 +269,8 @@ export function parseNavigationStateFromUrl(url: string | undefined) { | |||||||
|         ntxId, |         ntxId, | ||||||
|         hoistedNoteId, |         hoistedNoteId, | ||||||
|         viewScope, |         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 ctrlKey = evt && utils.isCtrlKey(evt); | ||||||
|     const shiftKey = evt?.shiftKey; |     const shiftKey = evt?.shiftKey; | ||||||
|     const isLeftClick = !evt || ("which" in evt && evt.which === 1); |     const isLeftClick = !evt || ("which" in evt && evt.which === 1); | ||||||
|  |     // Right click is handled separately. | ||||||
|     const isMiddleClick = evt && "which" in evt && evt.which === 2; |     const isMiddleClick = evt && "which" in evt && evt.which === 2; | ||||||
|     const targetIsBlank = ($link?.attr("target") === "_blank"); |     const targetIsBlank = ($link?.attr("target") === "_blank"); | ||||||
|     const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank; |     const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank; | ||||||
| @@ -311,7 +316,9 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | |||||||
|     const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey; |     const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey; | ||||||
|  |  | ||||||
|     if (notePath) { |     if (notePath) { | ||||||
|         if (openInNewWindow) { |         if (isLeftClick && openInPopup) { | ||||||
|  |             appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath }); | ||||||
|  |         } else if (openInNewWindow) { | ||||||
|             appContext.triggerCommand("openInWindow", { notePath, viewScope }); |             appContext.triggerCommand("openInWindow", { notePath, viewScope }); | ||||||
|         } else if (openInNewTab) { |         } else if (openInNewTab) { | ||||||
|             appContext.tabManager.openTabWithNoteWithHoisting(notePath, { |             appContext.tabManager.openTabWithNoteWithHoisting(notePath, { | ||||||
| @@ -387,12 +394,18 @@ function linkContextMenu(e: PointerEvent) { | |||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (utils.isCtrlKey(e) && e.button === 2) { | ||||||
|  |         appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath }); | ||||||
|  |         e.preventDefault(); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|  |  | ||||||
|     linkContextMenuService.openContextMenu(notePath, e, viewScope, null); |     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"); |     const $link = $el[0].tagName === "A" ? $el : $el.find("a"); | ||||||
|  |  | ||||||
|     href = href || $link.attr("href"); |     href = href || $link.attr("href"); | ||||||
|   | |||||||
| @@ -40,7 +40,10 @@ interface Options { | |||||||
|     allowCreatingNotes?: boolean; |     allowCreatingNotes?: boolean; | ||||||
|     allowJumpToSearchNotes?: boolean; |     allowJumpToSearchNotes?: boolean; | ||||||
|     allowExternalLinks?: boolean; |     allowExternalLinks?: boolean; | ||||||
|  |     /** If set, hides the right-side button corresponding to go to selected note. */ | ||||||
|     hideGoToSelectedNoteButton?: boolean; |     hideGoToSelectedNoteButton?: boolean; | ||||||
|  |     /** If set, hides all right-side buttons in the autocomplete dropdown */ | ||||||
|  |     hideAllButtons?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| async function autocompleteSourceForCKEditor(queryText: string) { | 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"); |     const $goToSelectedNoteButton = $("<a>").addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right"); | ||||||
|  |  | ||||||
|  |     if (!options.hideAllButtons) { | ||||||
|         $el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton); |         $el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (!options.hideGoToSelectedNoteButton) { |     if (!options.hideGoToSelectedNoteButton && !options.hideAllButtons) { | ||||||
|         $el.after($goToSelectedNoteButton); |         $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 { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; | ||||||
| import type { CKTextEditor } from "@triliumnext/ckeditor5"; | import type { CKTextEditor } from "@triliumnext/ckeditor5"; | ||||||
|  |  | ||||||
| interface CreateNoteOpts { | export interface CreateNoteOpts { | ||||||
|     isProtected?: boolean; |     isProtected?: boolean; | ||||||
|     saveSelection?: boolean; |     saveSelection?: boolean; | ||||||
|     title?: string | null; |     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 { ViewModeArgs } from "../widgets/view_widgets/view_mode.js"; | ||||||
| import type ViewMode 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 type ViewTypeOptions = "list" | "grid" | "calendar" | "table" | "geoMap"; | ||||||
|  |  | ||||||
| export default class NoteListRenderer { | export default class NoteListRenderer { | ||||||
|  |  | ||||||
|     private viewType: ViewTypeOptions; |     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); |         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 { |     #getViewType(parentNote: FNote): ViewTypeOptions { | ||||||
| @@ -47,15 +32,36 @@ export default class NoteListRenderer { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     get isFullHeight() { |     get isFullHeight() { | ||||||
|         return this.viewMode?.isFullHeight; |         switch (this.viewType) { | ||||||
|  |             case "list": | ||||||
|  |             case "grid": | ||||||
|  |                 return false; | ||||||
|  |             default: | ||||||
|  |                 return true; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async renderList() { |     async renderList() { | ||||||
|         if (!this.viewMode) { |         const args = this.args; | ||||||
|             return null; |         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) { |         if (isContentEmpty) { | ||||||
|             classes.push("note-no-content"); |             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>`; |     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 += $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; |     return content; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -81,8 +81,8 @@ body { | |||||||
|  |  | ||||||
|     /* -- Overrides the default colors used by the ckeditor5-image package. --------------------- */ |     /* -- Overrides the default colors used by the ckeditor5-image package. --------------------- */ | ||||||
|  |  | ||||||
|     --ck-color-image-caption-background: var(--main-background-color); |     --ck-content-color-image-caption-background: var(--main-background-color); | ||||||
|     --ck-color-image-caption-text: var(--main-text-color); |     --ck-content-color-image-caption-text: var(--main-text-color); | ||||||
|  |  | ||||||
|     /* -- Overrides the default colors used by the ckeditor5-widget package. -------------------- */ |     /* -- 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; |     color: var(--menu-text-color) !important; | ||||||
|     font-size: inherit; |     font-size: inherit; | ||||||
|     background-color: var(--menu-background-color) !important; |     background-color: var(--menu-background-color) !important; | ||||||
| @@ -342,7 +343,8 @@ button kbd { | |||||||
|     break-after: avoid; |     break-after: avoid; | ||||||
| } | } | ||||||
|  |  | ||||||
| body.desktop .dropdown-menu { | body.desktop .dropdown-menu, | ||||||
|  | body.desktop .tabulator-popup-container { | ||||||
|     border: 1px solid var(--dropdown-border-color); |     border: 1px solid var(--dropdown-border-color); | ||||||
|     box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity)); |     box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity)); | ||||||
|     animation: dropdown-menu-opening 100ms ease-in; |     animation: dropdown-menu-opening 100ms ease-in; | ||||||
| @@ -385,7 +387,8 @@ body.desktop .dropdown-menu { | |||||||
| } | } | ||||||
|  |  | ||||||
| .dropdown-menu a:hover:not(.disabled), | .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; |     color: var(--hover-item-text-color) !important; | ||||||
|     background-color: var(--hover-item-background-color) !important; |     background-color: var(--hover-item-background-color) !important; | ||||||
|     border-color: var(--hover-item-border-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) */ |     /* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */ | ||||||
|     min-width: 0; |     min-width: 0; | ||||||
|     padding: 0; |     padding: 0; | ||||||
|  |     z-index: 1000; | ||||||
| } | } | ||||||
|  |  | ||||||
| pre:not(.hljs) { | pre:not(.hljs) { | ||||||
| @@ -771,6 +775,14 @@ table.promoted-attributes-in-tooltip th { | |||||||
|     font-size: small; |     font-size: small; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .note-tooltip-content .open-popup-button { | ||||||
|  |     position: absolute; | ||||||
|  |     right: 15px; | ||||||
|  |     bottom: 8px; | ||||||
|  |     font-size: 1.2em; | ||||||
|  |     color: inherit; | ||||||
|  | } | ||||||
|  |  | ||||||
| .note-tooltip-attributes { | .note-tooltip-attributes { | ||||||
|     display: -webkit-box; |     display: -webkit-box; | ||||||
|     -webkit-box-orient: vertical; |     -webkit-box-orient: vertical; | ||||||
| @@ -912,6 +924,13 @@ div[data-notify="container"] { | |||||||
|     font-family: var(--monospace-font-family); |     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 { | .ck-content .table table th { | ||||||
|     background-color: var(--accented-background-color); |     background-color: var(--accented-background-color); | ||||||
| } | } | ||||||
| @@ -1198,12 +1217,14 @@ body.mobile .dropdown-submenu > .dropdown-menu { | |||||||
| } | } | ||||||
|  |  | ||||||
| #context-menu-container, | #context-menu-container, | ||||||
| #context-menu-container .dropdown-menu { | #context-menu-container .dropdown-menu, | ||||||
|     padding: 3px 0 0; | .tabulator-popup-container { | ||||||
|  |     padding: 3px 0; | ||||||
|     z-index: 2000; |     z-index: 2000; | ||||||
| } | } | ||||||
|  |  | ||||||
| #context-menu-container .dropdown-item { | #context-menu-container .dropdown-item, | ||||||
|  | .tabulator-menu .tabulator-menu-item { | ||||||
|     padding: 0 7px 0 10px; |     padding: 0 7px 0 10px; | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|     user-select: none; |     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(./pages.css); | ||||||
| @import url(./ribbon.css); | @import url(./ribbon.css); | ||||||
| @import url(./notes/text.css); | @import url(./notes/text.css); | ||||||
|  | @import url(./notes/collections/table.css); | ||||||
|  |  | ||||||
| @font-face { | @font-face { | ||||||
|     font-family: "Inter"; |     font-family: "Inter"; | ||||||
| @@ -183,7 +184,7 @@ html body .dropdown-item[disabled] { | |||||||
|  |  | ||||||
| /* Menu item icon */ | /* Menu item icon */ | ||||||
| .dropdown-item .bx { | .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; |     color: var(--menu-item-icon-color) !important; | ||||||
|     font-size: 1.1em; |     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": "移动到...", |     "move-to": "移动到...", | ||||||
|     "paste-into": "粘贴到里面", |     "paste-into": "粘贴到里面", | ||||||
|     "paste-after": "粘贴到后面", |     "paste-after": "粘贴到后面", | ||||||
|     "duplicate-subtree": "复制子树", |  | ||||||
|     "export": "导出", |     "export": "导出", | ||||||
|     "import-into-note": "导入到笔记", |     "import-into-note": "导入到笔记", | ||||||
|     "apply-bulk-actions": "应用批量操作", |     "apply-bulk-actions": "应用批量操作", | ||||||
|   | |||||||
| @@ -1384,7 +1384,7 @@ | |||||||
|     "move-to": "Verschieben nach...", |     "move-to": "Verschieben nach...", | ||||||
|     "paste-into": "Als Unternotiz einfügen", |     "paste-into": "Als Unternotiz einfügen", | ||||||
|     "paste-after": "Danach einfügen", |     "paste-after": "Danach einfügen", | ||||||
|     "duplicate-subtree": "Notizbaum duplizieren", |     "duplicate": "Duplizieren", | ||||||
|     "export": "Exportieren", |     "export": "Exportieren", | ||||||
|     "import-into-note": "In Notiz importieren", |     "import-into-note": "In Notiz importieren", | ||||||
|     "apply-bulk-actions": "Massenaktionen ausführen", |     "apply-bulk-actions": "Massenaktionen ausführen", | ||||||
|   | |||||||
| @@ -1025,7 +1025,7 @@ | |||||||
|     "title": "Consistency Checks", |     "title": "Consistency Checks", | ||||||
|     "find_and_fix_button": "Find and fix consistency issues", |     "find_and_fix_button": "Find and fix consistency issues", | ||||||
|     "finding_and_fixing_message": "Finding and fixing 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": { |   "database_anonymization": { | ||||||
|     "title": "Database Anonymization", |     "title": "Database Anonymization", | ||||||
| @@ -1595,12 +1595,13 @@ | |||||||
|     "move-to": "Move to...", |     "move-to": "Move to...", | ||||||
|     "paste-into": "Paste into", |     "paste-into": "Paste into", | ||||||
|     "paste-after": "Paste after", |     "paste-after": "Paste after", | ||||||
|     "duplicate-subtree": "Duplicate subtree", |     "duplicate": "Duplicate", | ||||||
|     "export": "Export", |     "export": "Export", | ||||||
|     "import-into-note": "Import into note", |     "import-into-note": "Import into note", | ||||||
|     "apply-bulk-actions": "Apply bulk actions", |     "apply-bulk-actions": "Apply bulk actions", | ||||||
|     "converted-to-attachments": "{{count}} notes have been converted to attachments.", |     "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_info": { | ||||||
|     "shared_publicly": "This note is shared publicly on", |     "shared_publicly": "This note is shared publicly on", | ||||||
| @@ -1832,7 +1833,8 @@ | |||||||
|   "link_context_menu": { |   "link_context_menu": { | ||||||
|     "open_note_in_new_tab": "Open note in a new tab", |     "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_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": { |   "electron_integration": { | ||||||
|     "desktop-application": "Desktop Application", |     "desktop-application": "Desktop Application", | ||||||
| @@ -1852,7 +1854,8 @@ | |||||||
|     "full-text-search": "Full text search" |     "full-text-search": "Full text search" | ||||||
|   }, |   }, | ||||||
|   "note_tooltip": { |   "note_tooltip": { | ||||||
|     "note-has-been-deleted": "Note has been deleted." |     "note-has-been-deleted": "Note has been deleted.", | ||||||
|  |     "quick-edit": "Quick edit" | ||||||
|   }, |   }, | ||||||
|   "geo-map": { |   "geo-map": { | ||||||
|     "create-child-note-title": "Create a new child note and add it to the map", |     "create-child-note-title": "Create a new child note and add it to the map", | ||||||
| @@ -1941,10 +1944,29 @@ | |||||||
|   }, |   }, | ||||||
|   "table_view": { |   "table_view": { | ||||||
|     "new-row": "New row", |     "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": { |   "book_properties_config": { | ||||||
|     "hide-weekends": "Hide weekends", |     "hide-weekends": "Hide weekends", | ||||||
|     "display-week-numbers": "Display week numbers" |     "display-week-numbers": "Display week numbers" | ||||||
|  |   }, | ||||||
|  |   "table_context_menu": { | ||||||
|  |     "delete_row": "Delete row" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1593,7 +1593,7 @@ | |||||||
|     "move-to": "Mover a...", |     "move-to": "Mover a...", | ||||||
|     "paste-into": "Pegar en", |     "paste-into": "Pegar en", | ||||||
|     "paste-after": "Pegar después de", |     "paste-after": "Pegar después de", | ||||||
|     "duplicate-subtree": "Duplicar subárbol", |     "duplicate": "Duplicar", | ||||||
|     "export": "Exportar", |     "export": "Exportar", | ||||||
|     "import-into-note": "Importar a nota", |     "import-into-note": "Importar a nota", | ||||||
|     "apply-bulk-actions": "Aplicar acciones en lote", |     "apply-bulk-actions": "Aplicar acciones en lote", | ||||||
|   | |||||||
| @@ -1389,7 +1389,7 @@ | |||||||
|     "move-to": "Déplacer vers...", |     "move-to": "Déplacer vers...", | ||||||
|     "paste-into": "Coller dans", |     "paste-into": "Coller dans", | ||||||
|     "paste-after": "Coller après", |     "paste-after": "Coller après", | ||||||
|     "duplicate-subtree": "Dupliquer le sous-arbre", |     "duplicate": "Dupliquer", | ||||||
|     "export": "Exporter", |     "export": "Exporter", | ||||||
|     "import-into-note": "Importer dans la note", |     "import-into-note": "Importer dans la note", | ||||||
|     "apply-bulk-actions": "Appliquer des Actions groupées", |     "apply-bulk-actions": "Appliquer des Actions groupées", | ||||||
|   | |||||||
| @@ -1349,7 +1349,7 @@ | |||||||
|     "copy-note-path-to-clipboard": "Copiază calea notiței în clipboard", |     "copy-note-path-to-clipboard": "Copiază calea notiței în clipboard", | ||||||
|     "cut": "Decupează", |     "cut": "Decupează", | ||||||
|     "delete": "Șterge", |     "delete": "Șterge", | ||||||
|     "duplicate-subtree": "Dublifică ierarhia", |     "duplicate": "Dublifică", | ||||||
|     "edit-branch-prefix": "Editează prefixul ramurii", |     "edit-branch-prefix": "Editează prefixul ramurii", | ||||||
|     "expand-subtree": "Expandează subnotițele", |     "expand-subtree": "Expandează subnotițele", | ||||||
|     "export": "Exportă", |     "export": "Exportă", | ||||||
|   | |||||||
| @@ -1336,7 +1336,6 @@ | |||||||
|     "move-to": "移動到...", |     "move-to": "移動到...", | ||||||
|     "paste-into": "貼上到裡面", |     "paste-into": "貼上到裡面", | ||||||
|     "paste-after": "貼上到後面", |     "paste-after": "貼上到後面", | ||||||
|     "duplicate-subtree": "複製子樹", |  | ||||||
|     "export": "匯出", |     "export": "匯出", | ||||||
|     "import-into-note": "匯入到筆記", |     "import-into-note": "匯入到筆記", | ||||||
|     "apply-bulk-actions": "應用批量操作", |     "apply-bulk-actions": "應用批量操作", | ||||||
|   | |||||||
| @@ -78,7 +78,7 @@ const TPL = /*html*/` | |||||||
|         } |         } | ||||||
|     </style> |     </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> |         <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> |         <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; |     x: number; | ||||||
|     y: number; |     y: number; | ||||||
|     focus?: "name"; |     focus?: "name"; | ||||||
|  |     parent?: HTMLElement; | ||||||
|  |     hideMultiplicity?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface SearchRelatedResponse { | 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) { |         if (!attribute) { | ||||||
|             this.hide(); |             this.hide(); | ||||||
|  |  | ||||||
| @@ -528,7 +530,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { | |||||||
|         this.$rowPromotedAlias.toggle(!!definition.isPromoted); |         this.$rowPromotedAlias.toggle(!!definition.isPromoted); | ||||||
|         this.$inputPromotedAlias.val(definition.promotedAlias || "").attr("disabled", disabledFn); |         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.$inputMultiplicity.val(definition.multiplicity || "").attr("disabled", disabledFn); | ||||||
|  |  | ||||||
|         this.$rowLabelType.toggle(this.attrType === "label-definition"); |         this.$rowLabelType.toggle(this.attrType === "label-definition"); | ||||||
| @@ -560,18 +562,21 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { | |||||||
|  |  | ||||||
|         this.toggleInt(true); |         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 detPosition = this.getDetailPosition(x, offset); | ||||||
|         const outerHeight = this.$widget.outerHeight(); |         const outerHeight = this.$widget.outerHeight(); | ||||||
|         const height = $(window).height(); |         const height = $(window).height(); | ||||||
|  |  | ||||||
|         if (detPosition && outerHeight && height) { |         if (!detPosition || !outerHeight || !height) { | ||||||
|  |             console.warn("Can't position popup, is it attached?"); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         this.$widget |         this.$widget | ||||||
|             .css("left", detPosition.left) |             .css("left", detPosition.left) | ||||||
|             .css("right", detPosition.right) |             .css("right", detPosition.right) | ||||||
|             .css("top", y - offset.top + 70) |             .css("top", y - offset.top + 70) | ||||||
|             .css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000); |             .css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000); | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (focus === "name") { |         if (focus === "name") { | ||||||
|             this.$inputName.trigger("focus").trigger("select"); |             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 server from "../../services/server.js"; | ||||||
| import contextMenuService from "../../menus/context_menu.js"; | import contextMenuService from "../../menus/context_menu.js"; | ||||||
| import attributeParser, { type Attribute } from "../../services/attribute_parser.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 froca from "../../services/froca.js"; | ||||||
| import attributeRenderer from "../../services/attribute_renderer.js"; | import attributeRenderer from "../../services/attribute_renderer.js"; | ||||||
| import noteCreateService from "../../services/note_create.js"; | import noteCreateService from "../../services/note_create.js"; | ||||||
| @@ -417,16 +417,16 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem | |||||||
|         this.$editor.tooltip("show"); |         this.$editor.tooltip("show"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     getClickIndex(pos: Position) { |     getClickIndex(pos: ModelPosition) { | ||||||
|         let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0); |         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) { |         while (curNode?.previousSibling) { | ||||||
|             curNode = curNode.previousSibling; |             curNode = curNode.previousSibling; | ||||||
|  |  | ||||||
|             if ((curNode as Element).name === "reference") { |             if ((curNode as ModelElement).name === "reference") { | ||||||
|                 clickIndex += (curNode.getAttribute("notePath") as string).length + 1; |                 clickIndex += (curNode.getAttribute("href") as string).length + 1; | ||||||
|             } else if ("data" in curNode) { |             } else if ("data" in curNode) { | ||||||
|                 clickIndex += (curNode.data as string).length; |                 clickIndex += (curNode.data as string).length; | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import type { default as Component, TypedComponent } from "../../components/component.js"; | import type { TypedComponent } from "../../components/component.js"; | ||||||
| import BasicWidget, { TypedBasicWidget } from "../basic_widget.js"; | import { TypedBasicWidget } from "../basic_widget.js"; | ||||||
|  |  | ||||||
| export default class Container<T extends TypedComponent<any>> extends TypedBasicWidget<T> { | export default class Container<T extends TypedComponent<any>> extends TypedBasicWidget<T> { | ||||||
|     doRender() { |     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> = { | export const byBookType: Record<ViewTypeOptions, string | null> = { | ||||||
|     list: null, |     list: "mULW0Q3VojwY", | ||||||
|     grid: null, |     grid: "8QqnMzx393bx", | ||||||
|     calendar: "xWbu3jpNWapp", |     calendar: "xWbu3jpNWapp", | ||||||
|     table: "2FvYrpmOXm29", |     table: "2FvYrpmOXm29", | ||||||
|     geoMap: "81SGnPGMk7Xc" |     geoMap: "81SGnPGMk7Xc" | ||||||
|   | |||||||
| @@ -195,7 +195,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | |||||||
|         // https://github.com/zadam/trilium/issues/2522 |         // https://github.com/zadam/trilium/issues/2522 | ||||||
|         const isBackendNote = this.noteContext?.noteId === "_backendLog"; |         const isBackendNote = this.noteContext?.noteId === "_backendLog"; | ||||||
|         const isSqlNote = this.mime === "text/x-sqlite;schema=trilium"; |         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) |         const isFullHeight = (!this.noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote) | ||||||
|             || this.noteContext?.viewScope?.viewMode === "attachments" |             || this.noteContext?.viewScope?.viewMode === "attachments" | ||||||
|             || isBackendNote; |             || isBackendNote; | ||||||
|   | |||||||
| @@ -3,8 +3,6 @@ import NoteListRenderer from "../services/note_list_renderer.js"; | |||||||
| import type FNote from "../entities/fnote.js"; | import type FNote from "../entities/fnote.js"; | ||||||
| import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData, EventNames } from "../components/app_context.js"; | import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData, EventNames } from "../components/app_context.js"; | ||||||
| import type ViewMode from "./view_widgets/view_mode.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*/` | const TPL = /*html*/` | ||||||
| <div class="note-list-widget"> | <div class="note-list-widget"> | ||||||
| @@ -39,24 +37,36 @@ export default class NoteListWidget extends NoteContextAwareWidget { | |||||||
|     private noteIdRefreshed?: string; |     private noteIdRefreshed?: string; | ||||||
|     private shownNoteId?: string | null; |     private shownNoteId?: string | null; | ||||||
|     private viewMode?: ViewMode<any> | 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(); |         super(); | ||||||
|         this.attributeDetailWidget = new AttributeDetailWidget() |  | ||||||
|                 .contentSized() |         this.displayOnlyCollections = displayOnlyCollections; | ||||||
|                 .setParent(this); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     isEnabled() { |     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() { |     doRender() { | ||||||
|         this.$widget = $(TPL); |         this.$widget = $(TPL); | ||||||
|         this.contentSized(); |         this.contentSized(); | ||||||
|         this.$content = this.$widget.find(".note-list-widget-content"); |         this.$content = this.$widget.find(".note-list-widget-content"); | ||||||
|         this.$widget.append(this.attributeDetailWidget.render()); |  | ||||||
|  |  | ||||||
|         const observer = new IntersectionObserver( |         const observer = new IntersectionObserver( | ||||||
|             (entries) => { |             (entries) => { | ||||||
| @@ -75,23 +85,6 @@ export default class NoteListWidget extends NoteContextAwareWidget { | |||||||
|         setTimeout(() => observer.observe(this.$widget[0]), 10); |         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() { |     checkRenderStatus() { | ||||||
|         // console.log("this.isIntersecting", this.isIntersecting); |         // console.log("this.isIntersecting", this.isIntersecting); | ||||||
|         // console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId); |         // console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId); | ||||||
| @@ -107,8 +100,7 @@ export default class NoteListWidget extends NoteContextAwareWidget { | |||||||
|         const noteListRenderer = new NoteListRenderer({ |         const noteListRenderer = new NoteListRenderer({ | ||||||
|             $parent: this.$content, |             $parent: this.$content, | ||||||
|             parentNote: note, |             parentNote: note, | ||||||
|             parentNotePath: this.notePath, |             parentNotePath: this.notePath | ||||||
|             noteIds: note.getChildNoteIds() |  | ||||||
|         }); |         }); | ||||||
|         this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight); |         this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight); | ||||||
|         await noteListRenderer.renderList(); |         await noteListRenderer.renderList(); | ||||||
| @@ -153,12 +145,6 @@ export default class NoteListWidget extends NoteContextAwareWidget { | |||||||
|             this.refresh(); |             this.refresh(); | ||||||
|             this.checkRenderStatus(); |             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">) { |     buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) { | ||||||
|   | |||||||
| @@ -240,24 +240,25 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | |||||||
|         this.$tree.on("mousedown", ".fancytree-title", (e) => { |         this.$tree.on("mousedown", ".fancytree-title", (e) => { | ||||||
|             if (e.which === 2) { |             if (e.which === 2) { | ||||||
|                 const node = $.ui.fancytree.getNode(e as unknown as Event); |                 const node = $.ui.fancytree.getNode(e as unknown as Event); | ||||||
|  |  | ||||||
|                 const notePath = treeService.getNotePath(node); |                 const notePath = treeService.getNotePath(node); | ||||||
|  |  | ||||||
|                 if (notePath) { |                 if (notePath) { | ||||||
|  |                     e.stopPropagation(); | ||||||
|  |                     e.preventDefault(); | ||||||
|  |  | ||||||
|                     appContext.tabManager.openTabWithNoteWithHoisting(notePath, { |                     appContext.tabManager.openTabWithNoteWithHoisting(notePath, { | ||||||
|                         activate: e.shiftKey ? true : false |                         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.stopPropagation(); | ||||||
|                 e.preventDefault(); |                 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.$treeSettingsPopup = this.$widget.find(".tree-settings-popup"); | ||||||
|         this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find(".hide-archived-notes"); |         this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find(".hide-archived-notes"); | ||||||
| @@ -712,7 +713,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | |||||||
|             }); |             }); | ||||||
|         } else { |         } else { | ||||||
|             this.$tree.on("contextmenu", ".fancytree-node", (e) => { |             this.$tree.on("contextmenu", ".fancytree-node", (e) => { | ||||||
|  |                 if (!utils.isCtrlKey(e)) { | ||||||
|                     this.showContextMenu(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 |                 return false; // blocks default browser right click menu | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,10 +23,15 @@ const TPL = /*html*/` | |||||||
|             align-items: center; |             align-items: center; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .book-properties-container > * { |         .book-properties-container > div { | ||||||
|             margin-right: 15px; |             margin-right: 15px; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         .book-properties-container > .type-number > label { | ||||||
|  |             display: flex; | ||||||
|  |             align-items: baseline; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         .book-properties-container input[type="checkbox"] { |         .book-properties-container input[type="checkbox"] { | ||||||
|             margin-right: 5px; |             margin-right: 5px; | ||||||
|         } |         } | ||||||
| @@ -127,6 +132,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget { | |||||||
|  |  | ||||||
|     renderBookProperty(property: BookProperty) { |     renderBookProperty(property: BookProperty) { | ||||||
|         const $container = $("<div>"); |         const $container = $("<div>"); | ||||||
|  |         $container.addClass(`type-${property.type}`); | ||||||
|         const note = this.note; |         const note = this.note; | ||||||
|         if (!note) { |         if (!note) { | ||||||
|             return $container; |             return $container; | ||||||
| @@ -168,6 +174,27 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget { | |||||||
|                 }); |                 }); | ||||||
|                 $container.append($button); |                 $container.append($button); | ||||||
|                 break; |                 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; |         return $container; | ||||||
|   | |||||||
| @@ -4,8 +4,6 @@ import attributes from "../../services/attributes"; | |||||||
| import { ViewTypeOptions } from "../../services/note_list_renderer" | import { ViewTypeOptions } from "../../services/note_list_renderer" | ||||||
| import NoteContextAwareWidget from "../note_context_aware_widget"; | import NoteContextAwareWidget from "../note_context_aware_widget"; | ||||||
|  |  | ||||||
| export type BookProperty = CheckBoxProperty | ButtonProperty; |  | ||||||
|  |  | ||||||
| interface BookConfig { | interface BookConfig { | ||||||
|     properties: BookProperty[]; |     properties: BookProperty[]; | ||||||
| } | } | ||||||
| @@ -24,6 +22,16 @@ interface ButtonProperty { | |||||||
|     onClick: (context: BookContext) => void; |     onClick: (context: BookContext) => void; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | interface NumberProperty { | ||||||
|  |     type: "number", | ||||||
|  |     label: string; | ||||||
|  |     bindToLabel: string; | ||||||
|  |     width?: number; | ||||||
|  |     min?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty; | ||||||
|  |  | ||||||
| interface BookContext { | interface BookContext { | ||||||
|     note: FNote; |     note: FNote; | ||||||
|     triggerCommand: NoteContextAwareWidget["triggerCommand"]; |     triggerCommand: NoteContextAwareWidget["triggerCommand"]; | ||||||
| @@ -85,6 +93,13 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = { | |||||||
|         properties: [] |         properties: [] | ||||||
|     }, |     }, | ||||||
|     table: { |     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(); |         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() { |     async getTitle() { | ||||||
|         return { |         return { | ||||||
|             show: await this.#shouldDisplay(), |             show: await this.#shouldDisplay(), | ||||||
| @@ -58,11 +70,7 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async #shouldDisplay() { |     async #shouldDisplay() { | ||||||
|         if (options.get("textNoteEditorType") !== "ckeditor-classic") { |         if (!this.isEnabled()) { | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!this.note || this.note.type !== "text") { |  | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -69,11 +69,6 @@ interface AttributeResult { | |||||||
|     attributeId: string; |     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 { | export default class PromotedAttributesWidget extends NoteContextAwareWidget { | ||||||
|  |  | ||||||
|     private $container!: JQuery<HTMLElement>; |     private $container!: JQuery<HTMLElement>; | ||||||
|   | |||||||
| @@ -68,7 +68,6 @@ export default class SearchResultWidget extends NoteContextAwareWidget { | |||||||
|         const noteListRenderer = new NoteListRenderer({ |         const noteListRenderer = new NoteListRenderer({ | ||||||
|             $parent: this.$content, |             $parent: this.$content, | ||||||
|             parentNote: note, |             parentNote: note, | ||||||
|             noteIds: note.getChildNoteIds(), |  | ||||||
|             showNotePath: true |             showNotePath: true | ||||||
|         }); |         }); | ||||||
|         await noteListRenderer.renderList(); |         await noteListRenderer.renderList(); | ||||||
|   | |||||||
| @@ -59,7 +59,7 @@ async function handleContentUpdate(affectedNoteIds: string[]) { | |||||||
|     const templateNoteIds = new Set(templateCache.keys()); |     const templateNoteIds = new Set(templateCache.keys()); | ||||||
|     const affectedTemplateNoteIds = templateNoteIds.intersection(updatedNoteIds); |     const affectedTemplateNoteIds = templateNoteIds.intersection(updatedNoteIds); | ||||||
|  |  | ||||||
|     await froca.getNotes(affectedNoteIds); |     await froca.getNotes(affectedNoteIds, true); | ||||||
|  |  | ||||||
|     let fullReloadNeeded = false; |     let fullReloadNeeded = false; | ||||||
|     for (const affectedTemplateNoteId of affectedTemplateNoteIds) { |     for (const affectedTemplateNoteId of affectedTemplateNoteIds) { | ||||||
|   | |||||||
| @@ -178,13 +178,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | |||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             if (isClassicEditor) { |             if (isClassicEditor) { | ||||||
|                 let $classicToolbarWidget; |                 const $classicToolbarWidget = this.findClassicToolbar(); | ||||||
|                 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"); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $classicToolbarWidget.empty(); |                 $classicToolbarWidget.empty(); | ||||||
|                 if ($classicToolbarWidget.length) { |                 if ($classicToolbarWidget.length) { | ||||||
| @@ -271,8 +265,13 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     focus() { |     focus() { | ||||||
|  |         const editor = this.watchdog.editor; | ||||||
|  |         if (editor) { | ||||||
|  |             editor.editing.view.focus(); | ||||||
|  |         } else { | ||||||
|             this.$editor.trigger("focus"); |             this.$editor.trigger("focus"); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     scrollToEnd() { |     scrollToEnd() { | ||||||
|         this.watchdog?.editor?.model.change((writer) => { |         this.watchdog?.editor?.model.change((writer) => { | ||||||
| @@ -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">) { |     buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) { | ||||||
|         const { TouchBar, buildIcon } = data; |         const { TouchBar, buildIcon } = data; | ||||||
|         const { TouchBarSegmentedControl, TouchBarGroup, TouchBarButton } = TouchBar; |         const { TouchBarSegmentedControl, TouchBarGroup, TouchBarButton } = TouchBar; | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ import TypeWidget from "./type_widget.js"; | |||||||
| import appContext from "../../components/app_context.js"; | import appContext from "../../components/app_context.js"; | ||||||
| import searchService from "../../services/search.js"; | import searchService from "../../services/search.js"; | ||||||
| import { t } from "../../services/i18n.js"; | import { t } from "../../services/i18n.js"; | ||||||
| import type FNote from "../../entities/fnote.js"; |  | ||||||
|  |  | ||||||
| const TPL = /*html*/` | const TPL = /*html*/` | ||||||
| <div class="note-detail-empty note-detail-printable"> | <div class="note-detail-empty note-detail-printable"> | ||||||
|   | |||||||
| @@ -22,7 +22,8 @@ const TPL = /*html*/` | |||||||
|             padding: 0; |             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; |             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} |      * {@inheritdoc} | ||||||
|      * |      * | ||||||
|   | |||||||
| @@ -113,7 +113,6 @@ export default class CalendarView extends ViewMode<{}> { | |||||||
|  |  | ||||||
|     private $root: JQuery<HTMLElement>; |     private $root: JQuery<HTMLElement>; | ||||||
|     private $calendarContainer: JQuery<HTMLElement>; |     private $calendarContainer: JQuery<HTMLElement>; | ||||||
|     private noteIds: string[]; |  | ||||||
|     private calendar?: Calendar; |     private calendar?: Calendar; | ||||||
|     private isCalendarRoot: boolean; |     private isCalendarRoot: boolean; | ||||||
|     private lastView?: string; |     private lastView?: string; | ||||||
| @@ -124,15 +123,10 @@ export default class CalendarView extends ViewMode<{}> { | |||||||
|  |  | ||||||
|         this.$root = $(TPL); |         this.$root = $(TPL); | ||||||
|         this.$calendarContainer = this.$root.find(".calendar-container"); |         this.$calendarContainer = this.$root.find(".calendar-container"); | ||||||
|         this.noteIds = args.noteIds; |  | ||||||
|         this.isCalendarRoot = false; |         this.isCalendarRoot = false; | ||||||
|         args.$parent.append(this.$root); |         args.$parent.append(this.$root); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     get isFullHeight(): boolean { |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async renderList(): Promise<JQuery<HTMLElement> | undefined> { |     async renderList(): Promise<JQuery<HTMLElement> | undefined> { | ||||||
|         this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot"); |         this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot"); | ||||||
|         const isEditable = !this.isCalendarRoot; |         const isEditable = !this.isCalendarRoot; | ||||||
| @@ -225,6 +219,7 @@ export default class CalendarView extends ViewMode<{}> { | |||||||
|                     $(mainContainer ?? e.el).append($(promotedAttributesHtml)); |                     $(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) => { |             dateClick: async (e) => { | ||||||
|                 if (!this.isCalendarRoot) { |                 if (!this.isCalendarRoot) { | ||||||
|                     return; |                     return; | ||||||
| @@ -232,7 +227,8 @@ export default class CalendarView extends ViewMode<{}> { | |||||||
|  |  | ||||||
|                 const note = await date_notes.getDayNote(e.dateStr); |                 const note = await date_notes.getDayNote(e.dateStr); | ||||||
|                 if (note) { |                 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), |             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. |         // Refresh note IDs if they got changed. | ||||||
|         if (loadResults.getBranchRows().some((branch) => branch.parentNoteId === this.parentNote.noteId)) { |         if (loadResults.getBranchRows().some((branch) => branch.parentNoteId === this.parentNote.noteId)) { | ||||||
|             this.noteIds = this.parentNote.getChildNoteIds(); |             this.noteIds = this.parentNote.getChildNoteIds(); | ||||||
| @@ -405,9 +401,14 @@ export default class CalendarView extends ViewMode<{}> { | |||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // Refresh on note title change. | ||||||
|  |         if (loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId))) { | ||||||
|  |             this.calendar?.refetchEvents(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         // Refresh dataset on subnote change. |         // Refresh dataset on subnote change. | ||||||
|         if (this.calendar && loadResults.getAttributeRows().some((a) => this.noteIds.includes(a.noteId ?? ""))) { |         if (loadResults.getAttributeRows().some((a) => this.noteIds.includes(a.noteId ?? ""))) { | ||||||
|             this.calendar.refetchEvents(); |             this.calendar?.refetchEvents(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -436,7 +437,7 @@ export default class CalendarView extends ViewMode<{}> { | |||||||
|             events.push(await CalendarView.buildEvent(dateNote, { startDate })); |             events.push(await CalendarView.buildEvent(dateNote, { startDate })); | ||||||
|  |  | ||||||
|             if (dateNote.hasChildren()) { |             if (dateNote.hasChildren()) { | ||||||
|                 const childNoteIds = dateNote.getChildNoteIds(); |                 const childNoteIds = await dateNote.getSubtreeNoteIds(); | ||||||
|                 for (const childNoteId of childNoteIds) { |                 for (const childNoteId of childNoteIds) { | ||||||
|                     childNoteToDateMapping[childNoteId] = startDate; |                     childNoteToDateMapping[childNoteId] = startDate; | ||||||
|                 } |                 } | ||||||
| @@ -462,13 +463,6 @@ export default class CalendarView extends ViewMode<{}> { | |||||||
|         for (const note of notes) { |         for (const note of notes) { | ||||||
|             const startDate = CalendarView.#getCustomisableLabel(note, "startDate", "calendar:startDate"); |             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) { |             if (!startDate) { | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
| @@ -533,7 +527,7 @@ export default class CalendarView extends ViewMode<{}> { | |||||||
|             const eventData: EventInput = { |             const eventData: EventInput = { | ||||||
|                 title: title, |                 title: title, | ||||||
|                 start: startDate, |                 start: startDate, | ||||||
|                 url: `#${note.noteId}`, |                 url: `#${note.noteId}?popup`, | ||||||
|                 noteId: note.noteId, |                 noteId: note.noteId, | ||||||
|                 color: color ?? undefined, |                 color: color ?? undefined, | ||||||
|                 iconClass: note.getLabelValue("iconClass"), |                 iconClass: note.getLabelValue("iconClass"), | ||||||
|   | |||||||
| @@ -29,6 +29,11 @@ const TPL = /*html*/` | |||||||
|             z-index: 1; |             z-index: 1; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         .leaflet-top, | ||||||
|  |         .leaflet-bottom { | ||||||
|  |             z-index: 997; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         .geo-map-container.placing-note { |         .geo-map-container.placing-note { | ||||||
|             cursor: crosshair; |             cursor: crosshair; | ||||||
|         } |         } | ||||||
| @@ -221,7 +226,7 @@ export default class GeoView extends ViewMode<MapData> { | |||||||
|  |  | ||||||
|         // Add the new markers. |         // Add the new markers. | ||||||
|         this.currentMarkerData = {}; |         this.currentMarkerData = {}; | ||||||
|         const notes = await this.parentNote.getChildNotes(); |         const notes = await this.parentNote.getSubtreeNotes(); | ||||||
|         const draggable = !this.isReadOnly; |         const draggable = !this.isReadOnly; | ||||||
|         for (const childNote of notes) { |         for (const childNote of notes) { | ||||||
|             if (childNote.mime === "application/gpx+xml") { |             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) { |     #changeState(newState: State) { | ||||||
|         this._state = newState; |         this._state = newState; | ||||||
|         this.$container.toggleClass("placing-note", newState === State.NewNote); |         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 any of the children branches are altered. | ||||||
|         if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.parentNote.noteId)) { |         if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.parentNote.noteId)) { | ||||||
|             this.#reloadMarkers(); |             this.#reloadMarkers(); | ||||||
|   | |||||||
| @@ -36,10 +36,17 @@ export default function processNoteWithMarker(map: Map, note: FNote, location: s | |||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     newMarker.on("contextmenu", (e) => { |     newMarker.on("contextmenu", (e) => { | ||||||
|         openContextMenu(note.noteId, e, isEditable); |         openContextMenu(note.noteId, e, isEditable); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     if (!isEditable) { | ||||||
|  |         newMarker.on("click", (e) => { | ||||||
|  |             appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return newMarker; |     return newMarker; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -161,7 +161,7 @@ const TPL = /*html*/` | |||||||
| class ListOrGridView extends ViewMode<{}> { | class ListOrGridView extends ViewMode<{}> { | ||||||
|     private $noteList: JQuery<HTMLElement>; |     private $noteList: JQuery<HTMLElement>; | ||||||
|  |  | ||||||
|     private noteIds: string[]; |     private filteredNoteIds!: string[]; | ||||||
|     private page?: number; |     private page?: number; | ||||||
|     private pageSize?: number; |     private pageSize?: number; | ||||||
|     private showNotePath?: boolean; |     private showNotePath?: boolean; | ||||||
| @@ -174,13 +174,6 @@ class ListOrGridView extends ViewMode<{}> { | |||||||
|         super(args, viewType); |         super(args, viewType); | ||||||
|         this.$noteList = $(TPL); |         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); |         args.$parent.append(this.$noteList); | ||||||
|  |  | ||||||
| @@ -204,8 +197,14 @@ class ListOrGridView extends ViewMode<{}> { | |||||||
|         return new Set(includedLinks.map((rel) => rel.value)); |         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() { |     async renderList() { | ||||||
|         if (this.noteIds.length === 0 || !this.page || !this.pageSize) { |         if (this.filteredNoteIds.length === 0 || !this.page || !this.pageSize) { | ||||||
|             this.$noteList.hide(); |             this.$noteList.hide(); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @@ -226,7 +225,7 @@ class ListOrGridView extends ViewMode<{}> { | |||||||
|         const startIdx = (this.page - 1) * this.pageSize; |         const startIdx = (this.page - 1) * this.pageSize; | ||||||
|         const endIdx = startIdx + 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); |         const pageNotes = await froca.getNotes(pageNoteIds); | ||||||
|  |  | ||||||
|         for (const note of pageNotes) { |         for (const note of pageNotes) { | ||||||
| @@ -246,7 +245,7 @@ class ListOrGridView extends ViewMode<{}> { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const pageCount = Math.ceil(this.noteIds.length / this.pageSize); |         const pageCount = Math.ceil(this.filteredNoteIds.length / this.pageSize); | ||||||
|  |  | ||||||
|         $pager.toggle(pageCount > 1); |         $pager.toggle(pageCount > 1); | ||||||
|  |  | ||||||
| @@ -257,7 +256,7 @@ class ListOrGridView extends ViewMode<{}> { | |||||||
|                 lastPrinted = true; |                 lastPrinted = true; | ||||||
|  |  | ||||||
|                 const startIndex = (i - 1) * this.pageSize + 1; |                 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( |                 $pager.append( | ||||||
|                     i === this.page |                     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 |         // 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) { |     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 { RelationEditor } from "./relation_editor.js"; | ||||||
| import { NoteFormatter, NoteTitleFormatter } from "./formatters.js"; | import { MonospaceFormatter, NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js"; | ||||||
| import { applyHeaderMenu } from "./header-menu.js"; |  | ||||||
| import type { ColumnDefinition } from "tabulator-tables"; | import type { ColumnDefinition } from "tabulator-tables"; | ||||||
| import { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; | import { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; | ||||||
|  |  | ||||||
| type ColumnType = LabelType | "relation"; | type ColumnType = LabelType | "relation"; | ||||||
|  |  | ||||||
| export interface PromotedAttributeInformation { | export interface AttributeDefinitionInformation { | ||||||
|     name: string; |     name: string; | ||||||
|     title?: string; |     title?: string; | ||||||
|     type?: ColumnType; |     type?: ColumnType; | ||||||
| @@ -42,19 +41,30 @@ const labelTypeMappings: Record<ColumnType, Partial<ColumnDefinition>> = { | |||||||
|     } |     } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export function buildColumnDefinitions(info: PromotedAttributeInformation[], existingColumnData?: ColumnDefinition[]) { | interface BuildColumnArgs { | ||||||
|     const columnDefs: ColumnDefinition[] = [ |     info: AttributeDefinitionInformation[]; | ||||||
|  |     movableRows: boolean; | ||||||
|  |     existingColumnData: ColumnDefinition[] | undefined; | ||||||
|  |     rowNumberHint: number; | ||||||
|  |     position?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function buildColumnDefinitions({ info, movableRows, existingColumnData, rowNumberHint, position }: BuildColumnArgs) { | ||||||
|  |     let columnDefs: ColumnDefinition[] = [ | ||||||
|         { |         { | ||||||
|             title: "#", |             title: "#", | ||||||
|             formatter: "rownum", |  | ||||||
|             headerSort: false, |             headerSort: false, | ||||||
|             hozAlign: "center", |             hozAlign: "center", | ||||||
|             resizable: false, |             resizable: false, | ||||||
|             frozen: true |             frozen: true, | ||||||
|  |             rowHandle: movableRows, | ||||||
|  |             width: calculateIndexColumnWidth(rowNumberHint, movableRows), | ||||||
|  |             formatter: RowNumberFormatter(movableRows) | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             field: "noteId", |             field: "noteId", | ||||||
|             title: "Note ID", |             title: "Note ID", | ||||||
|  |             formatter: MonospaceFormatter, | ||||||
|             visible: false |             visible: false | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
| @@ -79,32 +89,59 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[], exi | |||||||
|             field, |             field, | ||||||
|             title: title ?? name, |             title: title ?? name, | ||||||
|             editor: "input", |             editor: "input", | ||||||
|  |             rowHandle: false, | ||||||
|             ...labelTypeMappings[type ?? "text"], |             ...labelTypeMappings[type ?? "text"], | ||||||
|         }); |         }); | ||||||
|         seenFields.add(field); |         seenFields.add(field); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     applyHeaderMenu(columnDefs); |  | ||||||
|     if (existingColumnData) { |     if (existingColumnData) { | ||||||
|         restoreExistingData(columnDefs, existingColumnData); |         columnDefs = restoreExistingData(columnDefs, existingColumnData, position); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return columnDefs; |     return columnDefs; | ||||||
| } | } | ||||||
|  |  | ||||||
| function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[]) { | export function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[], position?: number) { | ||||||
|     const byField = new Map<string, ColumnDefinition>; |     // 1. Keep existing columns, but restore their properties like width, visibility and order. | ||||||
|     for (const def of oldDefs) { |     const newItemsByField = new Map<string, ColumnDefinition>( | ||||||
|         byField.set(def.field ?? "", def); |         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[]; | ||||||
|  |  | ||||||
|  |     // 2. Determine new columns. | ||||||
|  |     const existingFields = new Set(existingColumns.map(item => item.field)); | ||||||
|  |     const newColumns = newDefs | ||||||
|  |         .filter(item => !existingFields.has(item.field!)); | ||||||
|  |  | ||||||
|  |     // 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) | ||||||
|  |     ]; | ||||||
| } | } | ||||||
|  |  | ||||||
|     for (const newDef of newDefs) { | function calculateIndexColumnWidth(rowNumberHint: number, movableRows: boolean): number { | ||||||
|         const oldDef = byField.get(newDef.field ?? ""); |     let columnWidth = 16 * (rowNumberHint.toString().length || 1); | ||||||
|         if (!oldDef) { |     if (movableRows) { | ||||||
|             continue; |         columnWidth += 32; | ||||||
|         } |  | ||||||
|  |  | ||||||
|         newDef.width = oldDef.width; |  | ||||||
|         newDef.visible = oldDef.visible; |  | ||||||
|     } |     } | ||||||
|  |     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*/`\ |     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")} |             <span class="bx bx-plus"></span> ${t("table_view.new-row")} | ||||||
|         </button> |         </button> | ||||||
|  |  | ||||||
|         <button class="btn btn-sm" style="padding: 0px 10px 0px 10px;" data-trigger-command="addNoteListItem"> |         <button class="btn btn-sm" data-trigger-command="addNewTableColumn"> | ||||||
|             <span class="bx bx-columns"></span> ${t("table_view.new-column")} |             <span class="bx bx-carousel"></span> ${t("table_view.new-column")} | ||||||
|         </button> |         </button> | ||||||
|     `.trimStart(); |     `.trimStart(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,45 +1,89 @@ | |||||||
| import { CellComponent } from "tabulator-tables"; | 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. |  * Custom formatter to represent a note, with the icon and note title being rendered. | ||||||
|  * |  * | ||||||
|  * The value of the cell must be the note ID. |  * 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(); |     let noteId = cell.getValue(); | ||||||
|     if (!noteId) { |     if (!noteId) { | ||||||
|         return ""; |         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 () => { |         onRendered(async () => { | ||||||
|         const { $noteRef, href } = buildNoteLink(noteId); |             const note = await froca.getNote(noteId); | ||||||
|         await loadReferenceLinkTitle($noteRef, href); |             if (!note) { | ||||||
|         cell.getElement().appendChild($noteRef[0]); |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const el = buildLink(note); | ||||||
|  |             if (el) { | ||||||
|  |                 cell.getElement().appendChild(el); | ||||||
|  |             } | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         return ""; |         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. |  * 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) { | export function NoteTitleFormatter(cell: CellComponent) { | ||||||
|     const { noteId, iconClass } = cell.getRow().getData(); |     const { noteId, iconClass, colorClass } = cell.getRow().getData(); | ||||||
|     if (!noteId) { |     if (!noteId) { | ||||||
|         return ""; |         return ""; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const { $noteRef } = buildNoteLink(noteId); |     const { $noteRef } = buildNoteLink(noteId, cell.getValue(), iconClass, colorClass); | ||||||
|     $noteRef.text(cell.getValue()); |  | ||||||
|     $noteRef.prepend($("<span>").addClass(iconClass)); |  | ||||||
|  |  | ||||||
|     return $noteRef[0].outerHTML; |     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 $noteRef = $("<span>"); | ||||||
|     const href = `#root/${noteId}`; |     const href = `#root/${noteId}`; | ||||||
|     $noteRef.addClass("reference-link"); |     $noteRef.addClass("reference-link"); | ||||||
|     $noteRef.attr("data-href", href); |     $noteRef.attr("data-href", href); | ||||||
|  |     $noteRef.text(title); | ||||||
|  |     $noteRef.prepend($("<span>").addClass(iconClass)); | ||||||
|  |     if (colorClass) { | ||||||
|  |         $noteRef.addClass(colorClass); | ||||||
|  |     } | ||||||
|     return { $noteRef, href }; |     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 ViewMode, { type ViewModeArgs } from "../view_mode.js"; | ||||||
| import attributes, { setAttribute, setLabel } from "../../../services/attributes.js"; | import attributes from "../../../services/attributes.js"; | ||||||
| import server from "../../../services/server.js"; |  | ||||||
| import SpacedUpdate from "../../../services/spaced_update.js"; | import SpacedUpdate from "../../../services/spaced_update.js"; | ||||||
| import type { CommandListenerData, EventData } from "../../../components/app_context.js"; | import type { EventData } from "../../../components/app_context.js"; | ||||||
| import type { Attribute } from "../../../services/attribute_parser.js"; | import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent, ColumnComponent} from 'tabulator-tables'; | ||||||
| import note_create from "../../../services/note_create.js"; | import "tabulator-tables/dist/css/tabulator.css"; | ||||||
| import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MenuModule, MoveRowsModule, ColumnDefinition} from 'tabulator-tables'; | import "../../../../src/stylesheets/table.css"; | ||||||
| import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; |  | ||||||
| import { canReorderRows, configureReorderingRows } from "./dragging.js"; | import { canReorderRows, configureReorderingRows } from "./dragging.js"; | ||||||
| import buildFooter from "./footer.js"; | import buildFooter from "./footer.js"; | ||||||
| import getPromotedAttributeInformation, { buildRowDefinitions } from "./rows.js"; | import getAttributeDefinitionInformation, { buildRowDefinitions } from "./rows.js"; | ||||||
| import { buildColumnDefinitions } from "./columns.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*/` | const TPL = /*html*/` | ||||||
| <div class="table-view"> | <div class="table-view"> | ||||||
| @@ -63,6 +63,26 @@ const TPL = /*html*/` | |||||||
|         justify-content: left; |         justify-content: left; | ||||||
|         gap: 0.5em; |         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> |     </style> | ||||||
|  |  | ||||||
|     <div class="table-view-container"></div> |     <div class="table-view-container"></div> | ||||||
| @@ -79,29 +99,24 @@ export default class TableView extends ViewMode<StateInfo> { | |||||||
|  |  | ||||||
|     private $root: JQuery<HTMLElement>; |     private $root: JQuery<HTMLElement>; | ||||||
|     private $container: JQuery<HTMLElement>; |     private $container: JQuery<HTMLElement>; | ||||||
|     private args: ViewModeArgs; |  | ||||||
|     private spacedUpdate: SpacedUpdate; |     private spacedUpdate: SpacedUpdate; | ||||||
|     private api?: Tabulator; |     private api?: Tabulator; | ||||||
|     private newAttribute?: Attribute; |  | ||||||
|     private persistentData: StateInfo["tableData"]; |     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 colEditing?: TableColumnEditing; | ||||||
|     private noteIdToEdit?: string; |     private rowEditing?: TableRowEditing; | ||||||
|  |     private maxDepth: number = -1; | ||||||
|  |     private rowNumberHint: number = 1; | ||||||
|  |  | ||||||
|     constructor(args: ViewModeArgs) { |     constructor(args: ViewModeArgs) { | ||||||
|         super(args, "table"); |         super(args, "table"); | ||||||
|  |  | ||||||
|         this.$root = $(TPL); |         this.$root = $(TPL); | ||||||
|         this.$container = this.$root.find(".table-view-container"); |         this.$container = this.$root.find(".table-view-container"); | ||||||
|         this.args = args; |  | ||||||
|         this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000); |         this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000); | ||||||
|         this.persistentData = {}; |         this.persistentData = {}; | ||||||
|         args.$parent.append(this.$root); |         args.$parent.append(this.$root); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     get isFullHeight(): boolean { |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async renderList() { |     async renderList() { | ||||||
|         this.$container.empty(); |         this.$container.empty(); | ||||||
|         this.renderTable(this.$container[0]); |         this.renderTable(this.$container[0]); | ||||||
| @@ -109,29 +124,34 @@ export default class TableView extends ViewMode<StateInfo> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async renderTable(el: HTMLElement) { |     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) { |         for (const module of modules) { | ||||||
|             Tabulator.registerModule(module); |             Tabulator.registerModule(module); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         this.initialize(el); |         this.initialize(el, info); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async initialize(el: HTMLElement) { |     private async initialize(el: HTMLElement, info: AttributeDefinitionInformation[]) { | ||||||
|         const notes = await froca.getNotes(this.args.noteIds); |  | ||||||
|         const info = getPromotedAttributeInformation(this.parentNote); |  | ||||||
|  |  | ||||||
|         const viewStorage = await this.viewStorage.restore(); |         const viewStorage = await this.viewStorage.restore(); | ||||||
|         this.persistentData = viewStorage?.tableData || {}; |         this.persistentData = viewStorage?.tableData || {}; | ||||||
|  |  | ||||||
|         const columnDefs = buildColumnDefinitions(info); |         this.maxDepth = parseInt(this.parentNote.getLabelValue("maxNestingDepth") ?? "-1", 10); | ||||||
|         const movableRows = canReorderRows(this.parentNote); |         const { definitions: rowData, hasSubtree: hasChildren, rowNumber } = await buildRowDefinitions(this.parentNote, info, this.maxDepth); | ||||||
|  |         this.rowNumberHint = rowNumber; | ||||||
|         this.api = new Tabulator(el, { |         const movableRows = canReorderRows(this.parentNote) && !hasChildren; | ||||||
|  |         const columnDefs = buildColumnDefinitions({ | ||||||
|  |             info, | ||||||
|  |             movableRows, | ||||||
|  |             existingColumnData: this.persistentData.columns, | ||||||
|  |             rowNumberHint: this.rowNumberHint | ||||||
|  |         }); | ||||||
|  |         let opts: Options = { | ||||||
|             layout: "fitDataFill", |             layout: "fitDataFill", | ||||||
|             index: "noteId", |             index: "branchId", | ||||||
|             columns: columnDefs, |             columns: columnDefs, | ||||||
|             data: await buildRowDefinitions(this.parentNote, notes, info), |             data: rowData, | ||||||
|             persistence: true, |             persistence: true, | ||||||
|             movableColumns: true, |             movableColumns: true, | ||||||
|             movableRows, |             movableRows, | ||||||
| @@ -141,9 +161,30 @@ export default class TableView extends ViewMode<StateInfo> { | |||||||
|                 this.spacedUpdate.scheduleUpdate(); |                 this.spacedUpdate.scheduleUpdate(); | ||||||
|             }, |             }, | ||||||
|             persistenceReaderFunc: (_id, type: string) => this.persistentData?.[type], |             persistenceReaderFunc: (_id, type: string) => this.persistentData?.[type], | ||||||
|         }); |         }; | ||||||
|  |  | ||||||
|  |         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); |             configureReorderingRows(this.api); | ||||||
|         this.setupEditing(); |         } | ||||||
|  |         setupContextMenu(this.api, this.parentNote); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private onSave() { |     private onSave() { | ||||||
| @@ -152,82 +193,35 @@ export default class TableView extends ViewMode<StateInfo> { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private setupEditing() { |     async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { | ||||||
|         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 { |  | ||||||
|         if (!this.api) { |         if (!this.api) { | ||||||
|             return; |             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. |         // Refresh if promoted attributes get changed. | ||||||
|         if (loadResults.getAttributeRows().find(attr => |         if (loadResults.getAttributeRows().find(attr => | ||||||
|             attr.type === "label" && |             attr.type === "label" && | ||||||
|             (attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) && |             (attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) && | ||||||
|             attributes.isAffecting(attr, this.parentNote))) { |             attributes.isAffecting(attr, this.parentNote))) { | ||||||
|             this.#manageColumnUpdate(); |             this.#manageColumnUpdate(); | ||||||
|  |             return await this.#manageRowsUpdate(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId)) { |         // Refresh max depth | ||||||
|             this.#manageRowsUpdate(); |         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!))) { |         if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? "")) | ||||||
|             this.#manageRowsUpdate(); |             || loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId)) | ||||||
|  |             || loadResults.getAttributeRows().some(attr => this.noteIds.includes(attr.noteId!))) { | ||||||
|  |             return await this.#manageRowsUpdate(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return false; |         return false; | ||||||
| @@ -238,27 +232,40 @@ export default class TableView extends ViewMode<StateInfo> { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const info = getPromotedAttributeInformation(this.parentNote); |         const info = getAttributeDefinitionInformation(this.parentNote); | ||||||
|         const columnDefs = buildColumnDefinitions(info, this.persistentData?.columns); |         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.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() { |     async #manageRowsUpdate() { | ||||||
|         if (!this.api) { |         if (!this.api) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const notes = await froca.getNotes(this.args.noteIds); |         const info = getAttributeDefinitionInformation(this.parentNote); | ||||||
|         const info = getPromotedAttributeInformation(this.parentNote); |         const { definitions, hasSubtree, rowNumber } = await buildRowDefinitions(this.parentNote, info, this.maxDepth); | ||||||
|         this.api.replaceData(await buildRowDefinitions(this.parentNote, notes, info)); |         this.rowNumberHint = rowNumber; | ||||||
|  |  | ||||||
|         if (this.noteIdToEdit) { |         // Force a refresh if the data tree needs enabling/disabling. | ||||||
|             const row = this.api?.getRows().find(r => r.getData().noteId === this.noteIdToEdit); |         if (this.api.options.dataTree !== hasSubtree) { | ||||||
|             if (row) { |             return true; | ||||||
|                 row.getCell("title").edit(); |  | ||||||
|             } |  | ||||||
|             this.noteIdToEdit = undefined; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         await this.api.replaceData(definitions); | ||||||
|  |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,24 +21,29 @@ export function RelationEditor(cell: CellComponent, onRendered, success, cancel, | |||||||
|     editor.style.boxSizing = "border-box"; |     editor.style.boxSizing = "border-box"; | ||||||
|  |  | ||||||
|     //Set value of editor to the current value of the cell |     //Set value of editor to the current value of the cell | ||||||
|     const noteId = cell.getValue(); |     const originalNoteId = cell.getValue(); | ||||||
|     if (noteId) { |     if (originalNoteId) { | ||||||
|         const note = froca.getNoteFromCache(noteId); |         const note = froca.getNoteFromCache(originalNoteId); | ||||||
|         editor.value = note.title; |         editor.value = note.title; | ||||||
|  |     } else { | ||||||
|  |         editor.value = ""; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     //set focus on the select box when the editor is selected |     //set focus on the select box when the editor is selected | ||||||
|     onRendered(function(){ |     onRendered(function(){ | ||||||
|  |         let newNoteId = originalNoteId; | ||||||
|  |  | ||||||
|         note_autocomplete.initNoteAutocomplete($editor, { |         note_autocomplete.initNoteAutocomplete($editor, { | ||||||
|             allowCreatingNotes: true |             allowCreatingNotes: true, | ||||||
|  |             hideAllButtons: true | ||||||
|         }).on("autocomplete:noteselected", (event, suggestion, dataset) => { |         }).on("autocomplete:noteselected", (event, suggestion, dataset) => { | ||||||
|             const notePath = suggestion.notePath; |             const notePath = suggestion.notePath; | ||||||
|             if (!notePath) { |             newNoteId = (notePath ?? "").split("/").at(-1); | ||||||
|                 return; |         }).on("blur", () => { | ||||||
|  |             if (!editor.value) { | ||||||
|  |                 newNoteId = ""; | ||||||
|             } |             } | ||||||
|  |             success(newNoteId); | ||||||
|             const noteId = notePath.split("/").at(-1); |  | ||||||
|             success(noteId); |  | ||||||
|         }); |         }); | ||||||
|         editor.focus(); |         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 FNote from "../../../entities/fnote.js"; | ||||||
| import type { LabelType } from "../../../services/promoted_attribute_definition_parser.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 = { | export type TableData = { | ||||||
|     iconClass: string; |     iconClass: string; | ||||||
| @@ -9,11 +9,17 @@ export type TableData = { | |||||||
|     labels: Record<string, boolean | string | null>; |     labels: Record<string, boolean | string | null>; | ||||||
|     relations: Record<string, boolean | string | null>; |     relations: Record<string, boolean | string | null>; | ||||||
|     branchId: string; |     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[] = []; |     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(); |         const note = await branch.getNote(); | ||||||
|         if (!note) { |         if (!note) { | ||||||
|             continue; // Skip if the note is not found |             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) { |         for (const { name, type } of infos) { | ||||||
|             if (type === "relation") { |             if (type === "relation") { | ||||||
|                 relations[name] = note.getRelationValue(name); |                 relations[name] = note.getRelationValue(name); | ||||||
|             } else if (type === "boolean") { |  | ||||||
|                 labels[name] = note.hasLabel(name); |  | ||||||
|             } else { |             } else { | ||||||
|                 labels[name] = note.getLabelValue(name); |                 labels[name] = note.getLabelValue(name); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         definitions.push({ |  | ||||||
|  |         const def: TableData = { | ||||||
|             iconClass: note.getIcon(), |             iconClass: note.getIcon(), | ||||||
|             noteId: note.noteId, |             noteId: note.noteId, | ||||||
|             title: note.title, |             title: note.title, | ||||||
|             labels, |             labels, | ||||||
|             relations, |             relations, | ||||||
|             branchId: branch.branchId |             branchId: branch.branchId, | ||||||
|         }); |             colorClass: note.getColorClass() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     return definitions; |         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; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| export default function getPromotedAttributeInformation(parentNote: FNote) { |         definitions.push(def); | ||||||
|     const info: PromotedAttributeInformation[] = []; |     } | ||||||
|     for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) { |  | ||||||
|         const def = promotedAttribute.getDefinition(); |     return { | ||||||
|  |         definitions, | ||||||
|  |         hasSubtree, | ||||||
|  |         rowNumber | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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") { |         if (def.multiplicity !== "single") { | ||||||
|             console.warn("Multiple values are not supported for now"); |             console.warn("Multiple values are not supported for now"); | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const [ labelType, name ] = promotedAttribute.name.split(":", 2); |         const [ labelType, name ] = attrDef.name.split(":", 2); | ||||||
|         if (promotedAttribute.type !== "label") { |         if (attrDef.type !== "label") { | ||||||
|             console.warn("Relations are not supported for now"); |             console.warn("Relations are not supported for now"); | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
| @@ -69,6 +90,5 @@ export default function getPromotedAttributeInformation(parentNote: FNote) { | |||||||
|             type |             type | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     console.log("Promoted attribute information", info); |  | ||||||
|     return info; |     return info; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import type { EventData } from "../../components/app_context.js"; | import type { EventData } from "../../components/app_context.js"; | ||||||
|  | import appContext from "../../components/app_context.js"; | ||||||
| import Component from "../../components/component.js"; | import Component from "../../components/component.js"; | ||||||
| import type FNote from "../../entities/fnote.js"; | import type FNote from "../../entities/fnote.js"; | ||||||
| import type { ViewTypeOptions } from "../../services/note_list_renderer.js"; | import type { ViewTypeOptions } from "../../services/note_list_renderer.js"; | ||||||
| @@ -8,7 +9,6 @@ export interface ViewModeArgs { | |||||||
|     $parent: JQuery<HTMLElement>; |     $parent: JQuery<HTMLElement>; | ||||||
|     parentNote: FNote; |     parentNote: FNote; | ||||||
|     parentNotePath?: string | null; |     parentNotePath?: string | null; | ||||||
|     noteIds: string[]; |  | ||||||
|     showNotePath?: boolean; |     showNotePath?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -17,6 +17,8 @@ export default abstract class ViewMode<T extends object> extends Component { | |||||||
|     private _viewStorage: ViewModeStorage<T> | null; |     private _viewStorage: ViewModeStorage<T> | null; | ||||||
|     protected parentNote: FNote; |     protected parentNote: FNote; | ||||||
|     protected viewType: ViewTypeOptions; |     protected viewType: ViewTypeOptions; | ||||||
|  |     protected noteIds: string[]; | ||||||
|  |     protected args: ViewModeArgs; | ||||||
|  |  | ||||||
|     constructor(args: ViewModeArgs, viewType: ViewTypeOptions) { |     constructor(args: ViewModeArgs, viewType: ViewTypeOptions) { | ||||||
|         super(); |         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 |         // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work | ||||||
|         args.$parent.empty(); |         args.$parent.empty(); | ||||||
|         this.viewType = viewType; |         this.viewType = viewType; | ||||||
|  |         this.args = args; | ||||||
|  |         this.noteIds = []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async beforeRender() { | ||||||
|  |         await this.#refreshNoteIds(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     abstract renderList(): Promise<JQuery<HTMLElement> | undefined>; |     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. |      * @param e the event data. | ||||||
|      * @return {@code true} if the view should be re-rendered, a falsy value otherwise. |      * @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. |         // Do nothing by default. | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     get isFullHeight() { |     async entitiesReloadedEvent(e: EventData<"entitiesReloaded">) { | ||||||
|         // Override to change its value. |         if (e.loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? ""))) { | ||||||
|         return false; |             this.#refreshNoteIds(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (await this.onEntitiesReloaded(e)) { | ||||||
|  |             appContext.triggerEvent("refreshNoteList", { noteId: this.parentNote.noteId }); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     get isReadOnly() { |     get isReadOnly() { | ||||||
| @@ -57,4 +70,14 @@ export default abstract class ViewMode<T extends object> extends Component { | |||||||
|         return this._viewStorage; |         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": { |   "devDependencies": { | ||||||
|     "dotenv": "17.1.0", |     "dotenv": "17.2.0", | ||||||
|     "electron": "37.2.0" |     "electron": "37.2.3" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "@triliumnext/desktop", |   "name": "@triliumnext/desktop", | ||||||
|   "version": "0.96.0", |   "version": "0.97.0", | ||||||
|   "description": "Build your personal knowledge base with Trilium Notes", |   "description": "Build your personal knowledge base with Trilium Notes", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "main": "main.cjs", |   "main": "main.cjs", | ||||||
| @@ -17,7 +17,7 @@ | |||||||
|     "@types/electron-squirrel-startup": "1.0.2", |     "@types/electron-squirrel-startup": "1.0.2", | ||||||
|     "@triliumnext/server": "workspace:*", |     "@triliumnext/server": "workspace:*", | ||||||
|     "copy-webpack-plugin": "13.0.0", |     "copy-webpack-plugin": "13.0.0", | ||||||
|     "electron": "37.2.0", |     "electron": "37.2.3", | ||||||
|     "@electron-forge/cli": "7.8.1", |     "@electron-forge/cli": "7.8.1", | ||||||
|     "@electron-forge/maker-deb": "7.8.1", |     "@electron-forge/maker-deb": "7.8.1", | ||||||
|     "@electron-forge/maker-dmg": "7.8.1", |     "@electron-forge/maker-dmg": "7.8.1", | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ | |||||||
|     "@triliumnext/desktop": "workspace:*", |     "@triliumnext/desktop": "workspace:*", | ||||||
|     "@types/fs-extra": "11.0.4", |     "@types/fs-extra": "11.0.4", | ||||||
|     "copy-webpack-plugin": "13.0.0", |     "copy-webpack-plugin": "13.0.0", | ||||||
|     "electron": "37.2.0", |     "electron": "37.2.3", | ||||||
|     "fs-extra": "11.3.0" |     "fs-extra": "11.3.0" | ||||||
|   }, |   }, | ||||||
|   "nx": { |   "nx": { | ||||||
|   | |||||||
| @@ -17,6 +17,6 @@ | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "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 | RUN corepack enable | ||||||
|  |  | ||||||
| # Install native dependencies since we might be building cross-platform. | # 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 | # We have to use --no-frozen-lockfile due to CKEditor patches | ||||||
| RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild | 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 | # Install only runtime dependencies | ||||||
| RUN apt-get update && \ | RUN apt-get update && \ | ||||||
|     apt-get install -y --no-install-recommends \ |     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 | RUN corepack enable | ||||||
|  |  | ||||||
| # Install native dependencies since we might be building cross-platform. | # 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 | # We have to use --no-frozen-lockfile due to CKEditor patches | ||||||
| RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild | RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild | ||||||
|  |  | ||||||
| FROM node:22.17.0-alpine | FROM node:22.17.1-alpine | ||||||
| # Install runtime dependencies | # Install runtime dependencies | ||||||
| RUN apk add --no-cache su-exec shadow | 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 | RUN corepack enable | ||||||
|  |  | ||||||
| # Install native dependencies since we might be building cross-platform. | # 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 | # We have to use --no-frozen-lockfile due to CKEditor patches | ||||||
| RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild | 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 | # Create a non-root user with configurable UID/GID | ||||||
| ARG USER=trilium | ARG USER=trilium | ||||||
| ARG UID=1001 | 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 | RUN corepack enable | ||||||
|  |  | ||||||
| # Install native dependencies since we might be building cross-platform. | # 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 | # We have to use --no-frozen-lockfile due to CKEditor patches | ||||||
| RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild | 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 | # Create a non-root user with configurable UID/GID | ||||||
| ARG USER=trilium | ARG USER=trilium | ||||||
| ARG UID=1001 | ARG UID=1001 | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "@triliumnext/server", |   "name": "@triliumnext/server", | ||||||
|   "version": "0.96.0", |   "version": "0.97.0", | ||||||
|   "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.", |   "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, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
| @@ -52,21 +52,21 @@ | |||||||
|     "cheerio": "1.1.0", |     "cheerio": "1.1.0", | ||||||
|     "chokidar": "4.0.3", |     "chokidar": "4.0.3", | ||||||
|     "cls-hooked": "4.2.2", |     "cls-hooked": "4.2.2", | ||||||
|     "compression": "1.8.0", |     "compression": "1.8.1", | ||||||
|     "cookie-parser": "1.4.7", |     "cookie-parser": "1.4.7", | ||||||
|     "csrf-csrf": "3.2.2", |     "csrf-csrf": "3.2.2", | ||||||
|     "dayjs": "1.11.13", |     "dayjs": "1.11.13", | ||||||
|     "debounce": "2.2.0", |     "debounce": "2.2.0", | ||||||
|     "debug": "4.4.1", |     "debug": "4.4.1", | ||||||
|     "ejs": "3.1.10", |     "ejs": "3.1.10", | ||||||
|     "electron": "37.2.0", |     "electron": "37.2.3", | ||||||
|     "electron-debug": "4.1.0", |     "electron-debug": "4.1.0", | ||||||
|     "electron-window-state": "5.0.3", |     "electron-window-state": "5.0.3", | ||||||
|     "escape-html": "1.0.3", |     "escape-html": "1.0.3", | ||||||
|     "express": "5.1.0", |     "express": "5.1.0", | ||||||
|     "express-openid-connect": "^2.17.1", |     "express-openid-connect": "^2.17.1", | ||||||
|     "express-rate-limit": "7.5.1", |     "express-rate-limit": "8.0.1", | ||||||
|     "express-session": "1.18.1", |     "express-session": "1.18.2", | ||||||
|     "file-uri-to-path": "2.0.0", |     "file-uri-to-path": "2.0.0", | ||||||
|     "fs-extra": "11.3.0", |     "fs-extra": "11.3.0", | ||||||
|     "helmet": "8.1.0", |     "helmet": "8.1.0", | ||||||
| @@ -83,12 +83,12 @@ | |||||||
|     "jimp": "1.6.0", |     "jimp": "1.6.0", | ||||||
|     "js-yaml": "4.1.0", |     "js-yaml": "4.1.0", | ||||||
|     "jsdom": "26.1.0", |     "jsdom": "26.1.0", | ||||||
|     "marked": "16.0.0", |     "marked": "16.1.1", | ||||||
|     "mime-types": "3.0.1", |     "mime-types": "3.0.1", | ||||||
|     "multer": "2.0.1", |     "multer": "2.0.2", | ||||||
|     "normalize-strings": "1.1.1", |     "normalize-strings": "1.1.1", | ||||||
|     "ollama": "0.5.16", |     "ollama": "0.5.16", | ||||||
|     "openai": "5.8.3", |     "openai": "5.10.1", | ||||||
|     "rand-token": "1.0.1", |     "rand-token": "1.0.1", | ||||||
|     "safe-compare": "1.1.4", |     "safe-compare": "1.1.4", | ||||||
|     "sanitize-filename": "1.6.3", |     "sanitize-filename": "1.6.3", | ||||||
|   | |||||||
							
								
								
									
										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><a class="reference-link" href="#root/_help_m523cpzocqaD">Saved Search</a> | ||||||
|           </th> |           </th> | ||||||
|           <td>Not supported.</td> |           <td>Not supported.</td> | ||||||
|  |           <td> </td> | ||||||
|         </tr> |         </tr> | ||||||
|         <tr> |         <tr> | ||||||
|           <th><a class="reference-link" href="#root/_help_iRwzGnHPzonm">Relation Map</a> |           <th><a class="reference-link" href="#root/_help_iRwzGnHPzonm">Relation Map</a> | ||||||
|           </th> |           </th> | ||||||
|           <td>Not supported.</td> |           <td>Not supported.</td> | ||||||
|  |           <td> </td> | ||||||
|         </tr> |         </tr> | ||||||
|         <tr> |         <tr> | ||||||
|           <th><a class="reference-link" href="#root/_help_bdUJEHsAPYQR">Note Map</a> |           <th><a class="reference-link" href="#root/_help_bdUJEHsAPYQR">Note Map</a> | ||||||
|           </th> |           </th> | ||||||
|           <td>Not supported.</td> |           <td>Not supported.</td> | ||||||
|  |           <td> </td> | ||||||
|         </tr> |         </tr> | ||||||
|         <tr> |         <tr> | ||||||
|           <th><a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a> |           <th><a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a> | ||||||
|           </th> |           </th> | ||||||
|           <td>Not supported.</td> |           <td>Not supported.</td> | ||||||
|  |           <td> </td> | ||||||
|         </tr> |         </tr> | ||||||
|         <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> |           </th> | ||||||
|           <td> |           <td> | ||||||
|             <ul> |             <ul> | ||||||
| @@ -132,6 +136,7 @@ class="image"> | |||||||
|           <th><a class="reference-link" href="#root/_help_1vHRoWCEjj0L">Web View</a> |           <th><a class="reference-link" href="#root/_help_1vHRoWCEjj0L">Web View</a> | ||||||
|           </th> |           </th> | ||||||
|           <td>Not supported.</td> |           <td>Not supported.</td> | ||||||
|  |           <td> </td> | ||||||
|         </tr> |         </tr> | ||||||
|         <tr> |         <tr> | ||||||
|           <th><a class="reference-link" href="#root/_help_gBbsAeiuUxI5">Mind Map</a> |           <th><a class="reference-link" href="#root/_help_gBbsAeiuUxI5">Mind Map</a> | ||||||
| @@ -144,9 +149,10 @@ class="image"> | |||||||
|           </td> |           </td> | ||||||
|         </tr> |         </tr> | ||||||
|         <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> |           </th> | ||||||
|           <td>Not supported.</td> |           <td>Not supported.</td> | ||||||
|  |           <td> </td> | ||||||
|         </tr> |         </tr> | ||||||
|         <tr> |         <tr> | ||||||
|           <th><a class="reference-link" href="#root/_help_W8vYD3Q1zjCR">File</a> |           <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 | <p>When a note has one or more child notes, they will be listed at the end | ||||||
|   of the note for easy navigation.</p> |   of the note for easy navigation.</p> | ||||||
| <h2>Configuration</h2> | <h2>Configuration</h2> | ||||||
| @@ -11,47 +15,11 @@ | |||||||
|     the desired number.</li> |     the desired number.</li> | ||||||
| </ul> | </ul> | ||||||
| <h2>View types</h2> | <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 | <p>By default, the notes will be displayed in a grid, however there are also | ||||||
|   some other view types available.</p> |   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" | <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_GTwFsgaA0lCt">Collections</a> note from the  | ||||||
|     href="#root/_help_BlN9DFI679QC">Ribbon</a>, but it can also be changed |   <a | ||||||
|     manually on any type of note using the <code>#viewType</code> attribute.</p> |   class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>, but it can also be changed manually on any type of note using | ||||||
| </aside> |     the <code>#viewType</code> attribute.</p> | ||||||
| <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> |  | ||||||
| @@ -2,8 +2,8 @@ | |||||||
|   <img style="aspect-ratio:767/606;" src="4_Calendar View_image.png" width="767" |   <img style="aspect-ratio:767/606;" src="4_Calendar View_image.png" width="767" | ||||||
|   height="606"> |   height="606"> | ||||||
| </figure> | </figure> | ||||||
| <p>The Calendar view of Book notes will display each child note in a calendar | <p>The Calendar view will display each child note in a calendar that has | ||||||
|   that has a start date and optionally an end date, as an event.</p> |   a start date and optionally an end date, as an event.</p> | ||||||
| <p>The Calendar view has multiple display modes:</p> | <p>The Calendar view has multiple display modes:</p> | ||||||
| <ul> | <ul> | ||||||
|   <li>Week view, where all the 7 days of the week (or 5 if the weekends are |   <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>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> |   <li>List view, which displays all the events of a given month in sequence.</li> | ||||||
| </ul> | </ul> | ||||||
| <p>Unlike other Book view types, the Calendar view also allows some kind | <p>Unlike other Collection view types, the Calendar view also allows some | ||||||
|   of interaction, such as moving events around as well as creating new ones.</p> |   kind of interaction, such as moving events around as well as creating new | ||||||
|  |   ones.</p> | ||||||
| <h2>Creating a calendar</h2> | <h2>Creating a calendar</h2> | ||||||
| <figure class="table"> | <figure class="table"> | ||||||
|   <table> |   <table> | ||||||
| @@ -32,17 +33,17 @@ | |||||||
|         <td> |         <td> | ||||||
|           <img src="2_Calendar View_image.png"> |           <img src="2_Calendar View_image.png"> | ||||||
|         </td> |         </td> | ||||||
|         <td>The Calendar View works only for Book note types. To create a new note, |         <td>The Calendar View works only for Collection note types. To create a new | ||||||
|           right click on the note tree on the left and select Insert note after, |           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> |           or Insert child note and then select <em>Collection</em>.</td> | ||||||
|       </tr> |       </tr> | ||||||
|       <tr> |       <tr> | ||||||
|         <td>2</td> |         <td>2</td> | ||||||
|         <td> |         <td> | ||||||
|           <img src="3_Calendar View_image.png"> |           <img src="3_Calendar View_image.png"> | ||||||
|         </td> |         </td> | ||||||
|         <td>Once created, the “View type” of the Book needs changed to “Calendar”, |         <td>Once created, the “View type” of the Collection needs changed to “Calendar”, | ||||||
|           by selecting the “Book Properties” tab in the ribbon.</td> |           by selecting the “Collection Properties” tab in the ribbon.</td> | ||||||
|       </tr> |       </tr> | ||||||
|     </tbody> |     </tbody> | ||||||
|   </table> |   </table> | ||||||
| @@ -63,7 +64,7 @@ | |||||||
|     <img src="Calendar View_image.png"> |     <img src="Calendar View_image.png"> | ||||||
|   </li> |   </li> | ||||||
|   <li>Creating new notes from the calendar will respect the <code>~child:template</code> relation |   <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> | </ul> | ||||||
| <h2>Interacting with events</h2> | <h2>Interacting with events</h2> | ||||||
| <ul> | <ul> | ||||||
| @@ -71,16 +72,30 @@ | |||||||
|     <br> |     <br> | ||||||
|     <img src="7_Calendar View_image.png"> |     <img src="7_Calendar View_image.png"> | ||||||
|   </li> |   </li> | ||||||
|   <li>Left clicking the event will go to that note. Middle clicking will open |   <li>Left clicking the event will open a <a class="reference-link" href="#root/_help_ZjLYv08Rp3qC">Quick edit</a> to | ||||||
|     the note in a new tab and right click will offer more options including |     edit the note in a popup while allowing easy return to the calendar by | ||||||
|     opening the note in a new split or window.</li> |     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>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 |   <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> |     edge of the event and dragging the mouse around.</li> | ||||||
| </ul> | </ul> | ||||||
| <h2>Configuring the calendar</h2> | <h2>Configuring the calendar view</h2> | ||||||
| <p>The following attributes can be added to the book type:</p> | <p>In the <em>Collections</em> tab in the <a class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>, | ||||||
| <figure class="table"> |   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> |   <table> | ||||||
|     <thead> |     <thead> | ||||||
|       <tr> |       <tr> | ||||||
| @@ -129,7 +144,7 @@ | |||||||
|   </figure> |   </figure> | ||||||
|   <p>In addition, the first day of the week can be either Sunday or Monday |   <p>In addition, the first day of the week can be either Sunday or Monday | ||||||
|     and can be adjusted from the application settings.</p> |     and can be adjusted from the application settings.</p> | ||||||
| <h2>Configuring the calendar events</h2> |   <h2>Configuring the calendar events using attributes</h2> | ||||||
|   <p>For each note of the calendar, the following attributes can be used:</p> |   <p>For each note of the calendar, the following attributes can be used:</p> | ||||||
|   <figure |   <figure | ||||||
|   class="table"> |   class="table"> | ||||||
| @@ -244,10 +259,10 @@ class="table"> | |||||||
|     <p> |     <p> | ||||||
|       <img src="11_Calendar View_image.png"> |       <img src="11_Calendar View_image.png"> | ||||||
|     </p> |     </p> | ||||||
|   <p>The calendar displays all the child notes of the book that have a <code>#startDate</code>. |     <p>The calendar displays all the child notes of the Collection that have | ||||||
|     An <code>#endDate</code> can optionally be added.</p> |       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, |     <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" |       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" | #label:endDate(inheritable)="promoted,alias=End Date,single,date" | ||||||
| #hidePromotedAttributes </code></pre> | #hidePromotedAttributes </code></pre> | ||||||
|     <p>This will result in:</p> |     <p>This will result in:</p> | ||||||
| @@ -261,7 +276,7 @@ class="table"> | |||||||
|     <h3>Using with the Journal / calendar</h3> |     <h3>Using with the Journal / calendar</h3> | ||||||
|     <p>It is possible to integrate the calendar view into the Journal with day |     <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 |       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> |       root) to Collection and then select the Calendar View.</p> | ||||||
|     <p>Based on the <code>#calendarRoot</code> (or <code>#workspaceCalendarRoot</code>) |     <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 |       attribute, the calendar will know that it's in a calendar and apply the | ||||||
|       following:</p> |       following:</p> | ||||||
| @@ -283,7 +298,7 @@ class="table"> | |||||||
|       However, it is possible to configure a different attribute to be displayed |       However, it is possible to configure a different attribute to be displayed | ||||||
|       instead.</p> |       instead.</p> | ||||||
|     <p>To do so, assign <code>#calendar:title</code> to the child note (not the |     <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 |       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 |       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 |       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> |       does not have the requested label, the title of the note will be used instead.</p> | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| <aside class="admonition important"> | <aside class="admonition important"> | ||||||
|   <p>Starting with Trilium v0.97.0, the geo map has been converted from a standalone |   <p>Starting with Trilium v0.97.0, the geo map has been converted from a standalone | ||||||
|     <a |     <a | ||||||
|     href="#root/pOsGYCXsbNQG/_help_KSZ04uQ2D1St">note type</a>to a type of view for the <a class="reference-link" |     href="#root/_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_0ESUbbAxVnoK">Note List</a>. </p> | ||||||
| </aside> | </aside> | ||||||
| <figure class="image image-style-align-center"> | <figure class="image image-style-align-center"> | ||||||
|   <img style="aspect-ratio:892/675;" src="9_Geo Map View_image.png" width="892" |   <img style="aspect-ratio:892/675;" src="9_Geo Map View_image.png" width="892" | ||||||
| @@ -45,6 +45,7 @@ | |||||||
|     </tbody> |     </tbody> | ||||||
|   </table> |   </table> | ||||||
| </figure> | </figure> | ||||||
|  |  | ||||||
| <h2>Repositioning the map</h2> | <h2>Repositioning the map</h2> | ||||||
| <ul> | <ul> | ||||||
|   <li>Click and drag the map in order to move across the map.</li> |   <li>Click and drag the map in order to move across the map.</li> | ||||||
| @@ -109,6 +110,7 @@ | |||||||
|     </tbody> |     </tbody> | ||||||
|   </table> |   </table> | ||||||
| </figure> | </figure> | ||||||
|  |  | ||||||
| <h3>Adding a new note using the contextual menu</h3> | <h3>Adding a new note using the contextual menu</h3> | ||||||
| <ol> | <ol> | ||||||
|   <li>Right click anywhere on the map, where to place the newly created marker |   <li>Right click anywhere on the map, where to place the newly created marker | ||||||
| @@ -119,13 +121,13 @@ | |||||||
| </ol> | </ol> | ||||||
| <h3>Adding an existing note on note from the note tree</h3> | <h3>Adding an existing note on note from the note tree</h3> | ||||||
| <ol> | <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>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> |   <li>The map should be updated with the new marker.</li> | ||||||
| </ol> | </ol> | ||||||
| <p>This works for:</p> | <p>This works for:</p> | ||||||
| <ul> | <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> |     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 but not yet positioned on the map.</li> | ||||||
|   <li>Notes that are a child of the geo map and also positioned, case in which |   <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> | <h2>How the location of the markers is stored</h2> | ||||||
| <p>The location of a marker is stored in the <code>#geolocation</code> attribute | <p>The location of a marker is stored in the <code>#geolocation</code> attribute | ||||||
|   of the child notes:</p> |   of the child notes:</p> | ||||||
| <p> | <img src="18_Geo Map View_image.png" width="1288" | ||||||
|   <img src="18_Geo Map View_image.png" width="1288" height="278"> | height="278"> | ||||||
| </p> |  | ||||||
| <p>This value can be added manually if needed. The value of the attribute | <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> |   is made up of the latitude and longitude separated by a comma.</p> | ||||||
| <h2>Repositioning markers</h2> | <h2>Repositioning markers</h2> | ||||||
| @@ -148,19 +149,18 @@ | |||||||
|   page (<kbd>Ctrl</kbd>+<kbd>R</kbd> ) to cancel it.</p> |   page (<kbd>Ctrl</kbd>+<kbd>R</kbd> ) to cancel it.</p> | ||||||
| <h2>Interaction with the markers</h2> | <h2>Interaction with the markers</h2> | ||||||
| <ul> | <ul> | ||||||
|   <li>Hovering over a marker will display the content of the note it belongs |   <li>Hovering over a marker will display a <a class="reference-link" href="#root/_help_lgKX7r3aL30x">Note Tooltip</a> with | ||||||
|     to. |     the content of the note it belongs to. | ||||||
|     <ul> |     <ul> | ||||||
|       <li>Clicking on the note title in the tooltip will navigate to the note in |       <li>Clicking on the note title in the tooltip will navigate to the note in | ||||||
|         the current view.</li> |         the current view.</li> | ||||||
|     </ul> |     </ul> | ||||||
|   </li> |   </li> | ||||||
|   <li>Middle-clicking the marker will open the note in a new tab.</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: |   <li>Right-clicking the marker will open a contextual menu (as described below).</li> | ||||||
|     <ul> |   <li>If the map is in read-only mode, clicking on a marker will open a  | ||||||
|       <li> </li> |     <a | ||||||
|     </ul> |     class="reference-link" href="#root/_help_ZjLYv08Rp3qC">Quick edit</a> popup for the corresponding note.</li> | ||||||
|   </li> |  | ||||||
| </ul> | </ul> | ||||||
| <h2>Contextual menu</h2> | <h2>Contextual menu</h2> | ||||||
| <p>It's possible to press the right mouse button to display a contextual | <p>It's possible to press the right mouse button to display a contextual | ||||||
| @@ -261,6 +261,7 @@ | |||||||
|     </tbody> |     </tbody> | ||||||
|   </table> |   </table> | ||||||
| </figure> | </figure> | ||||||
|  |  | ||||||
| <h3>Adding from OpenStreetMap</h3> | <h3>Adding from OpenStreetMap</h3> | ||||||
| <p>Similarly to the Google Maps approach:</p> | <p>Similarly to the Google Maps approach:</p> | ||||||
| <figure class="table" style="width:100%;"> | <figure class="table" style="width:100%;"> | ||||||
| @@ -310,6 +311,7 @@ | |||||||
|     </tbody> |     </tbody> | ||||||
|   </table> |   </table> | ||||||
| </figure> | </figure> | ||||||
|  |  | ||||||
| <h2>Adding GPS tracks (.gpx)</h2> | <h2>Adding GPS tracks (.gpx)</h2> | ||||||
| <p>Trilium has basic support for displaying GPS tracks on the geo map.</p> | <p>Trilium has basic support for displaying GPS tracks on the geo map.</p> | ||||||
| <figure | <figure | ||||||
| @@ -377,18 +379,19 @@ class="table" style="width:100%;"> | |||||||
|   <p>When a map is in read-only all editing features will be disabled such |   <p>When a map is in read-only all editing features will be disabled such | ||||||
|     as:</p> |     as:</p> | ||||||
|   <ul> |   <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>Dragging markers.</li> | ||||||
|     <li>Editing from the contextual menu (removing locations or adding new items).</li> |     <li>Editing from the contextual menu (removing locations or adding new items).</li> | ||||||
|   </ul> |   </ul> | ||||||
|   <p>To enable read-only mode simply press the <em>Lock</em> icon from the  |   <p>To enable read-only mode simply press the <em>Lock</em> icon from the  | ||||||
|     <a |     <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> |   <h2>Troubleshooting</h2> | ||||||
|   <figure class="image image-style-align-right image_resized" style="width:34.06%;"> |   <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" |     <img style="aspect-ratio:678/499;" src="13_Geo Map View_image.png" width="678" | ||||||
|     height="499"> |     height="499"> | ||||||
|   </figure> |   </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 |   <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 |     of the map to not render correctly due to fractional scaling. The only | ||||||
|   | |||||||
							
								
								
									
										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 | <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>. |   notes and the columns are <a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>. | ||||||
|   In addition, values are editable.</p> |   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> | <h2>Interaction</h2> | ||||||
| <h3>Creating a new table</h3> | <h3>Creating a new table</h3> | ||||||
| <p>Right click the <a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> and | <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> |   select <em>Insert child note</em> and look for the <em>Table item</em>.</p> | ||||||
| <h3>Adding columns</h3> | <h3>Adding columns</h3> | ||||||
| <p>Each column is a <a href="#root/_help_OFXdgB2nNk1F">promoted attribute</a> that | <p>Each column is a <a href="#root/_help_OFXdgB2nNk1F">promoted or unpromoted attribute</a> that | ||||||
|   is defined on the Book note. Ideally, the promoted attributes need to be |   is defined on the Collection note.</p> | ||||||
|   inheritable in order to show up in the child notes.</p> | <p>To create a new column, either:</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> |  | ||||||
| <ul> | <ul> | ||||||
|   <li>The current item number, identified by the <code>#</code> symbol. This simply |   <li>Press <em>Add new column</em> at the bottom of the table.</li> | ||||||
|     counts the note and is affected by sorting.</li> |   <li>Right click on an existing column and select Add column to the left/right.</li> | ||||||
|   <li><a class="reference-link" href="#root/_help_m1lbrzyKDaRB">Note ID</a>, |   <li>Right click on the empty space of the column header and select <em>Label</em> or <em>Relation</em> in | ||||||
|     representing the unique ID used internally by Trilium</li> |     the <em>New column</em> section.</li> | ||||||
|   <li>The title of the note.</li> |  | ||||||
| </ul> | </ul> | ||||||
| <h3>Adding new rows</h3> | <h3>Adding new rows</h3> | ||||||
| <p>Each row is actually a note that is a child of the book note.</p> | <p>Each row is actually a note that is a child of the Collection note.</p> | ||||||
| <p>To create a new note, press <em>Add new row</em> at the bottom of the table. | <p>To create a new note, either:</p> | ||||||
|   By default it will try to edit the title of the newly created note.</p> | <ul> | ||||||
| <p>Alternatively, the note can be created from the<a class="reference-link" |   <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> |   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> | <h3>Editing data</h3> | ||||||
| <p>Simply click on a cell within a row to change its value. The change will | <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 |   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 |   <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> |     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>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> |     </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> | <h2>Working with the data</h2> | ||||||
| <h3>Sorting</h3> | <h3>Sorting by column</h3> | ||||||
| <p>It is possible to sort the data by the values of a column:</p> | <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> | <ul> | ||||||
|   <li>To do so, simply click on a column.</li> |   <li>To do so, simply click on a column.</li> | ||||||
|   <li>To switch between ascending or descending sort, simply click again on |   <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 |     the same column. The arrow next to the column will indicate the direction | ||||||
|     of the sort.</li> |     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> | </ul> | ||||||
| <h3>Reordering and hiding columns</h3> | <h3>Reordering and hiding columns</h3> | ||||||
| <ul> | <ul> | ||||||
| @@ -55,36 +129,52 @@ | |||||||
|     the item corresponding to the column.</li> |     the item corresponding to the column.</li> | ||||||
| </ul> | </ul> | ||||||
| <h3>Reordering rows</h3> | <h3>Reordering rows</h3> | ||||||
| <p>Notes can be dragged around to change their order. This will also change | <p>Notes can be dragged around to change their order. To do so, move the | ||||||
|   the order of the note in the <a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</p> |   mouse over the three vertical dots near the number row and drag the mouse | ||||||
| <p>Currently, it's possible to reorder notes even if sorting is used, but |   to the desired position.</p> | ||||||
|   the result might be inconsistent.</p> | <p>This will also change the order of the note in the <a class="reference-link" | ||||||
| <h2>Limitations</h2> |   href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</p> | ||||||
| <p>The table functionality is still in its early stages, as such it faces | <p>Reordering does have some limitations:</p> | ||||||
|   quite a few important limitations:</p> | <ul> | ||||||
| <ol> |   <li>If the parent note has <code>#sorted</code>, reordering will be disabled.</li> | ||||||
|   <li>As mentioned previously, the columns of the table are defined as  |   <li>If using nested tables, then reordering will also be disabled.</li> | ||||||
|     <a |   <li>Currently, it's possible to reorder notes even if column sorting is used, | ||||||
|     class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>. |     but the result might be inconsistent.</li> | ||||||
|       <ol> | </ul> | ||||||
|         <li>But only the promoted attributes that are defined at the level of the | <h3>Nested trees</h3> | ||||||
|           Book note are actually taken into consideration.</li> | <p>If the child notes of the collection also have their own child notes, | ||||||
|         <li>There are plans to recursively look for columns across the sub-hierarchy.</li> |   then they will be displayed in a hierarchy.</p> | ||||||
|       </ol> | <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> | ||||||
|   <li>Hierarchy is not yet supported, so the table will only show the items |   <li>Manually set <code>maxNestingDepth</code> to the desired value.</li> | ||||||
|     that are direct children of the <em>Book</em> note.</li> | </ul> | ||||||
|   <li>Multiple labels and relations are not supported. If a <a class="reference-link" | <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 |   href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> is defined | ||||||
|     with a <em>Multi value</em> specificity, they will be ignored.</li> |   with a <em>Multi value</em> specificity, they will be ignored.</p> | ||||||
| </ol> |  | ||||||
| <h2>Use in search</h2> | <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 | <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> |   adding the <code>#viewType=table</code> attribute.</p> | ||||||
| <p>Unlike when used in a book, saved searches are not limited to the sub-hierarchy | <p>Unlike when used in a Collection, saved searches are not limited to the | ||||||
|   of a note and allows for advanced queries thanks to the power of the  |   sub-hierarchy of a note and allows for advanced queries thanks to the power | ||||||
|   <a |   of the <a class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a>.</p> | ||||||
|   class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a>.</p> |  | ||||||
| <p>However, there are also some limitations:</p> | <p>However, there are also some limitations:</p> | ||||||
| <ul> | <ul> | ||||||
|   <li>It's not possible to reorder notes.</li> |   <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. |     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 |     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> |     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 |     disallow all interaction that would otherwise change the map (dragging | ||||||
|     notes, adding new items).</li> |     notes, adding new items).</li> | ||||||
| </ul> | </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> ( |   in the context menu, or with the associated keyboard <a href="#root/_help_A9Oc6YKKc65v">shortcuts</a>: <code>CTRL-C</code> ( | ||||||
|   <a |   <a | ||||||
|   href="#root/_help_IakOLONlIfGI">copy</a>), <kbd>Ctrl</kbd> + <kbd>X</kbd> (cut) and <kbd>Ctrl</kbd> + <kbd>V</kbd> (paste).</p> |   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 | <p>See <a class="reference-link" href="#root/_help_YtSN43OrfzaA">Note tree contextual menu</a> for | ||||||
|   more information.</p> |   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 |