Compare commits
357 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 | ||
|
|
b6f50b6af0 | ||
|
|
fc454cba03 | ||
|
|
6f165df29e | ||
|
|
d16468071d | ||
|
|
20a492523f | ||
|
|
1216f51c78 | ||
|
|
ea3ac1041b | ||
|
|
d838e8baf0 | ||
|
|
60a7347d7d | ||
|
|
4e05e79426 | ||
|
|
aa872f47f2 | ||
|
|
fbd833ad86 | ||
|
|
bee65ed32c | ||
|
|
5adca76a9a | ||
|
|
e7467f6446 | ||
|
|
e49473fbd3 | ||
|
|
bfec44aa5a | ||
|
|
55b3bf6036 | ||
|
|
c9c07f0cb0 | ||
|
|
e25727441d | ||
|
|
51b7955ccd | ||
|
|
196bba9cda | ||
|
|
430ed78d85 | ||
|
|
2d11ed805d | ||
|
|
f55426bdb0 | ||
|
|
87b5068fec | ||
|
|
9ddd1a4ae2 | ||
|
|
736bc9c9bd | ||
|
|
5a2da62992 | ||
|
|
1a72eb91ee | ||
|
|
0d3c5b06e2 |
2
.github/FUNDING.yml
vendored
@@ -2,3 +2,5 @@
|
||||
|
||||
github: [eliandoran]
|
||||
custom: ["https://paypal.me/eliandoran"]
|
||||
liberapay: ElianDoran
|
||||
buy_me_a_coffee: eliandoran
|
||||
|
||||
17
.github/workflows/checks.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Checks
|
||||
on:
|
||||
push:
|
||||
pull_request_target:
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check if PRs have conflicts
|
||||
uses: eps1lon/actions-label-merge-conflict@v3
|
||||
with:
|
||||
dirtyLabel: "merge-conflicts"
|
||||
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"
|
||||
4
.mailmap
@@ -1,2 +1,2 @@
|
||||
Adam Zivner <adam.zivner@gmail.com>
|
||||
Adam Zivner <zadam.apps@gmail.com>
|
||||
zadam <adam.zivner@gmail.com>
|
||||
zadam <zadam.apps@gmail.com>
|
||||
9
.vscode/settings.json
vendored
@@ -28,5 +28,12 @@
|
||||
"typescript.validate.enable": true,
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"docs/**/*.html": true,
|
||||
"docs/**/*.png": true,
|
||||
"apps/server/src/assets/doc_notes/**": true,
|
||||
"apps/edit-docs/demo/**": true
|
||||
}
|
||||
}
|
||||
15
README.md
@@ -1,6 +1,7 @@
|
||||
# Trilium Notes
|
||||
|
||||

|
||||
Donate:  
|
||||
|
||||

|
||||

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