Compare commits

...

163 Commits

Author SHA1 Message Date
Elian Doran
de6a6cbb07 feat(tree): open notes in new window from tree (#8269) 2026-01-10 19:37:12 +02:00
Elian Doran
2dcb003909 chore(deps): update pnpm to v10.28.0 (#8329) 2026-01-10 19:36:34 +02:00
Elian Doran
d5c934a518 add logseq to supported protocols (#8320) 2026-01-10 19:36:03 +02:00
Elian Doran
779909837c Allow hiding children from tree (especially for collections) (#8335) 2026-01-10 18:32:41 +02:00
Elian Doran
a4cb375a0f test(client): fix broken tests 2026-01-10 18:21:38 +02:00
Elian Doran
e2da8c28ca fix(tree): spotlighted note has + button & insert child context menu 2026-01-10 18:21:16 +02:00
Elian Doran
7808848f05 Translations update from Hosted Weblate (#8339) 2026-01-10 18:02:07 +02:00
Elian Doran
7c05109645 Merge remote-tracking branch 'origin/main' into feature/hide_from_tree 2026-01-10 18:01:05 +02:00
Hosted Weblate
8f9e89b73b Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-01-10 15:59:25 +00:00
Elian Doran
861a61a4d8 Tree performance improvement (#8274) 2026-01-10 17:59:08 +02:00
Elian Doran
52d4083814 chore(tree): address requested changes 2026-01-10 17:46:45 +02:00
Elian Doran
0272189b22 fix(tree): performance issue due to batch update 2026-01-10 17:32:06 +02:00
Elian Doran
ddba0e823c fix(tree): tree is updated on note content updates 2026-01-10 17:32:02 +02:00
Elian Doran
be81acb9e7 feat(tree): respect motion settings instead of always disabling animation 2026-01-10 16:11:15 +02:00
Elian Doran
3bb97385c9 fix(client): a case where inheritance boolean is not correct 2026-01-10 13:17:44 +02:00
Elian Doran
a72cec0494 chore(client): address a few requested changes 2026-01-10 13:06:13 +02:00
Elian Doran
cb02198c6f docs(user): document hiding the subtree 2026-01-10 12:57:11 +02:00
Elian Doran
298d438230 Merge branch 'feature/tree_performance_improvement' into feature/hide_from_tree 2026-01-10 12:34:44 +02:00
Elian Doran
cb2f7932dd Merge remote-tracking branch 'origin/main' into feature/tree_performance_improvement 2026-01-10 12:34:26 +02:00
Elian Doran
3354bd669f fix(client/tree): toast displayed when doing operations outside of tree 2026-01-10 12:32:55 +02:00
Elian Doran
8ad779be66 fix(client/load_results): component ID not preserved for attributes & branches 2026-01-10 12:26:08 +02:00
Elian Doran
f462034868 fix(client/tree): toast not appearing when inserted via paste 2026-01-10 11:55:49 +02:00
Elian Doran
26c25cd4cd fix(client): edge case not handled when parent note overrides to false 2026-01-10 11:25:00 +02:00
Elian Doran
6398830c2d test(client): attribute toggling 2026-01-10 11:15:21 +02:00
Elian Doran
0b065063f2 refactor(client): use same logic for setting boolean with inheritance 2026-01-10 10:37:19 +02:00
Elian Doran
a3a9de6fdd feat(collections): add setting to hide subtree 2026-01-10 10:34:06 +02:00
Elian Doran
d77d30f29e fix(note_tree): subtree hidden cannot be overridden through inheritance 2026-01-10 10:17:09 +02:00
Elian Doran
5cabc6379d fix(note_tree): not reacting to changes in subtreeHidden 2026-01-10 10:14:15 +02:00
Elian Doran
af537e6a48 feat(tree): add option to hide or show subtree 2026-01-10 10:08:50 +02:00
Elian Doran
faf3797663 feat(tree): disable insert child note if subnotes are hidden 2026-01-10 09:57:09 +02:00
Elian Doran
db57f3ff62 feat(tree): add tooltip when moving into hidden subtree 2026-01-10 09:53:23 +02:00
Elian Doran
0f77caad69 feat(tree): hide items dragged into a subtreeHidden 2026-01-10 09:40:19 +02:00
renovate[bot]
751b91e1b8 chore(deps): update pnpm to v10.28.0 2026-01-10 00:41:10 +00:00
Elian Doran
968a17fbfb fix(tree): alignment of note count badge 2026-01-10 00:55:33 +02:00
Elian Doran
712e87b39f feat(tree): distinguish spotlighted node 2026-01-10 00:52:11 +02:00
Elian Doran
0b40b42315 fix(tree): random exceptions when switching between two spotlighted notes 2026-01-10 00:42:24 +02:00
Elian Doran
62996b1162 feat(tree): remove spotlighted note after switching to another one 2026-01-10 00:38:28 +02:00
Elian Doran
b67ccc6091 feat(tree): basic spotlight support for hidden child 2026-01-10 00:15:59 +02:00
Elian Doran
211d2dcf99 feat(collections): hide children by default for some collection types 2026-01-09 20:06:49 +02:00
Elian Doran
ee52e16a75 feat(tree): add title for subnote count badge 2026-01-09 19:15:57 +02:00
Elian Doran
0c27bd25fa chore(tree): align child count to the right 2026-01-09 17:03:56 +02:00
Elian Doran
b6a6e78d01 feat(tree): hide add button if subtree is hidden 2026-01-09 16:59:55 +02:00
Elian Doran
92e6a29e70 feat(tree): display number of children if subtree is hidden 2026-01-09 16:54:04 +02:00
Elian Doran
acc8cee7cd Merge remote-tracking branch 'origin/feature/tree_performance_improvement' into feature/hide_from_tree 2026-01-09 16:38:56 +02:00
Elian Doran
afefbe154b feat(tree): hide arrow if children are hidden 2026-01-09 16:37:11 +02:00
SngAbc
83fa55b7d9 Merge branch 'main' into feat/tree/new_window 2026-01-09 22:35:30 +08:00
Elian Doran
4f6c10d995 feat(tree): allow hiding child notes via attribute 2026-01-09 16:32:55 +02:00
Elian Doran
ed972d2601 e2e(server): math popup fails on CI 2026-01-09 12:13:39 +02:00
Elian Doran
6b57ee5654 e2e(server): flakiness in tab_bar 2026-01-09 11:54:16 +02:00
contributor
e469af1ca5 add logseq to supported protocols 2026-01-09 11:51:49 +02:00
Elian Doran
6d41f076c2 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-01-09 11:25:26 +02:00
Elian Doran
8cff591746 fix(toc): unnecessary <ol> when no children 2026-01-09 11:25:24 +02:00
Elian Doran
b3ccf89094 Translations update from Hosted Weblate (#8315) 2026-01-09 00:06:09 +02:00
Ulices
d31c6b1627 Translated using Weblate (Spanish)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/es/
2026-01-08 23:01:53 +01:00
Yatrik Patel
1481356d1f Translated using Weblate (Hindi)
Currently translated at 36.8% (56 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-08 23:01:52 +01:00
Ulices
a54661fd0a Translated using Weblate (Spanish)
Currently translated at 93.8% (1643 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-01-08 23:01:49 +01:00
Elian Doran
ae4a3f10ae fix(toc): equations not rendered in new layout 2026-01-08 21:26:04 +02:00
Elian Doran
fe3160e7a1 e2e(server): adapt tests to new layout directly 2026-01-08 20:32:54 +02:00
Elian Doran
66659d4786 e2e(server): flaky test in PDF 2026-01-08 20:11:21 +02:00
Elian Doran
0b25b09040 feat(ci): check version consistency before releasing 2026-01-08 19:49:29 +02:00
Elian Doran
0d41cc2660 Merge remote-tracking branch 'origin/stable' 2026-01-08 19:42:10 +02:00
Elian Doran
f5e8822718 chore(release): prepare for v0.101.3 2026-01-08 19:38:21 +02:00
Elian Doran
bdc220ec12 Merge remote-tracking branch 'origin/stable' 2026-01-08 18:19:16 +02:00
Elian Doran
3eb68e5271 Stable fixes (#8310) 2026-01-08 18:16:55 +02:00
Elian Doran
521952ebcc test(client): remove debug statements 2026-01-08 18:10:00 +02:00
Elian Doran
034091a696 docs(release): prepare for v0.101.2 2026-01-08 18:08:34 +02:00
Elian Doran
ae881101d8 fix(note_list): archived notes displayed in empty grid card (closes #8184) 2026-01-08 17:23:40 +02:00
Elian Doran
b11a30c49c fix(launcher_bar): crashing if there is a non-launcher note (closes #8218) 2026-01-08 16:55:51 +02:00
Elian Doran
4625efda7f fix(note_list): skip rendering of included notes for performance (closes #8017) 2026-01-08 16:50:27 +02:00
Elian Doran
3c168d750d fix(client): cycle in include causing infinite loop (closes #8294) 2026-01-08 16:44:35 +02:00
Elian Doran
5cc7b259ce fix(client): max content width not preserved (closes #8065) 2026-01-08 15:59:57 +02:00
Elian Doran
f7ae046b20 fix(mermaid): error container not scrollable (closes #8299) 2026-01-08 15:52:19 +02:00
Elian Doran
02f43d6239 fix(mermaid): code not scrollable (closes #8299) 2026-01-08 15:33:16 +02:00
Elian Doran
53e1fa1047 fix(mermaid) diagrams not saving content and SVG attachment (#8220) 2026-01-08 15:22:07 +02:00
Elian Doran
b1dc0e234f fix(popupEditor): fix closing of popupEditor when inserting note link (#8224) 2026-01-08 15:21:23 +02:00
Elian Doran
9d380dd828 fix(sql_console): cannot copy table data (#8268) 2026-01-08 15:20:36 +02:00
Elian Doran
1f77540dbb fix(text): Title is not selected when creating a note via the launcher (#8292) 2026-01-08 15:20:14 +02:00
Elian Doran
716612680d Translations update from Hosted Weblate (#8293) 2026-01-07 19:35:42 +02:00
Michael
3800fb85eb Translated using Weblate (German)
Currently translated at 95.4% (1672 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-01-07 18:32:56 +01:00
Rafa Osuna
d807984be4 Translated using Weblate (Spanish)
Currently translated at 92.7% (1624 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-01-07 18:32:56 +01:00
Giovi
2c92ae8898 Translated using Weblate (Italian)
Currently translated at 100.0% (1751 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-01-07 18:32:55 +01:00
Argann Bonneau
3d8cbc81c4 Translated using Weblate (French)
Currently translated at 94.5% (1656 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2026-01-07 18:32:54 +01:00
Yatrik Patel
d747c94450 Translated using Weblate (Hindi)
Currently translated at 3.4% (4 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/
2026-01-07 18:32:53 +01:00
pythaac
a627d1f96e Translated using Weblate (Korean)
Currently translated at 76.3% (116 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2026-01-07 18:32:53 +01:00
Yatrik Patel
869db5e478 Translated using Weblate (Hindi)
Currently translated at 0.9% (17 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-07 18:32:52 +01:00
Yatrik Patel
73e94d385e Translated using Weblate (Hindi)
Currently translated at 5.9% (23 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-07 18:32:51 +01:00
Kim Nøglegaard
8f4ebeb335 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-07 18:32:51 +01:00
Yatrik Patel
263ee864be Translated using Weblate (Hindi)
Currently translated at 9.2% (14 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-07 18:32:50 +01:00
Elian Doran
f078732624 fix(text): Title is not selected when creating a note via the launcher (#8292) 2026-01-07 19:32:37 +02:00
SngAbc
fac1f6b16c fix(text): Title is not focused when creating a note via the launcher
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-07 10:33:17 +08:00
SiriusXT
a5841c1423 fix(text): Title is not focused when creating a note via the launcher 2026-01-07 10:11:24 +08:00
Elian Doran
aaca18003d Translations update from Hosted Weblate (#8279) 2026-01-06 13:54:24 +02:00
Kim Nøglegaard
5ec521b024 Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.4% (104 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-06 04:01:53 +01:00
Yatrik Patel
b3c0be7559 Translated using Weblate (Hindi)
Currently translated at 3.0% (12 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-06 04:01:51 +01:00
Máté Zsólya
d52b735b99 Translated using Weblate (Hungarian)
Currently translated at 1.9% (34 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hu/
2026-01-06 04:01:49 +01:00
Yatrik Patel
639b1f2863 Translated using Weblate (Hindi)
Currently translated at 5.9% (9 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-06 04:01:47 +01:00
Elian Doran
aff4f7e010 feat(tree): disable animation for performance 2026-01-06 01:34:02 +02:00
Elian Doran
dec4dafba6 feat(tree): avoid async 2026-01-06 01:26:56 +02:00
Elian Doran
d0cdcfc32c refactor(tree): use loop for mini optimisation 2026-01-06 01:21:43 +02:00
Elian Doran
0867b81c7a feat(tree): use template for create child to improve performance 2026-01-06 01:13:31 +02:00
Elian Doran
bde6068f2d refactor(tree): extract enchance title into separate method 2026-01-06 01:07:37 +02:00
Elian Doran
47fd2affa4 feat(tree): use direct DOM manipulation instead of jQuery 2026-01-06 00:59:32 +02:00
Elian Doran
7f2cc885fe Feat(math): Improve legacy math input with MathLive (#7842) 2026-01-06 00:12:38 +02:00
Elian Doran
19a365a370 fix(sql_console): cannot copy table data (#8268) 2026-01-06 00:10:11 +02:00
Elian Doran
9a50da328e chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.9 (#8265) 2026-01-05 23:53:05 +02:00
Elian Doran
181e36a7c1 Merge remote-tracking branch 'origin/main' into Meinzzzz/main
; Conflicts:
;	.gitignore
2026-01-05 23:46:12 +02:00
Elian Doran
178508d245 Merge branch 'main' into fix/sql_select_text 2026-01-05 23:43:29 +02:00
Elian Doran
d132d084cf Merge branch 'main' into renovate/rollup-plugin-webpack-stats-2.x 2026-01-05 23:43:06 +02:00
Elian Doran
494b55d685 fix(ckeditor): missing pl locale 2026-01-05 23:39:36 +02:00
Elian Doran
51513d3779 fix(status_bar): count not refreshing properly after change 2026-01-05 21:03:32 +02:00
SiriusXT
5b95b9875b feat(tree): open notes in new window from tree 2026-01-05 19:27:44 +08:00
SngAbc
458398f2ca Merge branch 'main' into fix/sql_select_text 2026-01-05 13:51:45 +08:00
SngAbc
7a6cc4f51e fix(sql_console): cannot copy table data
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-05 12:16:16 +08:00
SiriusXT
f4ccce7de5 fix(sql_console): cannot copy table data 2026-01-05 11:23:50 +08:00
renovate[bot]
f8b5417d6c chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.9 2026-01-05 01:03:52 +00:00
meinzzzz
87ab41c80c Fix shift+tab behavior in MathInputView 2025-12-23 18:02:40 +01:00
Meinzzzz
d2391f94c0 Fix offline math rendering by bundling local fonts 2025-12-15 21:32:50 +01:00
Meinzzzz
050ddb8c55 Improve css to fix tooltips 2025-12-15 20:17:58 +01:00
Meinzzzz
bc23e0984a Undo unnecessary formatting changes 2025-12-14 22:00:56 +01:00
Meinzzzz
07de353207 Adding comments and improving code quality in math input views 2025-12-14 20:21:42 +01:00
Meinzzzz
c02491d2e6 Remove unnecessary any casts in math plugin 2025-12-12 23:09:20 +01:00
Meinzzzz
a6ede8f905 Improve mathinputview 2025-12-12 21:33:59 +01:00
Meinzzzz
22941a9ce0 Fix sync issues 2025-12-12 19:48:09 +01:00
Meinzzzz
633a09d414 Fix sync bug 2025-12-11 23:06:13 +01:00
Meinzzzz
29f0881c5a Fix clicking issue in Mathfield 2025-12-10 22:44:02 +01:00
Meinzzzz
60debca37b Improve comments 2025-12-10 18:36:34 +01:00
Meinzzzz
30ea81d0fb Improve virtual keyboard logic and fix Tab issues 2025-12-08 22:59:08 +01:00
Meinzzzz
b1d92c4fe6 Fix Tab issues 2025-12-08 22:39:12 +01:00
Meinzzzz
70f46de2d8 MathLive virtual keyboard only appears when focusing the mathfield 2025-12-08 20:30:07 +01:00
Meinzzzz
f1b2d0b870 Increas Mathfield font size and ensure virtual keyboard appears above CKEditor 2025-12-08 20:22:52 +01:00
Meinzzzz
8a385972fc Close Virtual Keyboard when Mathinput is closed 2025-12-08 18:49:06 +01:00
Meinzzzz
28dd85c1d1 Merge upstream changes and resolve conflicts 2025-12-07 23:51:41 +01:00
meinzzzz
827c8e0e72 Refactor: Combine MathLive and LaTeX inputs into one single component 2025-12-07 23:19:48 +01:00
meinzzzz
162c076a14 Improve MathLive integration and lazy loading 2025-12-02 22:30:37 +01:00
meinzzzz
9386465de7 Added mathrender error class for better error handling in math rendering 2025-12-02 22:29:20 +01:00
meinzzzz
acca22f3a1 Improve Synchronization Between Mathlive and rawlatex input 2025-12-02 22:28:16 +01:00
meinzzzz
f8d84814e0 Fix differential d problems 2025-11-26 23:02:34 +01:00
meinzzzz
c46cf41842 Small improvements 2025-11-26 22:48:57 +01:00
meinzzzz
64ab1c4116 Imrovement for Latex 2025-11-26 22:29:29 +01:00
meinzzzz
a6de1041c7 Fix bug in math rendering where old content was not cleared 2025-11-26 21:59:33 +01:00
meinzzzz
c8d34e65ea Improve max window size 2025-11-26 21:49:09 +01:00
meinzzzz
51db729546 Improve and simplify Mathfield integration 2025-11-25 23:27:06 +01:00
meinzzzz
d2052ad236 Disable mathlive sound effects 2025-11-24 21:51:59 +01:00
meinzzzz
9c4301467f Remove unused icons from ckeditor5-math package 2025-11-24 19:46:04 +01:00
meinzzzz
e7355dc0e4 remove gitignore unneccesary changes 2025-11-24 18:43:52 +01:00
meinzzzz
4110fec94f Removed unnecessary declare keyboard 2025-11-24 18:28:59 +01:00
meinzzzz
d5e601eae9 Simpliyfied resize logic for math input form and improved css 2025-11-24 17:56:18 +01:00
meinzzzz
4f044c4a57 Use icons form CKEditor5 icons, instead of testing icons. 2025-11-23 22:43:07 +01:00
meinzzzz
5821c350e1 Fixing class property initialization order 2025-11-23 17:58:51 +01:00
meinzzzz
edba8188fe Fix dark selection colors in MathLive math-field 2025-11-23 13:44:28 +01:00
meinzzzz
1471a72633 refactor: avoid recursive updates in mathLiveInput by normalizing value before updateing 2025-11-23 13:34:22 +01:00
meinzzzz
56834cb88a Improve MathLive and Raw LaTeX input views to propagate mousedown events 2025-11-23 13:29:26 +01:00
meinzzzz
a0f16f9184 Fix typos in mathform.css 2025-11-23 13:09:56 +01:00
meinzzzz
de80eb4806 Improve mathform.css styling for better visual integration 2025-11-22 22:42:34 +01:00
meinzzzz
48a4b81fbe remove automated screenshot files 2025-11-22 21:40:55 +01:00
meinzzzz
e225794f72 Better window focus handling in MathFormView 2025-11-22 21:35:37 +01:00
meinzzzz
4eef30f8b5 Fix names 2025-11-22 00:20:20 +01:00
meinzzzz
569b09609d Remove mathlive dependency and chunking 2025-11-22 00:01:14 +01:00
meinzzzz
39838c25c2 Fixed chaching problems 2025-11-21 23:50:49 +01:00
meinzzzz
49e90c08a9 Better Names for Math UI Components 2025-11-20 22:45:21 +01:00
meinzzzz
e777b06fb8 Math 2025-11-20 18:53:39 +01:00
meinzzzz
497ec2ac74 Merge branch 'main' of https://github.com/Meinzzzz/Trilium-Mathlive 2025-11-20 18:00:18 +01:00
meinzzzz
c5d282d203 Mathlive 2025-11-20 00:09:10 +01:00
81 changed files with 2443 additions and 711 deletions

View File

@@ -11,6 +11,14 @@ concurrency:
cancel-in-progress: true
jobs:
sanity-check:
name: Sanity Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Check version consistency
run: pnpm tsx ${{ github.workspace }}/scripts/check-version-consistency.ts ${{ github.ref_name }}
make-electron:
name: Make Electron
strategy:

2
.gitignore vendored
View File

@@ -51,4 +51,4 @@ upload
# docs
site/
apps/*/coverage
scripts/translation/.language*.json
scripts/translation/.language*.json

View File

@@ -9,7 +9,7 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.27.0",
"packageManager": "pnpm@10.28.0",
"devDependencies": {
"@redocly/cli": "2.14.3",
"archiver": "7.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.101.1",
"version": "0.101.3",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",

View File

@@ -154,6 +154,7 @@ export type CommandMappings = {
};
openInTab: ContextMenuCommandData;
openNoteInSplit: ContextMenuCommandData;
openNoteInWindow: ContextMenuCommandData;
openNoteInPopup: ContextMenuCommandData;
toggleNoteHoisting: ContextMenuCommandData;
insertNoteAfter: ContextMenuCommandData;

View File

@@ -616,7 +616,9 @@ export default class FNote {
}
isFolder() {
return this.type === "search" || this.getFilteredChildBranches().length > 0;
if (this.isLabelTruthy("subtreeHidden")) return false;
if (this.type === "search") return true;
return this.getFilteredChildBranches().length > 0;
}
getFilteredChildBranches() {

View File

@@ -1,21 +1,21 @@
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
import treeService from "../services/tree.js";
import froca from "../services/froca.js";
import clipboard from "../services/clipboard.js";
import noteCreateService from "../services/note_create.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import appContext, { type ContextMenuCommandData, type FilteredCommandNames } from "../components/app_context.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import type FAttachment from "../entities/fattachment.js";
import attributes from "../services/attributes.js";
import { executeBulkActions } from "../services/bulk_action.js";
import clipboard from "../services/clipboard.js";
import dialogService from "../services/dialog.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import noteCreateService from "../services/note_create.js";
import noteTypesService from "../services/note_types.js";
import server from "../services/server.js";
import toastService from "../services/toast.js";
import dialogService from "../services/dialog.js";
import { t } from "../services/i18n.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type FAttachment from "../entities/fattachment.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import attributes from "../services/attributes.js";
import { executeBulkActions } from "../services/bulk_action.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
// TODO: Deduplicate once client/server is well split.
interface ConvertToAttachmentResponse {
@@ -72,6 +72,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node);
const notSearch = note?.type !== "search";
const hasSubtreeHidden = note?.isLabelTruthy("subtreeHidden") ?? false;
const isSpotlighted = this.node.extraClasses.includes("spotlighted-node");
const notOptionsOrHelp = !note?.noteId.startsWith("_options") && !note?.noteId.startsWith("_help");
const parentNotSearch = !parentNote || parentNote.type !== "search";
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
@@ -79,17 +81,18 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const items: (MenuItem<TreeCommandNames> | null)[] = [
{ title: t("tree-context-menu.open-in-a-new-tab"), command: "openInTab", shortcut: "Ctrl+Click", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-a-new-window"), command: "openNoteInWindow", uiIcon: "bx bx-window-open", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes },
isHoisted
? null
: {
title: `${t("tree-context-menu.hoist-note")}`,
command: "toggleNoteHoisting",
keyboardShortcut: "toggleNoteHoisting",
uiIcon: "bx bxs-chevrons-up",
enabled: noSelectedNotes && notSearch
},
title: `${t("tree-context-menu.hoist-note")}`,
command: "toggleNoteHoisting",
keyboardShortcut: "toggleNoteHoisting",
uiIcon: "bx bxs-chevrons-up",
enabled: noSelectedNotes && notSearch
},
!isHoisted || !isNotRoot
? null
: { title: t("tree-context-menu.unhoist-note"), command: "toggleNoteHoisting", keyboardShortcut: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
@@ -112,7 +115,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
keyboardShortcut: "createNoteInto",
uiIcon: "bx bx-plus",
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes && notOptionsOrHelp,
enabled: notSearch && noSelectedNotes && notOptionsOrHelp && !hasSubtreeHidden && !isSpotlighted,
columns: 2
},
@@ -150,8 +153,17 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
{ kind: "separator" },
{ title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
!hasSubtreeHidden && { title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
!hasSubtreeHidden && { title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{
title: hasSubtreeHidden ? t("tree-context-menu.show-subtree") : t("tree-context-menu.hide-subtree"),
uiIcon: "bx bx-show",
handler: async () => {
const note = await froca.getNote(this.node.data.noteId);
if (!note) return;
attributes.setBooleanWithInheritance(note, "subtreeHidden", !hasSubtreeHidden);
}
},
{
title: t("tree-context-menu.sort-by"),
command: "sortChildNotes",
@@ -164,7 +176,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptionsOrHelp }
]
].filter(Boolean) as MenuItem<TreeCommandNames>[]
},
{ kind: "separator" },
@@ -292,25 +304,30 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
noteCreateService.createNote(parentNotePath, {
target: "after",
targetBranchId: this.node.data.branchId,
type: type,
isProtected: isProtected,
templateNoteId: templateNoteId
type,
isProtected,
templateNoteId
});
} else if (command === "insertChildNote") {
const parentNotePath = treeService.getNotePath(this.node);
noteCreateService.createNote(parentNotePath, {
type: type,
type,
isProtected: this.node.data.isProtected,
templateNoteId: templateNoteId
templateNoteId
});
} else if (command === "openNoteInSplit") {
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
} else if (command === "openNoteInWindow") {
appContext.triggerCommand("openInWindow", {
notePath,
hoistedNoteId: appContext.tabManager.getActiveContext()?.hoistedNoteId
});
} else if (command === "openNoteInPopup") {
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
} else if (command === "convertNoteToAttachment") {
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
return;
@@ -332,11 +349,11 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
} else if (command === "copyNotePathToClipboard") {
navigator.clipboard.writeText("#" + notePath);
navigator.clipboard.writeText(`#${ notePath}`);
} else if (command) {
this.treeWidget.triggerCommand<TreeCommandNames>(command, {
node: this.node,
notePath: notePath,
notePath,
noteId: this.node.data.noteId,
selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node),
selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node)

View File

@@ -0,0 +1,139 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildNote } from "../test/easy-froca";
import { setBooleanWithInheritance } from "./attributes";
import froca from "./froca";
import server from "./server.js";
// Spy on server methods to track calls
// @ts-expect-error the generic typing is causing issues here
server.put = vi.fn(async <T> (url: string, data?: T) => ({} as T));
// @ts-expect-error the generic typing is causing issues here
server.remove = vi.fn(async <T> (url: string) => ({} as T));
describe("Set boolean with inheritance", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("doesn't call server if value matches directly", async () => {
const noteWithLabel = buildNote({
title: "New note",
"#foo": ""
});
const noteWithoutLabel = buildNote({
title: "New note"
});
await setBooleanWithInheritance(noteWithLabel, "foo", true);
await setBooleanWithInheritance(noteWithoutLabel, "foo", false);
expect(server.put).not.toHaveBeenCalled();
expect(server.remove).not.toHaveBeenCalled();
});
it("sets boolean normally without inheritance", async () => {
const standaloneNote = buildNote({
title: "New note"
});
await setBooleanWithInheritance(standaloneNote, "foo", true);
expect(server.put).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "",
isInheritable: false
});
});
it("removes boolean normally without inheritance", async () => {
const standaloneNote = buildNote({
title: "New note",
"#foo": ""
});
const attributeId = standaloneNote.getLabel("foo")!.attributeId;
await setBooleanWithInheritance(standaloneNote, "foo", false);
expect(server.remove).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/attributes/${attributeId}`);
});
it("doesn't call server if value matches inherited", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "",
"children": [
{
title: "Child note"
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(true);
await setBooleanWithInheritance(childNote, "foo", true);
expect(server.put).not.toHaveBeenCalled();
expect(server.remove).not.toHaveBeenCalled();
});
it("overrides boolean with inheritance", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "",
"children": [
{
title: "Child note"
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(true);
await setBooleanWithInheritance(childNote, "foo", false);
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "false",
isInheritable: false
});
});
it("overrides boolean with inherited false", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "false",
"children": [
{
title: "Child note"
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(false);
await setBooleanWithInheritance(childNote, "foo", true);
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "",
isInheritable: false
});
});
it("deletes override boolean with inherited false with already existing value", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "false",
"children": [
{
title: "Child note",
"#foo": "false",
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(false);
await setBooleanWithInheritance(childNote, "foo", true);
expect(server.put).toBeCalledWith(`notes/${childNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "",
isInheritable: false
});
});
});

View File

@@ -1,14 +1,15 @@
import server from "./server.js";
import froca from "./froca.js";
import type FNote from "../entities/fnote.js";
import type { AttributeRow } from "./load_results.js";
import { AttributeType } from "@triliumnext/commons";
import type FNote from "../entities/fnote.js";
import froca from "./froca.js";
import type { AttributeRow } from "./load_results.js";
import server from "./server.js";
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
await server.put(`notes/${noteId}/attribute`, {
type: "label",
name: name,
value: value,
name,
value,
isInheritable
});
}
@@ -16,8 +17,8 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe
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,
name,
value,
isInheritable
});
}
@@ -25,12 +26,42 @@ export async function setLabel(noteId: string, name: string, value: string = "",
export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) {
await server.put(`notes/${noteId}/set-attribute`, {
type: "relation",
name: name,
value: value,
name,
value,
isInheritable
});
}
/**
* Sets a boolean label on the given note, taking inheritance into account. If the desired value matches the inherited
* value, any owned label will be removed to allow the inherited value to take effect. If the desired value differs
* from the inherited value, an owned label will be created or updated to reflect the desired value.
*
* When checking if the boolean value is set, don't use `note.hasLabel`; instead use `note.isLabelTruthy`.
*
* @param note the note on which to set the boolean label.
* @param labelName the name of the label to set.
* @param value the boolean value to set for the label.
*/
export async function setBooleanWithInheritance(note: FNote, labelName: string, value: boolean) {
const actualValue = note.isLabelTruthy(labelName);
if (actualValue === value) return;
const hasInheritedValue = !note.hasOwnedLabel(labelName) && note.hasLabel(labelName);
if (hasInheritedValue) {
if (value) {
setLabel(note.noteId, labelName, "");
} else {
// Label is inherited - override to false.
setLabel(note.noteId, labelName, "false");
}
} else if (value) {
setLabel(note.noteId, labelName, "");
} else {
removeOwnedLabelByName(note, labelName);
}
}
async function removeAttributeById(noteId: string, attributeId: string) {
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
}
@@ -142,6 +173,7 @@ export default {
setLabel,
setRelation,
setAttribute,
setBooleanWithInheritance,
removeAttributeById,
removeOwnedLabelByName,
removeOwnedRelationByName,

View File

@@ -1,12 +1,12 @@
import utils from "./utils.js";
import server from "./server.js";
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
import appContext from "../components/app_context.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
import froca from "./froca.js";
import hoistedNoteService from "./hoisted_note.js";
import ws from "./ws.js";
import appContext from "../components/app_context.js";
import { t } from "./i18n.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
import server from "./server.js";
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
import utils from "./utils.js";
import ws from "./ws.js";
// TODO: Deduplicate type with server
interface Response {
@@ -66,7 +66,7 @@ async function moveAfterBranch(branchIdsToMove: string[], afterBranchId: string)
}
}
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string) {
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string, componentId?: string) {
const newParentBranch = froca.getBranch(newParentBranchId);
if (!newParentBranch) {
return;
@@ -86,7 +86,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
continue;
}
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`, undefined, componentId);
if (!resp.success) {
toastService.showError(resp.message);

View File

@@ -23,6 +23,12 @@ export interface RenderOptions {
imageHasZoom?: boolean;
/** If enabled, it will prevent the default behavior in which an empty note would display a list of children. */
noChildrenList?: boolean;
/** If enabled, it will prevent rendering of included notes. */
noIncludedNotes?: boolean;
/** If enabled, it will include archived notes when rendering children list. */
includeArchivedNotes?: boolean;
/** Set of note IDs that have already been seen during rendering to prevent infinite recursion. */
seenNoteIds?: Set<string>;
}
const CODE_MIME_TYPES = new Set(["application/json"]);

View File

@@ -0,0 +1,132 @@
import { trimIndentation } from "@triliumnext/commons";
import { describe, expect, it } from "vitest";
import { buildNote } from "../test/easy-froca";
import renderText from "./content_renderer_text";
describe("Text content renderer", () => {
it("renders included note", async () => {
const contentEl = document.createElement("div");
const includedNote = buildNote({
title: "Included note",
content: "<p>This is the included note.</p>"
});
const note = buildNote({
title: "New note",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="${includedNote.noteId}" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl));
expect(contentEl.querySelectorAll("section.include-note").length).toBe(1);
expect(contentEl.querySelectorAll("section.include-note p").length).toBe(1);
});
it("skips rendering included note", async () => {
const contentEl = document.createElement("div");
const includedNote = buildNote({
title: "Included note",
content: "<p>This is the included note.</p>"
});
const note = buildNote({
title: "New note",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="${includedNote.noteId}" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl), { noIncludedNotes: true });
expect(contentEl.querySelectorAll("section.include-note").length).toBe(0);
});
it("doesn't enter infinite loop on direct recursion", async () => {
const contentEl = document.createElement("div");
const note = buildNote({
title: "New note",
id: "Y7mBwmRjQyb4",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="Y7mBwmRjQyb4" data-box-size="medium">
&nbsp;
</section>
<section class="include-note" data-note-id="Y7mBwmRjQyb4" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl));
expect(contentEl.querySelectorAll("section.include-note").length).toBe(0);
});
it("doesn't enter infinite loop on indirect recursion", async () => {
const contentEl = document.createElement("div");
buildNote({
id: "first",
title: "Included note",
content: trimIndentation`\
<p>This is the included note.</p>
<section class="include-note" data-note-id="second" data-box-size="medium">
&nbsp;
</section>
`
});
const note = buildNote({
id: "second",
title: "New note",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="first" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl));
expect(contentEl.querySelectorAll("section.include-note").length).toBe(1);
});
it("renders children list when note is empty", async () => {
const contentEl = document.createElement("div");
const parentNote = buildNote({
title: "Parent note",
children: [
{ title: "Child note 1" },
{ title: "Child note 2" }
]
});
await renderText(parentNote, $(contentEl));
const items = contentEl.querySelectorAll("a");
expect(items.length).toBe(2);
expect(items[0].textContent).toBe("Child note 1");
expect(items[1].textContent).toBe("Child note 2");
});
it("skips archived notes in children list", async () => {
const contentEl = document.createElement("div");
const parentNote = buildNote({
title: "Parent note",
children: [
{ title: "Child note 1" },
{ title: "Child note 2", "#archived": "" },
{ title: "Child note 3" }
]
});
await renderText(parentNote, $(contentEl));
const items = contentEl.querySelectorAll("a");
expect(items.length).toBe(2);
expect(items[0].textContent).toBe("Child note 1");
expect(items[1].textContent).toBe("Child note 3");
});
});

View File

@@ -15,7 +15,14 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
if (blob && !isHtmlEmpty(blob.content)) {
$renderedContent.append($('<div class="ck-content">').html(blob.content));
await renderIncludedNotes($renderedContent[0]);
const seenNoteIds = options.seenNoteIds ?? new Set<string>();
seenNoteIds.add("noteId" in note ? note.noteId : note.attachmentId);
if (!options.noIncludedNotes) {
await renderIncludedNotes($renderedContent[0], seenNoteIds);
} else {
$renderedContent.find("section.include-note").remove();
}
if ($renderedContent.find("span.math-tex").length > 0) {
renderMathInElement($renderedContent[0], { trust: true });
@@ -35,11 +42,11 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
await formatCodeBlocks($renderedContent);
} else if (note instanceof FNote && !options.noChildrenList) {
await renderChildrenList($renderedContent, note);
await renderChildrenList($renderedContent, note, options.includeArchivedNotes ?? false);
}
}
async function renderIncludedNotes(contentEl: HTMLElement) {
async function renderIncludedNotes(contentEl: HTMLElement, seenNoteIds: Set<string>) {
// TODO: Consider duplicating with server's share/content_renderer.ts.
const includeNoteEls = contentEl.querySelectorAll("section.include-note");
@@ -66,7 +73,15 @@ async function renderIncludedNotes(contentEl: HTMLElement) {
continue;
}
const renderedContent = (await content_renderer.getRenderedContent(note)).$renderedContent;
if (seenNoteIds.has(noteId)) {
console.warn(`Skipping inclusion of ${noteId} to avoid circular reference.`);
includeNoteEl.remove();
continue;
}
const renderedContent = (await content_renderer.getRenderedContent(note, {
seenNoteIds
})).$renderedContent;
includeNoteEl.replaceChildren(...renderedContent);
}
}
@@ -98,7 +113,7 @@ export async function applyInlineMermaid(container: HTMLDivElement) {
}
}
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote, includeArchivedNotes: boolean) {
let childNoteIds = note.getChildNoteIds();
if (!childNoteIds.length) {
@@ -108,14 +123,16 @@ async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: F
$renderedContent.css("padding", "10px");
$renderedContent.addClass("text-with-ellipsis");
// just load the first 10 child notes
if (childNoteIds.length > 10) {
childNoteIds = childNoteIds.slice(0, 10);
}
// just load the first 10 child notes
const childNotes = await froca.getNotes(childNoteIds);
for (const childNote of childNotes) {
if (childNote.isArchived && !includeArchivedNotes) continue;
$renderedContent.append(
await link.createLink(`${note.noteId}/${childNote.noteId}`, {
showTooltip: false,

View File

@@ -1,4 +1,5 @@
import type { AttachmentRow, EtapiTokenRow, NoteType, OptionNames } from "@triliumnext/commons";
import type { AttributeType } from "../entities/fattribute.js";
import type { EntityChange } from "../server_types.js";
@@ -135,7 +136,14 @@ export default class LoadResults {
}
getBranchRows() {
return this.branchRows.map((row) => this.getEntityRow("branches", row.branchId)).filter((branch) => !!branch);
return this.branchRows.map((row) => {
const branch = this.getEntityRow("branches", row.branchId);
if (branch) {
// Merge the componentId from the tracked row with the entity data
return { ...branch, componentId: row.componentId };
}
return null;
}).filter((branch) => !!branch) as BranchRow[];
}
addNoteReordering(parentNoteId: string, componentId: string) {
@@ -153,7 +161,14 @@ export default class LoadResults {
getAttributeRows(componentId = "none"): AttributeRow[] {
return this.attributeRows
.filter((row) => row.componentId !== componentId)
.map((row) => this.getEntityRow("attributes", row.attributeId))
.map((row) => {
const attr = this.getEntityRow("attributes", row.attributeId);
if (attr) {
// Merge the componentId from the tracked row with the entity data
return { ...attr, componentId: row.componentId };
}
return null;
})
.filter((attr) => !!attr) as AttributeRow[];
}

View File

@@ -69,24 +69,6 @@ export function buildNote(noteDef: NoteDefinition) {
});
note.getBlob = async () => blob;
// Manage children.
if (noteDef.children) {
for (const childDef of noteDef.children) {
const childNote = buildNote(childDef);
const branchId = `${note.noteId}_${childNote.noteId}`;
const branch = new FBranch(froca, {
branchId,
noteId: childNote.noteId,
parentNoteId: note.noteId,
notePosition: childNotePosition,
fromSearchNote: false
});
froca.branches[branchId] = branch;
note.addChild(childNote.noteId, branchId, false);
childNotePosition += 10;
}
}
let position = 0;
for (const [ key, value ] of Object.entries(noteDef)) {
const attributeId = utils.randomString(12);
@@ -136,5 +118,25 @@ export function buildNote(noteDef: NoteDefinition) {
}
noteAttributeCache.attributes[note.noteId].push(attribute);
}
// Manage children.
if (noteDef.children) {
for (const childDef of noteDef.children) {
const childNote = buildNote(childDef);
const branchId = `${note.noteId}_${childNote.noteId}`;
const branch = new FBranch(froca, {
branchId,
noteId: childNote.noteId,
parentNoteId: note.noteId,
notePosition: childNotePosition,
fromSearchNote: false
});
froca.branches[branchId] = branch;
note.addChild(childNote.noteId, branchId, false);
childNote.addParent(note.noteId, branchId, false);
childNotePosition += 10;
}
}
return note;
}

View File

@@ -25,7 +25,8 @@
},
"widget-list-error": {
"title": "Abruf der Liste von Widgets vom Server ist fehlgeschlagen"
}
},
"open-script-note": "Script-Notiz öffnen"
},
"add_link": {
"add_link": "Link hinzufügen",
@@ -208,7 +209,8 @@
"info": {
"modalTitle": "Infonachricht",
"closeButton": "Schließen",
"okButton": "OK"
"okButton": "OK",
"copy_to_clipboard": "In die Zwischenablage kopieren"
},
"jump_to_note": {
"search_button": "Suche im Volltext",
@@ -695,7 +697,9 @@
"export_as_image": "Als Bild exportieren",
"export_as_image_png": "PNG (Raster)",
"export_as_image_svg": "SVG (Vektor)",
"note_map": "Notizen Karte"
"note_map": "Notizen Karte",
"view_revisions": "Notizrevisionen",
"advanced": "Erweitert"
},
"onclick_button": {
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"

View File

@@ -800,7 +800,8 @@
"geo-map": "Geo Map",
"board": "Board",
"presentation": "Presentation",
"include_archived_notes": "Show archived notes"
"include_archived_notes": "Show archived notes",
"hide_child_notes": "Hide child notes in tree"
},
"edited_notes": {
"no_edited_notes_found": "No edited notes on this day yet...",
@@ -1643,6 +1644,7 @@
"tree-context-menu": {
"open-in-a-new-tab": "Open in a new tab",
"open-in-a-new-split": "Open in a new split",
"open-in-a-new-window": "Open in a new window",
"insert-note-after": "Insert note after",
"insert-child-note": "Insert child note",
"archive": "Archive",
@@ -1655,6 +1657,8 @@
"advanced": "Advanced",
"expand-subtree": "Expand subtree",
"collapse-subtree": "Collapse subtree",
"hide-subtree": "Hide subtree",
"show-subtree": "Show subtree",
"sort-by": "Sort by...",
"recent-changes-in-subtree": "Recent changes in subtree",
"convert-to-attachment": "Convert to attachment",
@@ -1772,7 +1776,12 @@
"clone-indicator-tooltip": "This note has {{- count}} parents: {{- parents}}",
"clone-indicator-tooltip-single": "This note is cloned (1 additional parent: {{- parent}})",
"shared-indicator-tooltip": "This note is shared publicly",
"shared-indicator-tooltip-with-url": "This note is shared publicly at: {{- url}}"
"shared-indicator-tooltip-with-url": "This note is shared publicly at: {{- url}}",
"subtree-hidden-tooltip_one": "{{count}} child note that is hidden from the tree",
"subtree-hidden-tooltip_other": "{{count}} child notes that are hidden from the tree",
"subtree-hidden-moved-title": "Added to {{title}}",
"subtree-hidden-moved-description-collection": "This collection hides its child notes in the tree.",
"subtree-hidden-moved-description-other": "Child notes are hidden in the tree for this note."
},
"title_bar_buttons": {
"window-on-top": "Keep Window on Top"

View File

@@ -21,7 +21,13 @@
},
"bundle-error": {
"title": "Hubo un fallo al cargar un script personalizado",
"message": "El script de la nota con ID \"{{id}}\", titulado \"{{title}}\" no pudo ser ejecutado debido a:\n\n{{message}}"
"message": "El script no pudo ser ejecutado debido a:\n\n{{message}}"
},
"widget-list-error": {
"title": "Hubo un fallo al obtener la lista de widgets del servidor"
},
"widget-render-error": {
"title": "Hubo un fallo al renderizar un widget personalizado de React"
}
},
"add_link": {
@@ -162,7 +168,8 @@
"other": "Otro",
"quickSearch": "centrarse en la entrada de búsqueda rápida",
"inPageSearch": "búsqueda en la página",
"title": "Hoja de ayuda"
"title": "Hoja de ayuda",
"editShortcuts": "Editar atajos de teclado"
},
"import": {
"importIntoNote": "Importar a nota",
@@ -690,7 +697,7 @@
"convert_into_attachment_successful": "La nota '{{title}}' ha sido convertida a un archivo adjunto.",
"convert_into_attachment_prompt": "¿Está seguro que desea convertir la nota '{{title}}' en un archivo adjunto de la nota padre?",
"print_pdf": "Exportar como PDF...",
"open_note_on_server": "Abrir nota en el servidor"
"open_note_on_server": "Abrir nota en servidor"
},
"onclick_button": {
"no_click_handler": "El widget de botón '{{componentId}}' no tiene un controlador de clics definido"
@@ -736,7 +743,7 @@
"zpetne_odkazy": {
"relation": "relación",
"backlink_one": "{{count}} Vínculo de retroceso",
"backlink_many": "",
"backlink_many": "{{count}} Vínculos de retroceso",
"backlink_other": "{{count}} vínculos de retroceso"
},
"mobile_detail_menu": {
@@ -749,7 +756,10 @@
"note_icon": {
"change_note_icon": "Cambiar icono de nota",
"search": "Búsqueda:",
"reset-default": "Restablecer a icono por defecto"
"reset-default": "Restablecer a icono por defecto",
"search_placeholder_one": "Buscar {{number}} icono a través de {{count}} paquetes",
"search_placeholder_many": "Buscar {{number}} iconos a través de {{count}} paquetes",
"search_placeholder_other": "Buscar {{number}} iconos a través de {{count}} paquetes"
},
"basic_properties": {
"note_type": "Tipo de nota",
@@ -789,7 +799,7 @@
"file_type": "Tipo de archivo",
"file_size": "Tamaño del archivo",
"download": "Descargar",
"open": "Abrir",
"open": "Abrir externamente",
"upload_new_revision": "Subir nueva revisión",
"upload_success": "Se ha subido una nueva revisión de archivo.",
"upload_failed": "Error al cargar una nueva revisión de archivo.",
@@ -1302,11 +1312,11 @@
"code_mime_types": {
"title": "Tipos MIME disponibles en el menú desplegable",
"tooltip_syntax_highlighting": "Resaltado de sintaxis",
"tooltip_code_block_syntax": "Bloques de código en notas de texto",
"tooltip_code_note_syntax": "Notas de código"
"tooltip_code_block_syntax": "Bloques de Código en notas de Texto",
"tooltip_code_note_syntax": "Notas de Código"
},
"vim_key_bindings": {
"use_vim_keybindings_in_code_notes": "Atajos de teclas de Vim",
"use_vim_keybindings_in_code_notes": "Combinaciones de teclas Vim",
"enable_vim_keybindings": "Habilitar los atajos de teclas de Vim en la notas de código (no es modo ex)"
},
"wrap_lines": {
@@ -1571,7 +1581,7 @@
"will_be_deleted_in": "Este archivo adjunto se eliminará automáticamente en {{time}}",
"will_be_deleted_soon": "Este archivo adjunto se eliminará automáticamente pronto",
"deletion_reason": ", porque el archivo adjunto no está vinculado en el contenido de la nota. Para evitar la eliminación, vuelva a agregar el enlace del archivo adjunto al contenido o convierta el archivo adjunto en una nota.",
"role_and_size": "Rol: {{role}}, Tamaño: {{size}}",
"role_and_size": "Rol: {{role}}, tamaño: {{size}}, MIME: {{- mimeType}}",
"link_copied": "Enlace del archivo adjunto copiado al portapapeles.",
"unrecognized_role": "Rol de archivo adjunto no reconocido '{{role}}'."
},
@@ -1622,7 +1632,7 @@
"import-into-note": "Importar a nota",
"apply-bulk-actions": "Aplicar acciones en lote",
"converted-to-attachments": "{{count}} notas han sido convertidas en archivos adjuntos.",
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres?",
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres? Esta operación solo aplica a notas de Imagen, otras notas serán omitidas.",
"open-in-popup": "Edición rápida",
"archive": "Archivar",
"unarchive": "Desarchivar"
@@ -1717,7 +1727,10 @@
"note_detail": {
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'",
"printing": "Impresión en curso...",
"printing_pdf": "Exportando a PDF en curso.."
"printing_pdf": "Exportando a PDF en curso..",
"print_report_collection_content_one": "{{count}} nota en la colección no se puede imprimir porque no son compatibles o está protegida.",
"print_report_collection_content_many": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas."
},
"note_title": {
"placeholder": "escriba el título de la nota aquí..."
@@ -1929,7 +1942,7 @@
"unknown_widget": "Widget desconocido para \"{{id}}\"."
},
"note_language": {
"not_set": "No establecido",
"not_set": "Idioma no establecido",
"configure-languages": "Configurar idiomas..."
},
"content_language": {
@@ -1968,7 +1981,7 @@
"hide-weekends": "Ocultar fines de semana",
"show-scale": "Mostrar escala",
"display-week-numbers": "Mostrar números de semana",
"map-style": "Estilo de mapa:",
"map-style": "Estilo de mapa",
"max-nesting-depth": "Máxima profundidad de anidamiento:",
"vector_light": "Vector (claro)",
"vector_dark": "Vector (oscuro)",
@@ -2097,5 +2110,36 @@
"clear-color": "Borrar color de nota",
"set-color": "Asignar color de nota",
"set-custom-color": "Asignar color de nota personalizado"
},
"status_bar": {
"backlinks_one": "{{count}} vínculo de retroceso",
"backlinks_many": "{{count}} vínculos de retroceso",
"backlinks_other": "{{count}} vínculos de retroceso",
"backlinks_title_one": "Ver vínculo de retroceso",
"backlinks_title_many": "Ver vínculos de retroceso",
"backlinks_title_other": "Ver vínculos de retroceso",
"attachments_one": "{{count}} adjunto",
"attachments_many": "{{count}} adjuntos",
"attachments_other": "{{count}} adjuntos",
"attachments_title_one": "Ver adjunto en una nueva pestaña",
"attachments_title_many": "Ver adjuntos en una nueva pestaña",
"attachments_title_other": "Ver adjuntos en una nueva pestaña",
"attributes_one": "{{count}} atributo",
"attributes_many": "{{count}} atributos",
"attributes_other": "{{count}} atributos",
"note_paths_one": "{{count}} ruta",
"note_paths_many": "{{count}} rutas",
"note_paths_other": "{{count}} rutas"
},
"pdf": {
"attachments_one": "{{count}} adjunto",
"attachments_many": "{{count}} adjuntos",
"attachments_other": "{{count}} adjuntos",
"layers_one": "{{count}} capa",
"layers_many": "{{count}} capas",
"layers_other": "{{count}} capas",
"pages_one": "{{count}} página",
"pages_many": "{{count}} páginas",
"pages_other": "{{count}} páginas"
}
}

View File

@@ -21,7 +21,7 @@
},
"bundle-error": {
"title": "Echec du chargement d'un script personnalisé",
"message": "Le script de la note avec l'ID \"{{id}}\", intitulé \"{{title}}\" n'a pas pu être exécuté à cause de\n\n{{message}}"
"message": "Le script n'a pas pu être exécuté à cause de\n\n{{message}}"
},
"widget-list-error": {
"title": "Impossible d'obtenir la liste des widgets depuis le serveur"

View File

@@ -31,5 +31,17 @@
},
"add_link": {
"note": "नोट"
},
"bulk_actions": {
"other": "अन्य"
},
"clone_to": {
"search_for_note_by_its_name": "नोट क नाम से नोट खोजें"
},
"confirm": {
"also_delete_note": "नोट भी डिलीट करें"
},
"delete_notes": {
"delete_notes_preview": "नोट्स प्रिव्यू डिलीट करें"
}
}

View File

@@ -21,7 +21,13 @@
},
"bundle-error": {
"title": "Nem sikerült betölteni az egyéni szkriptet",
"message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}"
"message": "A skript nem hajtható végre a következő ok miatt:\n\n{{message}}"
},
"widget-list-error": {
"title": "A Widget-ek letöltése sikertelen volt"
},
"widget-render-error": {
"title": "Nem sikerült renderelni a React widget-et"
}
},
"add_link": {

View File

@@ -1895,7 +1895,11 @@
"create-child-note": "Crea nota figlio",
"unhoist": "Sganciare",
"toggle-sidebar": "Attiva/disattiva la barra laterale",
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione."
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione.",
"clone-indicator-tooltip": "Questa nota ha {{- count}} genitori: {{- parents}}",
"clone-indicator-tooltip-single": "Questa nota è stata clonata (1 genitore aggiuntivo: {{- parent}})",
"shared-indicator-tooltip": "Questa nota è condivisa pubblicamente",
"shared-indicator-tooltip-with-url": "Questa nota è condivisa pubblicamente all'indirizzo: {{- url}}"
},
"title_bar_buttons": {
"window-on-top": "Mantieni la finestra in primo piano"
@@ -2200,7 +2204,14 @@
"execute_sql_description": "Questa nota è una nota SQL. Clicca per eseguire la query SQL.",
"shared_copy_to_clipboard": "Copia link negli appunti",
"shared_open_in_browser": "Apri il link nel browser",
"shared_unshare": "Rimuovi condivisione"
"shared_unshare": "Rimuovi condivisione",
"save_status_saved": "Salvato",
"save_status_saving": "Salvataggio in corso...",
"save_status_unsaved": "Non salvato",
"save_status_error": "Salvataggio non riuscito",
"save_status_saving_tooltip": "Le modifiche sono state salvate.",
"save_status_unsaved_tooltip": "Ci sono modifiche non salvate. Verranno salvate automaticamente tra un attimo.",
"save_status_error_tooltip": "Si è verificato un errore durante il salvataggio della nota. Se possibile, prova a copiare il contenuto della nota altrove e a ricaricare l'applicazione."
},
"breadcrumb": {
"workspace_badge": "Area di lavoro",
@@ -2243,5 +2254,18 @@
"empty_button": "Nascondi il pannello",
"toggle": "Attiva/disattiva pannello destro",
"custom_widget_go_to_source": "Vai al codice sorgente"
},
"pdf": {
"attachments_one": "{{count}} allegato",
"attachments_many": "{{count}} allegati",
"attachments_other": "{{count}} allegati",
"layers_one": "{{count}} livello",
"layers_many": "{{count}} livelli",
"layers_other": "{{count}} livelli",
"pages_one": "{{count}} pagina",
"pages_many": "{{count}} pagine",
"pages_other": "{{count}} pagine",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Caricamento in corso..."
}
}

View File

@@ -69,7 +69,7 @@ declare namespace Fancytree {
debug(msg: any): void;
/** Expand (or collapse) all parent nodes. */
expandAll(flag?: boolean, options?: Object): void;
expandAll(flag?: boolean, options?: object): void;
/** [ext-filter] Dimm or hide whole branches.
* @returns {integer} count
@@ -221,6 +221,7 @@ declare namespace Fancytree {
branchId: string;
isProtected: boolean;
noteType: NoteType;
subtreeHidden: boolean;
}
interface FancytreeNewNode extends FancytreeNodeData {
@@ -369,7 +370,7 @@ declare namespace Fancytree {
* @param mode 'before', 'after', or 'child' (default='child')
* @param init NodeData (or simple title string)
*/
editCreateNode(mode?: string, init?: Object): void;
editCreateNode(mode?: string, init?: object): void;
/** [ext-edit] Stop inline editing.
*
@@ -526,7 +527,7 @@ declare namespace Fancytree {
*
* @param opts passed to `setExpanded()`. Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
*/
makeVisible(opts?: Object): JQueryPromise<any>;
makeVisible(opts?: object): JQueryPromise<any>;
/** Move this node to targetNode.
*
@@ -589,25 +590,25 @@ declare namespace Fancytree {
* @param effects animation options.
* @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane.
*/
scrollIntoView(effects?: boolean, options?: Object): JQueryPromise<any>;
scrollIntoView(effects?: boolean, options?: object): JQueryPromise<any>;
/**
* @param effects animation options.
* @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane.
*/
scrollIntoView(effects?: Object, options?: Object): JQueryPromise<any>;
scrollIntoView(effects?: object, options?: object): JQueryPromise<any>;
/**
* @param flag pass false to deactivate
* @param opts additional options. Defaults to {noEvents: false}
*/
setActive(flag?: boolean, opts?: Object): JQueryPromise<any>;
setActive(flag?: boolean, opts?: object): JQueryPromise<any>;
/**
* @param flag pass false to collapse.
* @param opts additional options. Defaults to {noAnimation:false, noEvents:false}
*/
setExpanded(flag?: boolean, opts?: Object): JQueryPromise<any>;
setExpanded(flag?: boolean, opts?: object): JQueryPromise<any>;
/**
* Set keyboard focus to this node.
@@ -1109,7 +1110,7 @@ declare namespace Fancytree {
/** class names added to the node markup (separate with space) */
extraClasses?: string | undefined;
/** all properties from will be copied to `node.data` */
data?: Object | undefined;
data?: object | undefined;
/** Will be added as title attribute of the node's icon span,thus enabling a tooltip. */
iconTooltip?: string | undefined;
@@ -1160,7 +1161,7 @@ declare namespace Fancytree {
escapeHtml(s: string): string;
getEventTarget(event: Event): Object;
getEventTarget(event: Event): object;
getEventTargetType(event: Event): string;
@@ -1179,7 +1180,7 @@ declare namespace Fancytree {
parseHtml($ul: JQuery): NodeData[];
/** Add Fancytree extension definition to the list of globally available extensions. */
registerExtension(definition: Object): void;
registerExtension(definition: object): void;
unescapeHtml(s: string): string;

View File

@@ -215,7 +215,7 @@ export default function NoteDetail() {
return (
<div
ref={containerRef}
class={`note-detail ${isFullHeight ? "full-height" : ""}`}
class={`component note-detail ${isFullHeight ? "full-height" : ""}`}
>
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
return <NoteDetailWrapper

View File

@@ -44,6 +44,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const { pageNotes, ...pagination } = usePagination(note, noteIds);
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
return (
<div class="note-list grid-view">
@@ -52,7 +53,7 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
<div class="note-list-container use-tn-links">
{pageNotes?.map(childNote => (
<GridNoteCard note={childNote} parentNote={note} highlightedTokens={highlightedTokens} />
<GridNoteCard note={childNote} parentNote={note} highlightedTokens={highlightedTokens} includeArchived={includeArchived} />
))}
</div>
@@ -94,14 +95,16 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan
</h5>
{isExpanded && <>
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList />
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList includeArchivedNotes={includeArchived} />
<NoteChildren note={note} parentNote={parentNote} highlightedTokens={highlightedTokens} currentLevel={currentLevel} expandDepth={expandDepth} includeArchived={includeArchived} />
</>}
</div>
);
}
function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
function GridNoteCard({ note, parentNote, highlightedTokens, includeArchived }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined, includeArchived: boolean }) {
const titleRef = useRef<HTMLSpanElement>(null);
const [ noteTitle, setNoteTitle ] = useState<string>();
const notePath = getNotePath(parentNote, note);
return (
@@ -120,6 +123,7 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
note={note}
trim
highlightedTokens={highlightedTokens}
includeArchivedNotes={includeArchived}
/>
</div>
);
@@ -136,14 +140,22 @@ function NoteAttributes({ note }: { note: FNote }) {
return <span className="note-list-attributes" ref={ref} />;
}
function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note: FNote, trim?: boolean, noChildrenList?: boolean, highlightedTokens: string[] | null | undefined }) {
function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: {
note: FNote;
trim?: boolean;
noChildrenList?: boolean;
highlightedTokens: string[] | null | undefined;
includeArchivedNotes: boolean;
}) {
const contentRef = useRef<HTMLDivElement>(null);
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
useEffect(() => {
content_renderer.getRenderedContent(note, {
trim,
noChildrenList
noChildrenList,
noIncludedNotes: true,
includeArchivedNotes
})
.then(({ $renderedContent, type }) => {
if (!contentRef.current) return;

View File

@@ -1,17 +1,18 @@
import { useCallback, useLayoutEffect, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import froca from "../../services/froca";
import { isDesktop, isMobile } from "../../services/utils";
import CalendarWidget from "./CalendarWidget";
import SpacerWidget from "./SpacerWidget";
import BookmarkButtons from "./BookmarkButtons";
import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget";
import SyncStatus from "./SyncStatus";
import HistoryNavigationButton from "./HistoryNavigation";
import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions";
import { useTriliumEvent } from "../react/hooks";
import { onWheelHorizontalScroll } from "../widget_utils";
import BookmarkButtons from "./BookmarkButtons";
import CalendarWidget from "./CalendarWidget";
import HistoryNavigationButton from "./HistoryNavigation";
import { LaunchBarContext } from "./launch_bar_widgets";
import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions";
import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget";
import SpacerWidget from "./SpacerWidget";
import SyncStatus from "./SyncStatus";
export default function LauncherContainer({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
const childNotes = useLauncherChildNotes();
@@ -34,18 +35,19 @@ export default function LauncherContainer({ isHorizontalLayout }: { isHorizontal
}}>
{childNotes?.map(childNote => {
if (childNote.type !== "launcher") {
throw new Error(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`);
console.warn(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`);
return false;
}
if (!isDesktop() && childNote.isLabelTruthy("desktopOnly")) {
return false;
}
return <Launcher key={childNote.noteId} note={childNote} isHorizontalLayout={isHorizontalLayout} />
return <Launcher key={childNote.noteId} note={childNote} isHorizontalLayout={isHorizontalLayout} />;
})}
</LaunchBarContext.Provider>
</div>
)
);
}
function Launcher({ note, isHorizontalLayout }: { note: FNote, isHorizontalLayout: boolean }) {
@@ -72,7 +74,7 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
const builtinWidget = note.getLabelValue("builtinWidget");
switch (builtinWidget) {
case "calendar":
return <CalendarWidget launcherNote={note} />
return <CalendarWidget launcherNote={note} />;
case "spacer":
// || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
@@ -86,15 +88,15 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
case "syncStatus":
return <SyncStatus />;
case "backInHistoryButton":
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />;
case "forwardInHistoryButton":
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />;
case "todayInJournal":
return <TodayLauncher launcherNote={note} />
return <TodayLauncher launcherNote={note} />;
case "quickSearch":
return <QuickSearchLauncherWidget />
return <QuickSearchLauncherWidget />;
case "aiChatLauncher":
return <AiChatButton launcherNote={note} />
return <AiChatButton launcherNote={note} />;
default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
}

View File

@@ -5,7 +5,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx";
import { type ComponentChildren, RefObject } from "preact";
import { createPortal } from "preact/compat";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
@@ -338,15 +338,19 @@ interface AttributesProps extends StatusBarContext {
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
const [ count, setCount ] = useState(note.attributes.length);
const refreshCount = useCallback((note: FNote) => {
return note.getAttributes().filter(a => !a.isAutoLink).length;
}, []);
// React to note changes.
useEffect(() => {
setCount(note.getAttributes().filter(a => !a.isAutoLink).length);
}, [ note ]);
setCount(refreshCount(note));
}, [ note, refreshCount ]);
// React to changes in count.
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
setCount(note.attributes.length);
setCount(refreshCount(note));
}
}));

View File

@@ -82,6 +82,13 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
))}
{properties.length > 0 && <FormDropdownDivider />}
<ViewProperty note={note} property={{
type: "checkbox",
icon: "bx bx-hide",
label: t("book_properties.hide_child_notes"),
bindToLabel: "subtreeHidden"
} as CheckBoxProperty} />
<ViewProperty note={note} property={{
type: "checkbox",
icon: "bx bx-archive",

View File

@@ -0,0 +1,20 @@
#left-pane .tree-wrapper {
.note-indicator-icon.subtree-hidden-badge {
font-family: inherit !important;
margin-inline: 0.5em;
margin-top: 0.3em;
background: var(--left-pane-item-action-button-background);
color: var(--left-pane-item-action-button-color);
padding: 0.1em 0.6em;
border-radius: 0.5em;
font-size: 0.7rem;
font-weight: normal;
float: right;
vertical-align: middle;
}
.spotlighted-node {
opacity: 0.8;
font-style: italic;
}
}

View File

@@ -3,13 +3,13 @@ import "jquery.fancytree/dist/modules/jquery.fancytree.dnd5.js";
import "jquery.fancytree/dist/modules/jquery.fancytree.clones.js";
import "jquery.fancytree/dist/modules/jquery.fancytree.filter.js";
import "../stylesheets/tree.css";
import "./note_tree.css";
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
import type { SetNoteOpts } from "../components/note_context.js";
import type { TouchBarItem } from "../components/touch_bar.js";
import type FBranch from "../entities/fbranch.js";
import type FNote from "../entities/fnote.js";
import type { NoteType } from "../entities/fnote.js";
import contextMenu from "../menus/context_menu.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
import branchService from "../services/branches.js";
@@ -153,7 +153,7 @@ const TPL = /*html*/`
const MAX_SEARCH_RESULTS_IN_TREE = 100;
// this has to be hanged on the actual elements to effectively intercept and stop click event
const cancelClickPropagation: (e: JQuery.ClickEvent) => void = (e) => e.stopPropagation();
const cancelClickPropagation: (e: Event) => void = (e) => e.stopPropagation();
// TODO: Fix once we remove Node.js API from public
type Timeout = NodeJS.Timeout | string | number | undefined;
@@ -190,6 +190,9 @@ export interface DragData {
export const TREE_CLIPBOARD_TYPE = "application/x-fancytree-node";
/** Entity changes below the given threshold will be processed without batching to avoid performance degradation. */
const BATCH_UPDATE_THRESHOLD = 10;
export default class NoteTreeWidget extends NoteContextAwareWidget {
private $tree!: JQuery<HTMLElement>;
private $treeActions!: JQuery<HTMLElement>;
@@ -201,6 +204,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
private treeName: "main";
private autoCollapseTimeoutId?: Timeout;
private lastFilteredHoistedNotePath?: string | null;
private spotlightedNotePath?: string | null;
private spotlightedNode: Fancytree.FancytreeNode | null = null;
private tree!: Fancytree.Fancytree;
constructor() {
@@ -353,6 +358,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.$tree.fancytree({
titlesTabbable: true,
keyboard: true,
toggleEffect: options.is("motionEnabled") ? undefined : false,
extensions: ["dnd5", "clones", "filter"],
source: treeData,
scrollOfs: {
@@ -552,7 +558,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} else if (data.hitMode === "after") {
branchService.moveAfterBranch(selectedBranchIds, node.data.branchId);
} else if (data.hitMode === "over") {
branchService.moveToParentNote(selectedBranchIds, node.data.branchId);
branchService.moveToParentNote(selectedBranchIds, node.data.branchId, this.componentId);
} else {
throw new Error(`Unknown hitMode '${data.hitMode}'`);
}
@@ -598,102 +604,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
clones: {
highlightActiveClones: true
},
async enhanceTitle (
event: Event,
data: {
node: Fancytree.FancytreeNode;
noteId: string;
}
) {
const node = data.node;
if (!node.data.noteId) {
// if there's "non-note" node, then don't enhance
// this can happen for e.g. "Load error!" node
return;
}
const note = await froca.getNote(node.data.noteId, true);
if (!note) {
return;
}
const activeNoteContext = appContext.tabManager.getActiveContext();
const $span = $(node.span);
$span.find(".tree-item-button").remove();
$span.find(".note-indicator-icon").remove();
const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root";
if (note.hasLabel("workspace") && !isHoistedNote) {
const $enterWorkspaceButton = $(`<span class="tree-item-button tn-icon enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($enterWorkspaceButton);
}
if (note.type === "search") {
const $refreshSearchButton = $(`<span class="tree-item-button tn-icon refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($refreshSearchButton);
}
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
if (!["search", "launcher"].includes(note.type)
&& !note.isOptions()
&& !note.isLaunchBarConfig()
&& !note.noteId.startsWith("_help")
) {
const $createChildNoteButton = $(`<span class="tree-item-button tn-icon add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($createChildNoteButton);
}
if (isHoistedNote) {
const $unhoistButton = $(`<span class="tree-item-button tn-icon unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
$span.append($unhoistButton);
}
// Add clone indicator with tooltip if note has multiple parents
const parentNotes = note.getParentNotes();
const realParents = parentNotes.filter(
(parent) => !["_share", "_lbBookmarks"].includes(parent.noteId) && parent.type !== "search"
);
if (realParents.length > 1) {
const parentTitles = realParents.map((p) => p.title).join(", ");
const tooltipText = realParents.length === 2
? t("note_tree.clone-indicator-tooltip-single", { parent: realParents[1].title })
: t("note_tree.clone-indicator-tooltip", { count: realParents.length, parents: parentTitles });
const $cloneIndicator = $(`<span class="note-indicator-icon clone-indicator"></span>`);
$cloneIndicator.attr("title", tooltipText);
$span.find(".fancytree-title").append($cloneIndicator);
}
// Add shared indicator with tooltip if note is shared
if (note.isShared()) {
const shareId = note.getOwnedLabelValue("shareAlias") || note.noteId;
const shareUrl = `${location.origin}${location.pathname}share/${shareId}`;
const tooltipText = t("note_tree.shared-indicator-tooltip-with-url", { url: shareUrl });
const $sharedIndicator = $(`<span class="note-indicator-icon shared-indicator"></span>`);
$sharedIndicator.attr("title", tooltipText);
$span.find(".fancytree-title").append($sharedIndicator);
}
},
enhanceTitle: buildEnhanceTitle(),
// this is done to automatically lazy load all expanded notes after tree load
loadChildren: (event, data) => {
data.node.visit((subNode) => {
@@ -803,6 +714,23 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
}
if (parentNote.isLabelTruthy("subtreeHidden")) {
// If we have a spotlighted note path, show only the child that leads to it
if (this.spotlightedNotePath) {
const spotlightPathSegments = this.spotlightedNotePath.split('/');
const parentIndex = spotlightPathSegments.indexOf(parentNote.noteId);
if (parentIndex >= 0 && parentIndex < spotlightPathSegments.length - 1) {
const nextNoteIdInPath = spotlightPathSegments[parentIndex + 1];
childBranches = childBranches.filter(branch => branch.noteId === nextNoteIdInPath);
} else {
childBranches = [];
}
} else {
childBranches = [];
}
}
for (const branch of childBranches) {
if (hideArchivedNotes) {
const note = branch.getNoteFromCache();
@@ -874,6 +802,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
lazy: true,
folder: isFolder,
expanded: !!branch.isExpanded && note.type !== "search",
subtreeHidden: note.isLabelTruthy("subtreeHidden"),
key: utils.randomString(12) // this should prevent some "duplicate key" errors
};
@@ -932,6 +861,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
extraClasses.push(...["tinted", colorClass]);
}
if (this.spotlightedNotePath && this.spotlightedNotePath.endsWith(`/${note.noteId}`)) {
extraClasses.push("spotlighted-node");
}
return extraClasses.join(" ");
}
@@ -1082,18 +1015,43 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
foundChildNode = this.findChildNode(parentNode, childNoteId);
if (!foundChildNode) {
if (logErrors) {
// besides real errors, this can be also caused by hiding of e.g. included images
// these are real notes with real notePath, user can display them in a detail,
// but they don't have a node in the tree
const childNote = await froca.getNote(childNoteId);
const childNote = await froca.getNote(childNoteId);
if (childNote?.type === "image") return;
if (!childNote || childNote.type !== "image") {
ws.logError(
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
);
// The child note can be part of a note with #subtreeHidden, case in which we need to "spotlight" it.
const parentNote = froca.getNoteFromCache(parentNode.data.noteId);
if (parentNote?.isLabelTruthy("subtreeHidden")) {
// Enable spotlight mode and reload the parent to show only the path to this note
this.spotlightedNotePath = notePath;
await parentNode.load(true);
// Try to find the child again after reload
foundChildNode = this.findChildNode(parentNode, childNoteId);
this.spotlightedNode = foundChildNode ?? null;
if (!foundChildNode) {
if (logErrors || !childNote) {
ws.logError(
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
);
return;
}
return;
}
parentNode = foundChildNode;
continue;
}
// besides real errors, this can be also caused by hiding of e.g. included images
// these are real notes with real notePath, user can display them in a detail,
// but they don't have a node in the tree
if (logErrors || !childNote) {
ws.logError(
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
);
return;
}
return;
@@ -1108,7 +1066,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
findChildNode(parentNode: Fancytree.FancytreeNode, childNoteId: string) {
return parentNode.getChildren().find((childNode) => childNode.data.noteId === childNoteId);
return parentNode.getChildren()?.find((childNode) => childNode.data.noteId === childNoteId);
}
async expandToNote(notePath: string, logErrors = true) {
@@ -1149,12 +1107,20 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
(!treeService.isNotePathInHiddenSubtree(this.noteContext.notePath) || (await hoistedNoteService.isHoistedInHiddenSubtree())) &&
(await this.getNodeFromPath(this.noteContext.notePath));
if (this.spotlightedNode && newActiveNode !== this.spotlightedNode) {
// Can get removed when switching to another note in a spotlighted subtree.
if (this.spotlightedNode.parent) {
this.spotlightedNode.remove();
}
this.spotlightedNode = null;
this.spotlightedNotePath = null;
}
if (newActiveNode !== oldActiveNode) {
let oldActiveNodeFocused = false;
if (oldActiveNode) {
oldActiveNodeFocused = oldActiveNode.hasFocus();
oldActiveNode.setActive(false);
oldActiveNode.setFocus(false);
}
@@ -1257,10 +1223,18 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const { movedActiveNode, parentsOfAddedNodes } = await this.#processBranchRows(branchRows, refreshCtx);
for (const noteId of loadResults.getNoteIds()) {
const contentReloaded = loadResults.isNoteContentReloaded(noteId);
if (contentReloaded && !loadResults.isNoteReloaded(noteId, contentReloaded.componentId)) {
// Only the note content was reloaded, not the note itself. This would cause a redundant update on every few seconds while editing a note.
continue;
}
refreshCtx.noteIdsToUpdate.add(noteId);
}
await this.#executeTreeUpdates(refreshCtx, loadResults);
if (refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0) {
await this.#executeTreeUpdates(refreshCtx, loadResults);
}
await this.#setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes);
@@ -1280,7 +1254,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} else {
refreshCtx.noteIdsToUpdate.add(attrRow.noteId);
}
} else if (attrRow.type === "label" && attrRow.name === "archived" && attrRow.noteId) {
} else if (attrRow.type === "label" && (attrRow.name === "archived" || attrRow.name === "subtreeHidden") && attrRow.noteId) {
const note = froca.getNoteFromCache(attrRow.noteId);
if (note) {
@@ -1365,18 +1339,34 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} else if (frocaBranch) {
// make sure it's loaded
// we're forcing lazy since it's not clear if the whole required subtree is in froca
const newNode = this.prepareNode(frocaBranch, true);
if (newNode) {
parentNode.addChildren([newNode]);
}
if (!parentNode.data.subtreeHidden) {
const newNode = this.prepareNode(frocaBranch, true);
if (newNode) {
parentNode.addChildren([newNode]);
}
if (frocaBranch?.isExpanded && note && note.hasChildren()) {
refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
}
if (frocaBranch?.isExpanded && note && note.hasChildren()) {
refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
}
this.sortChildren(parentNode);
this.sortChildren(parentNode);
} else if (branchRow.componentId === this.componentId) {
// Display the toast and focus to parent note only if we know for sure that the operation comes from the tree.
const parentNote = froca.getNoteFromCache(parentNode.data.noteId || "");
toastService.showPersistent({
id: `subtree-hidden-moved`,
title: t("note_tree.subtree-hidden-moved-title", { title: parentNote?.title }),
message: parentNote?.type === "book"
? t("note_tree.subtree-hidden-moved-description-collection")
: t("note_tree.subtree-hidden-moved-description-other"),
icon: "bx bx-hide",
timeout: 5_000,
});
parentNode.setActive(true);
}
// this might be a first child which would force an icon change
// also update the count if the subtree is hidden.
if (branchRow.parentNoteId) {
refreshCtx.noteIdsToUpdate.add(branchRow.parentNoteId);
}
@@ -1392,7 +1382,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
async #executeTreeUpdates(refreshCtx: RefreshContext, loadResults: LoadResults) {
await this.batchUpdate(async () => {
const performUpdates = async () => {
for (const noteId of refreshCtx.noteIdsToReload) {
for (const node of this.getNodesByNoteId(noteId)) {
await node.load(true);
@@ -1408,7 +1398,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
}
}
});
};
if (refreshCtx.noteIdsToReload.size + refreshCtx.noteIdsToUpdate.size >= BATCH_UPDATE_THRESHOLD) {
/**
* Batch updates are used for large number of updates to prevent multiple re-renders, however in the context of small updates (such as changing a note title)
* it can cause up to 400ms of delay for ~8k notes which is not acceptable. Therefore we use batching only for larger number of updates.
* Without batching, the updates would take a couple of milliseconds.
* We still keep the batching for potential cases where there are many updates, for example in a sync.
*/
await this.batchUpdate(performUpdates);
} else {
await performUpdates();
}
// for some reason, node update cannot be in the batchUpdate() block (node is not re-rendered)
for (const noteId of refreshCtx.noteIdsToUpdate) {
@@ -1672,7 +1674,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const toNode = node.getPrevSibling();
if (toNode !== null) {
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId);
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId, this.componentId);
}
}
@@ -1809,12 +1811,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
#moveLaunchers(selectedOrActiveBranchIds: string[], desktopParent: string, mobileParent: string) {
const desktopLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith("_lbMobile"));
if (desktopLaunchersToMove) {
branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`);
branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`, this.componentId);
}
const mobileLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => branchId.startsWith("_lbMobile"));
if (mobileLaunchersToMove) {
branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${ mobileParent}`);
branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${mobileParent}`, this.componentId);
}
}
@@ -1882,3 +1884,112 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return items;
}
}
function buildEnhanceTitle() {
const createChildTemplate = document.createElement("span");
createChildTemplate.className = "tree-item-button tn-icon add-note-button bx bx-plus";
createChildTemplate.title = t("note_tree.create-child-note");
return async function enhanceTitle(event: Event,
data: {
node: Fancytree.FancytreeNode;
noteId: string;
}) {
const node = data.node;
if (!node.data.noteId) {
// if there's "non-note" node, then don't enhance
// this can happen for e.g. "Load error!" node
return;
}
const note = froca.getNoteFromCache(node.data.noteId);
if (!note) return;
const activeNoteContext = appContext.tabManager.getActiveContext();
const $span = $(node.span);
$span.find(".tree-item-button").remove();
$span.find(".note-indicator-icon").remove();
const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root";
if (note.hasLabel("workspace") && !isHoistedNote) {
const $enterWorkspaceButton = $(`<span class="tree-item-button tn-icon enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($enterWorkspaceButton);
}
if (note.type === "search") {
const $refreshSearchButton = $(`<span class="tree-item-button tn-icon refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($refreshSearchButton);
}
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
const isSubtreeHidden = note.isLabelTruthy("subtreeHidden");
if (!["search", "launcher"].includes(note.type)
&& !note.isOptions()
&& !note.isLaunchBarConfig()
&& !note.noteId.startsWith("_help")
&& !isSubtreeHidden
&& !node.extraClasses.includes("spotlighted-node")
) {
const createChildItem = createChildTemplate.cloneNode();
createChildItem.addEventListener("click", cancelClickPropagation);
node.span.append(createChildItem);
}
if (isHoistedNote) {
const $unhoistButton = $(`<span class="tree-item-button tn-icon unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
$span.append($unhoistButton);
}
// Add clone indicator with tooltip if note has multiple parents
const parentNotes = note.getParentNotes();
const realParents: FNote[] = [];
for (const parent of parentNotes) {
if (parent.noteId !== "_share" && parent.noteId !== "_lbBookmarks" && parent.type !== "search") {
realParents.push(parent);
}
}
if (realParents.length > 1) {
const parentTitles = realParents.map((p) => p.title).join(", ");
const tooltipText = realParents.length === 2
? t("note_tree.clone-indicator-tooltip-single", { parent: realParents[1].title })
: t("note_tree.clone-indicator-tooltip", { count: realParents.length, parents: parentTitles });
const $cloneIndicator = $(`<span class="note-indicator-icon clone-indicator"></span>`);
$cloneIndicator.attr("title", tooltipText);
$span.find(".fancytree-title").append($cloneIndicator);
}
// Add shared indicator with tooltip if note is shared
if (note.isShared()) {
const shareId = note.getOwnedLabelValue("shareAlias") || note.noteId;
const shareUrl = `${location.origin}${location.pathname}share/${shareId}`;
const tooltipText = t("note_tree.shared-indicator-tooltip-with-url", { url: shareUrl });
const $sharedIndicator = $(`<span class="note-indicator-icon shared-indicator"></span>`);
$sharedIndicator.attr("title", tooltipText);
$span.find(".fancytree-title").append($sharedIndicator);
}
// Add a badge with the number of items if it hides children.
const count = note.getChildNoteIds().length;
if (isSubtreeHidden && count > 0) {
const $badge = $(`<span class="note-indicator-icon subtree-hidden-badge">${count}</span>`);
$badge.attr("title", t("note_tree.subtree-hidden-tooltip", { count }));
$span.find(".fancytree-title").append($badge);
}
};
}

View File

@@ -646,17 +646,13 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: F
const setter = useCallback((value: boolean) => {
if (note) {
if (value) {
attributes.setLabel(note.noteId, labelName, "");
} else {
attributes.removeOwnedLabelByName(note, labelName);
}
attributes.setBooleanWithInheritance(note, labelName, value);
}
}, [note]);
}, [note, labelName]);
useDebugValue(labelName);
const labelValue = !!note?.hasLabel(labelName);
const labelValue = !!note?.isLabelTruthy(labelName);
return [ labelValue, setter ] as const;
}

View File

@@ -2,12 +2,14 @@ import "./TableOfContents.css";
import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import clsx from "clsx";
import { useCallback, useEffect, useState } from "preact/hooks";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import math from "../../services/math";
import { randomString } from "../../services/utils";
import { useActiveNoteContext, useContentElement, useGetContextData, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
import Icon from "../react/Icon";
import RawHtml from "../react/RawHtml";
import RightPanelWidget from "./RightPanelWidget";
//#region Generic impl.
@@ -80,6 +82,22 @@ function TableOfContentsHeading({ heading, scrollToHeading, activeHeadingId }: {
}) {
const [ collapsed, setCollapsed ] = useState(false);
const isActive = heading.id === activeHeadingId;
const contentRef = useRef<HTMLElement>(null);
// Render math equations after component mounts/updates
useEffect(() => {
if (!contentRef.current) return;
const mathElements = contentRef.current.querySelectorAll(".ck-math-tex");
for (const mathEl of mathElements ?? []) {
try {
math.render(mathEl.textContent || "", mathEl as HTMLElement);
} catch (e) {
console.warn("Failed to render math in TOC:", e);
}
}
}, [heading.text]);
return (
<>
<li className={clsx(collapsed && "collapsed", isActive && "active")}>
@@ -90,12 +108,14 @@ function TableOfContentsHeading({ heading, scrollToHeading, activeHeadingId }: {
onClick={() => setCollapsed(!collapsed)}
/>
)}
<span
<RawHtml
containerRef={contentRef}
className="item-content"
onClick={() => scrollToHeading(heading)}
>{heading.text}</span>
html={heading.text}
/>
</li>
{heading.children && (
{heading.children.length > 0 && (
<ol>
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} activeHeadingId={activeHeadingId} />)}
</ol>
@@ -189,9 +209,23 @@ function extractTocFromTextEditor(editor: CKTextEditor) {
if (type !== "elementStart" || !item.is('element') || !item.name.startsWith('heading')) continue;
const level = Number(item.name.replace( 'heading', '' ));
const text = Array.from( item.getChildren() )
.map( c => c.is( '$text' ) ? c.data : '' )
.join( '' );
// Convert model element to view, then to DOM to get HTML
const viewEl = editor.editing.mapper.toViewElement(item);
let text = '';
if (viewEl) {
const domEl = editor.editing.view.domConverter.mapViewToDom(viewEl);
if (domEl instanceof HTMLElement) {
text = domEl.innerHTML;
}
}
// Fallback to plain text if conversion fails
if (!text) {
text = Array.from( item.getChildren() )
.map( c => c.is( '$text' ) ? c.data : '' )
.join( '' );
}
// Assign a unique ID
let tocId = item.getAttribute(TOC_ID) as string | undefined;

View File

@@ -23,7 +23,7 @@ export default function SqlResults() {
{t("sql_result.no_rows")}
</Alert>
) : (
<div class="sql-console-result-container">
<div className="sql-console-result-container selectable-text">
{results?.map(rows => {
// inserts, updates
if (typeof rows === "object" && !Array.isArray(rows)) {

View File

@@ -15,6 +15,8 @@
.note-detail-split .note-detail-split-editor {
width: 100%;
flex-grow: 1;
min-width: 0;
min-height: 0;
}
.note-detail-split .note-detail-split-editor .note-detail-code {
@@ -30,6 +32,7 @@
margin: 5px;
white-space: pre-wrap;
font-size: 0.85em;
overflow: auto;
}
.note-detail-split .note-detail-split-preview {

View File

@@ -286,7 +286,7 @@ function useWatchdogCrashHandling() {
const currentState = watchdog.state;
logInfo(`CKEditor state changed to ${currentState}`);
if (currentState === "ready") {
if (currentState === "ready" && hasCrashed.current) {
hasCrashed.current = false;
watchdog.editor?.focus();
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/desktop",
"version": "0.101.1",
"version": "0.101.3",
"description": "Build your personal knowledge base with Trilium Notes",
"private": true,
"main": "src/main.ts",

View File

@@ -1,4 +1,5 @@
import { test, expect } from "@playwright/test";
import { expect,test } from "@playwright/test";
import App from "../support/app";
const NOTE_TITLE = "Trilium Integration Test DB";
@@ -65,21 +66,21 @@ test("Tabs are restored in right order", async ({ page, context }) => {
// Open three tabs.
await app.closeAllTabs();
await app.goToNoteInNewTab("Code notes");
await expect(app.getActiveTab()).toContainText("Code notes");
await app.addNewTab();
await app.goToNoteInNewTab("Text notes");
await expect(app.getActiveTab()).toContainText("Text notes");
await app.addNewTab();
await app.goToNoteInNewTab("Mermaid");
await expect(app.getActiveTab()).toContainText("Mermaid");
// Select the mid one.
await app.getTab(1).click();
await expect(app.noteTreeActiveNote).toContainText("Text notes");
await expect(app.getTab(0)).toContainText("Code notes");
await expect(app.getTab(1)).toContainText("Text notes");
await expect(app.getTab(2)).toContainText("Mermaid");
// Refresh the page and check the order.
await app.goto( { preserveTabs: true });
await expect(app.getTab(0)).toContainText("Code notes", { timeout: 15_000 });
await expect(app.getTab(0)).toContainText("Code notes");
await expect(app.getTab(1)).toContainText("Text notes");
await expect(app.getTab(2)).toContainText("Mermaid");
@@ -128,8 +129,8 @@ test("New tab displays workspaces", async ({ page, context }) => {
const workspaceNotesEl = app.currentNoteSplitContent.locator(".workspace-notes");
await expect(workspaceNotesEl).toBeVisible();
expect(workspaceNotesEl).toContainText("Personal");
expect(workspaceNotesEl).toContainText("Work");
await expect(workspaceNotesEl).toContainText("Personal");
await expect(workspaceNotesEl).toContainText("Work");
await expect(workspaceNotesEl.locator(".bx.bxs-user")).toBeVisible();
await expect(workspaceNotesEl.locator(".bx.bx-briefcase-alt")).toBeVisible();

View File

@@ -1,12 +1,12 @@
import test, { BrowserContext, expect, Page } from "@playwright/test";
import test, { expect, Page } from "@playwright/test";
import App from "../support/app";
test.beforeEach(async ({ page, context }) => {
const app = await setLayout({ page, context }, true);
const app = new App(page, context);
await app.goto();
await app.setOption("rightPaneCollapsedItems", "[]");
});
test.afterEach(async ({ page, context }) => await setLayout({ page, context }, false));
test("Table of contents works", async ({ page, context }) => {
const app = new App(page, context);
@@ -73,13 +73,15 @@ test("Attachments listing works", async ({ page, context }) => {
test("Download original PDF works", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Dacia Logan.pdf");
await app.goToNoteInNewTab("Layers test.pdf");
const pdfHelper = new PdfHelper(app);
await pdfHelper.toBeInitialized();
const downloadButton = app.currentNoteSplit.locator(".icon-action.bx.bx-download");
await expect(downloadButton).toBeVisible();
const [ download ] = await Promise.all([
page.waitForEvent("download"),
app.currentNoteSplit.locator(".icon-action.bx.bx-download").click()
downloadButton.click()
]);
expect(download).toBeDefined();
});
@@ -105,13 +107,6 @@ test("Layers listing works", async ({ page, context }) => {
await expect(layersList.locator(".pdf-layer-item")).toHaveCount(0);
});
async function setLayout({ page, context}: { page: Page; context: BrowserContext }, newLayout: boolean) {
const app = new App(page, context);
await app.goto();
await app.setOption("newLayout", newLayout ? "true" : "false");
return app;
}
class PdfHelper {
private contentFrame: ReturnType<Page["frameLocator"]>;
@@ -125,5 +120,6 @@ class PdfHelper {
async toBeInitialized() {
await expect(this.contentFrame.locator("#pageNumber")).toBeVisible();
await expect(this.contentFrame.locator(".page")).toBeVisible();
}
}

View File

@@ -1,4 +1,5 @@
import { test, expect, Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import App from "../support/app";
test("Table of contents is displayed", async ({ page, context }) => {
@@ -8,7 +9,7 @@ test("Table of contents is displayed", async ({ page, context }) => {
await app.goToNoteInNewTab("Table of contents");
await expect(app.sidebar).toContainText("Table of Contents");
const rootList = app.sidebar.locator(".toc-widget > span > ol");
const rootList = app.sidebar.locator(".toc > ol");
// Heading 1.1
// Heading 1.1
@@ -42,7 +43,7 @@ test("Highlights list is displayed", async ({ page, context }) => {
await app.closeAllTabs();
await app.goToNoteInNewTab("Highlights list");
await expect(app.sidebar).toContainText("Highlights List");
await expect(app.sidebar).toContainText("10 highlights");
const rootList = app.sidebar.locator(".highlights-list ol");
let index = 0;
for (const highlightedEl of ["Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2"]) {
@@ -64,6 +65,8 @@ test("Displays math popup", async ({ page, context }) => {
await expect(mathForm).toBeVisible();
const input = mathForm.locator(".ck-input").first();
await expect(input).toBeVisible();
await expect(input).toBeEnabled();
await input.click();
await input.fill("e=mc^2");
await page.waitForTimeout(100);

View File

@@ -37,7 +37,7 @@ export default class App {
this.noteTreeHoistedNote = this.noteTree.locator(".fancytree-node", { has: page.locator(".unhoist-button") });
this.launcherBar = page.locator("#launcher-container");
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)");
this.currentNoteSplitTitle = this.currentNoteSplit.locator(".note-title");
this.currentNoteSplitTitle = this.currentNoteSplit.locator(".note-title").first();
this.currentNoteSplitContent = this.currentNoteSplit.locator(".note-detail-printable.visible");
this.sidebar = page.locator("#right-pane");
}
@@ -68,13 +68,15 @@ export default class App {
async goToNoteInNewTab(noteTitle: string) {
const autocomplete = this.currentNoteSplit.locator(".note-autocomplete");
await expect(autocomplete).toBeVisible();
await autocomplete.fill(noteTitle);
const resultsSelector = this.currentNoteSplit.locator(".note-detail-empty-results");
await expect(resultsSelector).toContainText(noteTitle);
await resultsSelector.locator(".aa-suggestion", { hasText: noteTitle })
.nth(1) // Select the second one, as the first one is "Create a new note"
.click();
const suggestionSelector = resultsSelector.locator(".aa-suggestion")
.nth(1); // Select the second one (best candidate), as the first one is "Create a new note"
await expect(suggestionSelector).toContainText(noteTitle);
suggestionSelector.click();
}
async goToSettings() {

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/server",
"version": "0.101.1",
"version": "0.101.3",
"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,
"main": "./src/main.ts",

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,95 @@
<figure class="image image-style-align-right">
<img style="aspect-ratio:328/45;" src="1_Hiding the subtree_image.png"
width="328" height="45">
<figcaption>An example of a collection with a relatively large number of children
that are hidden from the tree.</figcaption>
</figure>
<p>The tree works well when the notes are structured in a hierarchy so that
the number of items stays small. When a note has a large number of notes
(in the order of thousands or tens of thousands), two problems arise:</p>
<ul>
<li data-list-item-id="e536c86d371061c12f76f7de2a0af67be">Navigating between notes becomes cumbersome and the tree itself gets cluttered
with a large amount of notes.</li>
<li data-list-item-id="ecc37d6c4d0430254e98615842b94429d">The large amount of notes can slow down the application considerably.</li>
</ul>
<p>Since v0.102.0, Trilium allows the tree to hide the child notes of particular
notes. This works for both&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a>&nbsp;and
normal notes.</p>
<h2>Interaction</h2>
<p>When the subtree of a note is hidden, there are a few subtle changes:</p>
<ul>
<li data-list-item-id="ec1ce3d2030f36e4847f3bbd9468d28e3">To indicate that the subtree is hidden, the note will not have an expand
button and it will display the number of children to the right.</li>
<li
data-list-item-id="ea99d38ea6c8a816cf2ab7a7e73cfcac5">It's not possible to add a new note directly from the tree.
<ul>
<li data-list-item-id="ef0132a903a11e9f667b2b2f4c4fff17a">For&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a>,
it's best to use the built-in mechanism to create notes (for example by
creating a new point on a geo-map, or by adding a new row in a table).</li>
<li
data-list-item-id="e7db44100046c8c79bf79841285aacd1f">For normal notes, it's still possible to create children via other means
such as using the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/QEAPj01N5f7w/_help_hrZ1D00cLbal">Internal (reference) links</a>&nbsp;system.</li>
</ul>
</li>
<li data-list-item-id="eb049f46cf91db6de113af1099a14944e">Notes can be dragged from outside the note, case in which they will be
cloned into it.
<ul>
<li data-list-item-id="e96d9b7a0755e9c054bab5db4fc1aa25e">Instead of switching to the child notes that were copied, the parent note
is highlighted instead.</li>
<li data-list-item-id="ec667e3f94a0cfa3fa41ce38d3ed6ee95">A notification will indicate this behavior.</li>
</ul>
</li>
<li data-list-item-id="eb64670dd7ace6764c18602b440f88049">Similarly, features such as cut/copy and then paste into the note will
also work.</li>
</ul>
<h2>Spotlighting</h2>
<figure class="image image-style-align-right">
<img style="aspect-ratio:322/83;" src="Hiding the subtree_image.png"
width="322" height="83">
</figure>
<p>Even if the subtree of a note is hidden, if a child note manages to become
active, it will still appear inside the tree in a special state called <em>spotlighted</em>.</p>
<p>During this state, the note remains under its normal hierarchy, so that
its easy to tell its location. In addition, this means that:</p>
<ul>
<li data-list-item-id="e2490369eb3d99ca694dba23a3410abef">The note position is clearly visible when using the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_eIg8jdvaoNNd">Search</a>.</li>
<li
data-list-item-id="e041d3807f80dc77b022540b0551b8376">The note can still be operated on from the tree, such as adding a&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/IakOLONlIfGI/_help_TBwsyfadTA18">Branch prefix</a>&nbsp;or moving it outside the collection.</li>
</ul>
<p>The note appears in italics to indicate its temporary display. When switching
to another note, the spotlighted note will disappear.</p>
<aside class="admonition note">
<p>Only one note can be highlighted at the time. When working with multiple
notes such as dragging them into the collection, no note will be spotlighted.
This is intentional to avoid displaying a partial state of the subtree.</p>
</aside>
<h2>Working with collections</h2>
<p>By default, some of the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a>&nbsp;will
automatically hide their child notes, for example the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_CtBQqbwXDx1w">Kanban Board</a>&nbsp;or
the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_2FvYrpmOXm29">Table</a>.</p>
<p>The reasoning behind this is that collections are generally opaque to
the rest of the notes and they can generate a large amount of sub-notes
since they intentionally lack structure (in order to allow easy swapping
between views).</p>
<p>Some types of collections have the child notes intentionally shown, for
example the legacy ones (Grid and List), but also the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_zP3PMqaG71Ct">Presentation</a>&nbsp;which
requires the tree structure in order to organize and edit the slides.</p>
<p>To toggle this behavior:</p>
<ul>
<li data-list-item-id="e6d8c8c98802d70f13df626ea1f062122">In the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>,
press the Options button underneath the title and uncheck <em>Hide child notes in tree</em>.</li>
<li
data-list-item-id="e2398432e127c54239d679a6b13d8390b">Right click the collection note in the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and
select <em>Advanced</em><em>Show subtree</em>.</li>
</ul>
<h2>Working with normal notes</h2>
<p>It's possible to hide the subtree for normal notes as well, not just collections.
To do so, right click the note in the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and
select <em>Advanced</em><em>Hide subtree.</em>
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -47,7 +47,7 @@
href="#root/_help_tAassRL4RSQL">data directory</a>in the <code spellcheck="false">TRILIUM_DATA_DIR</code> environment
variable and separate port on <code spellcheck="false">TRILIUM_PORT</code> environment
variable. How to do that depends on the platform, in Unix-based systems
you can achieve that by running command such as this:</p><pre><code class="language-text-x-trilium-auto">TRILIUM_DATA_DIR=/home/me/path/to/data/dir TRILIUM_PORT=12345 trilium </code></pre>
you can achieve that by running command such as this:</p><pre><code class="language-text-x-sh">TRILIUM_DATA_DIR=/home/me/path/to/data/dir TRILIUM_PORT=12345 trilium </code></pre>
<p>You can save this command into a <code spellcheck="false">.sh</code> script
file or make an alias. Do this similarly for a second instance with different
data directory and port.</p>

View File

@@ -10,6 +10,18 @@
"creating-and-moving-notes": "नोट्स बनाना और स्थानांतरित करना",
"move-note-up": "नोट को ऊपर ले जाएं",
"move-note-down": "नोट को नीचे ले जाएं",
"note-clipboard": "नोट क्लिपबोर्ड"
"note-clipboard": "नोट क्लिपबोर्ड",
"duplicate-subtree": "डुप्लिकेट सबट्री",
"open-new-tab": "नया टैब खोलें",
"second-tab": "लिस्ट में दूसरी टैब एक्टिवेट करें",
"third-tab": "लिस्ट में तीसरी टैब एक्टिवेट करें",
"fourth-tab": "लिस्ट में चौथी टैब एक्टिवेट करें",
"sixth-tab": "लिस्ट में छठी टैब एक्टिवेट करें",
"seventh-tab": "लिस्ट में सातवीं टैब एक्टिवेट करें",
"eight-tab": "लिस्ट में आठवीं टैब एक्टिवेट करें",
"ninth-tab": "लिस्ट में नौवीं टैब एक्टिवेट करें",
"last-tab": "लिस्ट में आखिरी टैब एक्टिवेट करें",
"show-sql-console": "\"SQL कंसोल\" पेज खोलें",
"show-backend-log": "\"बैकेंड लॉग\" पेज खोलें"
}
}

View File

@@ -1,7 +1,12 @@
import { HiddenSubtreeItem } from "@triliumnext/commons";
import { HiddenSubtreeAttribute, HiddenSubtreeItem } from "@triliumnext/commons";
import { t } from "i18next";
export default function buildHiddenSubtreeTemplates() {
const hideSubtreeAttributes: HiddenSubtreeAttribute = {
name: "subtreeHidden",
type: "label"
};
const templates: HiddenSubtreeItem = {
id: "_templates",
title: t("hidden_subtree_templates.built-in-templates"),
@@ -93,6 +98,7 @@ export default function buildHiddenSubtreeTemplates() {
name: "hidePromotedAttributes",
type: "label"
},
hideSubtreeAttributes,
{
name: "label:startDate",
type: "label",
@@ -133,6 +139,7 @@ export default function buildHiddenSubtreeTemplates() {
name: "collection",
type: "label"
},
hideSubtreeAttributes,
{
name: "viewType",
type: "label",
@@ -163,6 +170,7 @@ export default function buildHiddenSubtreeTemplates() {
name: "hidePromotedAttributes",
type: "label"
},
hideSubtreeAttributes,
{
name: "label:geolocation",
type: "label",
@@ -194,6 +202,7 @@ export default function buildHiddenSubtreeTemplates() {
name: "hidePromotedAttributes",
type: "label"
},
hideSubtreeAttributes,
{
name: "label:status",
type: "label",

View File

@@ -115,7 +115,7 @@
},
"social_buttons": {
"github": "GitHub",
"github_discussions": "GitHub Discussions",
"github_discussions": "Discusiones de GitHub",
"matrix": "Matrix",
"reddit": "Reddit"
},

View File

@@ -6,13 +6,89 @@
},
"hero_section": {
"title": "अपने विचारों को व्यवस्थित करें। अपना व्यक्तिगत नॉलेज बेस बनाएं।",
"screenshot_alt": "ट्रिलियम नोट्स डेस्कटॉप एप्लिकेशन का स्क्रीनशॉट"
"screenshot_alt": "ट्रिलियम नोट्स डेस्कटॉप एप्लिकेशन का स्क्रीनशॉट",
"get_started": "शुरू करें",
"github": "गिटहब"
},
"organization_benefits": {
"note_structure_title": "नोट संरचना",
"note_structure_description": "नोटों को पदानुक्रमिक रूप से व्यवस्थित किया जा सकता है। फ़ोल्डर्स की कोई आवश्यकता नहीं है, क्योंकि प्रत्येक नोट में उप-नोट हो सकते हैं। एक एकल नोट को पदानुक्रम में कई स्थानों पर जोड़ा जा सकता है।"
},
"productivity_benefits": {
"protected_notes_title": "संरक्षित नोट्स"
"protected_notes_title": "संरक्षित नोट्स",
"web_clipper_title": "वेब क्लिपर"
},
"note_types": {
"canvas_title": "कैनवास",
"mindmap_title": "माइंडमैप"
},
"extensibility_benefits": {
"share_title": "वेब पर नोट्स शेयर करें"
},
"collections": {
"calendar_title": "कैलेंडर",
"table_title": "टेबल"
},
"download_now": {
"linux_small": "लिनक्स के लिए",
"more_platforms": "अधिक प्लेटफॉर्म और सर्वर सेटअप"
},
"header": {
"get-started": "शुरू करें",
"support-us": "हमें सपोर्ट करें"
},
"social_buttons": {
"github": "गिटहब",
"matrix": "मैट्रिक्स",
"reddit": "रेडिट"
},
"support_us": {
"title": "हमें सपोर्ट करें"
},
"404": {
"description": "आप जो पेज खोज रहे थे वह नहीं मिल पाया। शायद वह डिलीट हो चुका है या यूआरएल (URL) गलत है।"
},
"download_helper_desktop_windows": {
"title_x64": "Windows 64-bit",
"title_arm64": "ARM पर Windows",
"quick_start": "Winget द्वारा इंस्टॉल करने के लिए:",
"download_exe": "इंस्टॉलर (.exe) डाउनलोड करें",
"download_zip": "पोर्टेबल (.zip)"
},
"download_helper_desktop_linux": {
"title_x64": "Linux 64-bit",
"title_arm64": "ARM पर लिनक्स",
"download_deb": ".deb",
"download_rpm": ".rpm",
"download_flatpak": ".flatpak",
"download_zip": "पोर्टेबल (.zip)",
"download_nixpkgs": "nixpkgs"
},
"download_helper_desktop_macos": {
"title_x64": "Intel के लिए macOS",
"title_arm64": "Apple Silicon के लिए macOS",
"description_x64": "macOS Monterey या उसके बाद के वर्ज़न पर चलने वाले Intel-आधारित Macs के लिए।",
"description_arm64": "Apple Silicon Macs के लिए, जैसे कि M1 और M2 चिप्स वाले मॉडल।",
"quick_start": "Homebrew द्वारा इंस्टॉल करने के लिए:",
"download_dmg": "इंस्टॉलर (.dmg) डाउनलोड करें",
"download_homebrew_cask": "Homebrew Cask",
"download_zip": "पोर्टेबल (.zip)"
},
"download_helper_server_docker": {
"title": "Docker द्वारा सेल्फ-होस्टेड",
"description": "Docker कंटेनर का उपयोग करके Windows, Linux या macOS पर आसानी से डिप्लॉय करें।",
"download_ghcr": "ghcr.io"
},
"download_helper_server_linux": {
"title": "Linux पर सेल्फ-होस्टेड",
"description": "ट्रिलियम नोट्स को अपने खुद के सर्वर या VPS पर डिप्लॉय करें, जो अधिकांश डिस्ट्रीब्यूशनो के साथ कम्पेटिबल है।",
"download_tar_x64": "x64 (.tar.xz)",
"download_tar_arm64": "ARM (.tar.xz)",
"download_nixos": "NixOS मॉड्यूल"
},
"download_helper_server_hosted": {
"title": "पेड होस्टिंग",
"download_pikapod": "PikaPods पर सेटअप करें",
"download_triliumcc": "वैकल्पिक रूप से trilium.cc देखें"
}
}

View File

@@ -130,6 +130,23 @@
"mobile_question": "모바일 앱이 있나요?",
"mobile_answer": "현재 공식적인 모바일 앱은 없습니다. 하지만, 서버 인스턴스를 가지고 있다면 웹 브라우저를 이용해 접근하거나 PWA로 설치할 수 있습니다. 안드로이드에는 (데스크탑 클라이언트처럼)오프라인에서도 작동하는 TriliumDroid라는 비공식 앱이 있습니다.",
"database_question": "어디에 데이터가 저장되나요?",
"server_question": "Trilium을 사용하기 위해 서버가 필요한가요?"
"server_question": "Trilium을 사용하기 위해 서버가 필요한가요?",
"title": "자주 묻는 질문",
"database_answer": "모든 노트는 애플리케이션 폴더의 SQLite 데이터베이스에 저장됩니다. Trilium이 텍스트 파일 대신 데이터베이스를 사용하는 이유는 성능과 기능 모두 구현하기 훨씬 어렵기 때문입니다(트리 여러 위치에 같은 노트를 두는 Clone과 같은 기능). 애플리케이션 폴더를 찾으려면 About 창으로 가세요.",
"server_answer": "아니요, 서버는 웹 브라우저를 통해 접속할 수 있도록 허용하며, 여러 기기를 사용하는 경우 동기화를 관리합니다. 시작하려면 데스크톱 애플리케이션을 다운로드하여 사용하기만 하면 됩니다.",
"scaling_question": "이 애플리케이션은 얼마나 많은 노트를 처리할 수 있나요?",
"scaling_answer": "사용량에 따라 다르겠지만, 이 애플리케이션은 최소 10만 개의 노트를 문제없이 처리할 수 있습니다. 다만, Trilium은 (NextCloud와 같은) 파일 저장소라기보다는 지식 기반 애플리케이션에 가깝기 때문에, 대용량 파일(파일당 1GB 이상)을 많이 업로드할 경우 동기화 과정이 실패할 수 있다는 점에 유의하십시오.",
"network_share_question": "내 데이터베이스를 네트워크 드라이브로 공유할 수 있나요?",
"network_share_answer": "아니요, 일반적으로 SQLite 데이터베이스를 네트워크 드라이브로 공유하는 것은 좋지 않습니다. 경우에 따라 작동할 수도 있지만, 네트워크를 통한 파일 잠금이 완벽하지 않아 데이터베이스가 손상될 가능성이 있습니다.",
"security_question": "내 데이터는 어떻게 보호되나요?",
"security_answer": "기본적으로 노트는 암호화되지 않으며 데이터베이스에서 직접 읽을 수 있습니다. 노트를 암호화 대상으로 표시하면, AES-128-CBC를 사용하여 암호화됩니다."
},
"final_cta": {
"title": "Trilium Notes를 시작할 준비가 되셨나요?",
"description": "강력한 기능과 완벽한 개인 정보 보호를 통해 나만의 지식 기반을 구축하세요.",
"get_started": "시작하기"
},
"components": {
"link_learn_more": "자세히 알아보기..."
}
}

View File

@@ -10,13 +10,18 @@
"title": "Organiser tankene dine. Bygg din personlige kunnskapsbase.",
"github": "GitHub",
"get_started": "Kom i gang",
"dockerhub": "Docker Hub"
"dockerhub": "Docker Hub",
"screenshot_alt": "Screenshot fra Trilium Notes skrivebordsprogram",
"subtitle": "Trilium er en open-source-løsning for å ta notater og organisere en personlig kunnskapsbase. Kan brukes lokalt på arbeidsstasjonen din eller synkroniseres med en selv-hostet løsning for å ha dine notater med deg overalt."
},
"organization_benefits": {
"title": "Organisering",
"note_structure_title": "Notatstruktur",
"hoisting_title": "Arbeidsflate og fokusering",
"attributes_description": "Bruk relasjoner mellom notater eller legg til etiketter for enkel kategorisering. Bruk fremhevede attributter for å legge inn strukturert informasjon som kan brukes i tabeller og tavler."
"attributes_description": "Bruk relasjoner mellom notater eller legg til etiketter for enkel kategorisering. Bruk fremhevede attributter for å legge inn strukturert informasjon som kan brukes i tabeller og tavler.",
"note_structure_description": "Notater kan arrangeres herarkisk. Det trengs ikke mapper, siden alle notater kan inneholde undernotater. Ett notat kan legges inn flere steder i herarkiet.",
"attributes_title": "Notatetiketter og -relasjoner",
"hoisting_description": "Du kan enkelt skille personlige og arbeidsnotater ved å gruppere de under arbeidsrom, som fokuserer notat-treet ditt på kun ønskede notater."
},
"productivity_benefits": {
"sync_title": "Synkronisering",
@@ -26,7 +31,12 @@
"protected_notes_title": "Beskyttede notater",
"title": "Produktivitet og sikkerhet",
"sync_content": "Bruk en selv-hostet eller cloud-instans for å enkelt synkronisere notater på tvers av enheter, og ha de tilgjengelige fra din mobiltelefon ved hjelp av progressiv web-app.",
"jump_to_content": "Hopp raskt til notater eller grensesnittkommandoer over hele hierarkiet ved å søke etter tittel, med \"fuzzy\" matching for å ta hensyn til skrivefeil eller små differanser."
"jump_to_content": "Hopp raskt til notater eller grensesnittkommandoer over hele hierarkiet ved å søke etter tittel, med \"fuzzy\" matching for å ta hensyn til skrivefeil eller små differanser.",
"revisions_content": "Notater lagres periodisk i bakgrunnen og revisjonshistorikk kan brukes for tilbakeblikk eller å omgjøre uønskede endringer. Revisjoner kan også lages manuelt.",
"protected_notes_content": "Beskytt sensitiv personlig informasjon ved å kryptere notater og låse de med en passordkryptert sesjon.",
"jump_to_title": "Hurtigsøk og kommandoer",
"search_content": "Eller søk etter tekst i notatene og finjuster søket ved å filtrere på foreldrenotat eller dybde.",
"web_clipper_content": "Hent nettsider (eller screenshots) og legg de direkte i Trilium ved hjelp av web clipper nettleserutvidelse."
},
"note_types": {
"canvas_title": "Kanvas",
@@ -34,13 +44,26 @@
"text_title": "Tekstnotat",
"code_title": "Kodenotat",
"file_title": "Filnotat",
"mermaid_title": "Mermaid diagrammer"
"mermaid_title": "Mermaid diagrammer",
"title": "Flere måter å presentere informasjonen din",
"text_description": "Notatene redigeres med en visuell editor (WYSIWYG), som støtter tabeller, bilder, matematiske uttrykk og kodeblokker med syntaksutheving. Formater tekst hurtig med Markdown-inspirert syntaks eller \"slash-kommandoer\".",
"code_description": "Store samlinger med kildekode eller skript bruker en dedikert editor med syntaksfremheving for mange programmeringsspråk og med flere fargetema.",
"file_description": "Integrer multimediafiler som PDFer, bilder og video med forhåndsvisning i programmet.",
"mermaid_description": "Lag diagrammer som flytskjema, klasse- og sekvensdiagrammer, Ganttdiagrammer og mye mer ved hjelp av Mermaidsyntaks.",
"mindmap_description": "Organiser dine tanker visuelt eller gjør en brainstorming.",
"others_list": "og andre: <0>notatkart</0>, <1>relasjonskart</1>, <2>lagrede søk</2>, <3>rendret notat</3>, og <4>web view</4>.",
"canvas_description": "Arranger figurer, bilder og tekst på et uendelig lerret som bruker samme teknologi som excalidraw.com. Ideelt for diagrammer, skisser og visuell planlegging."
},
"extensibility_benefits": {
"import_export_title": "Import/eksport",
"scripting_title": "Avansert skripting",
"api_title": "REST API",
"title": "Deling og utvidbarhet"
"title": "Deling og utvidbarhet",
"share_title": "Del notater på nett",
"share_description": "Hvis du har en server, kan den brukes til å dele valgfrie notater med andre.",
"scripting_description": "Lag dine egne integrasjoner i Trilium med egendefinerte widgets, eller serversidelogikk.",
"import_export_description": "Samhandle med andre programmer ved hjelp av Markdown, ENEX og OML.",
"api_description": "Ved hjelp av den innebygde REST-APIen kan du programmatisk samhandle med Trilium."
},
"collections": {
"title": "Samlinger",
@@ -49,7 +72,11 @@
"geomap_title": "Geokart",
"presentation_title": "Presentasjon",
"board_title": "Kanbantavle",
"geomap_description": "Planlegg ferien din eller merk deg dine interessepunkter på et geografisk kart ved hjelp av definerbare markører. Vis lagrede GPX-spor for å se reisen din."
"geomap_description": "Planlegg ferien din eller merk deg dine interessepunkter på et geografisk kart ved hjelp av definerbare markører. Vis lagrede GPX-spor for å se reisen din.",
"calendar_description": "Organiser dine personlige eller jobb-arrangement ved hjelp av kalender, med støtte for heldags- og flerdagsarrangement. Få rask oversikt over dine arrangementer med ukes- måneds- og årsvisning. Dra og slipp hendelser for enkelt å gjøre endringer.",
"table_description": "Vis og rediger informasjon om notater i tabellform, med ulike kolonnetyper som tekst, nummer, avkrysningsbokser, dato og tid, lenker, farger og støtte for relasjoner. Du kan også vise notater i et hierarkisk tre i tabellen.",
"board_description": "Organiser oppgaver eller prosjekter i en Kanbantavle hvor du enkelt kan lage nye elementer og kolonner, og endre status på elementer ved å dra de rundt på tavlen.",
"presentation_description": "Organiser informasjon i lysbilder og presenter dem i fullskjermmodus med myke overganger. Lysbildene kan også eksporteres til PDF for enkel deling."
},
"header": {
"documentation": "Dokumentasjon",
@@ -67,14 +94,19 @@
"title": "Støtt oss",
"financial_donations_title": "Finansiell donasjon",
"github_sponsors": "GitHub Sponsors",
"financial_donations_description": "Trilium er bygget og vedlikeholdt med <Link>flere hundre timers arbeid</Link>. Ditt bidrag hjelper å holde det åpen kildekode, forbedre funksjonalitet og dekker driftskostnader."
"financial_donations_description": "Trilium er bygget og vedlikeholdt med <Link>flere hundre timers arbeid</Link>. Ditt bidrag hjelper å holde det åpen kildekode, forbedre funksjonalitet og dekker driftskostnader.",
"financial_donations_cta": "Vurder gjerne å støtte hovedutvikleren (<Link>eliandoran</Link>) av programmet via:",
"buy_me_a_coffee": "Buy Me A Coffee"
},
"download_helper_desktop_windows": {
"download_scoop": "Scoop",
"title_x64": "Windows 64-bit",
"download_zip": "Portable (.zip)",
"title_arm64": "Windows på ARM",
"download_exe": "Last ned installasjonsprogram (.exe)"
"download_exe": "Last ned installasjonsprogram (.exe)",
"description_x64": "Kompatibel med Intel- eller AMD-enheter som kjører Windows 10 og 11.",
"description_arm64": "Kompatibel med ARM-enheter (for eksempel Qualcomm Snapdragon).",
"quick_start": "For å installere via Winget:"
},
"download_helper_desktop_linux": {
"download_deb": ".deb",
@@ -84,21 +116,31 @@
"download_aur": "AUR",
"title_x64": "Linux 64-bit",
"download_zip": "Portable (.zip)",
"title_arm64": "Linux på ARM"
"title_arm64": "Linux på ARM",
"description_x64": "For de fleste Linux-distribusjoner, kompatibelt med x86_64-arkitektur.",
"description_arm64": "For ARM-baserte Linux-distribusjoner, kompatibelt med aarch64-arkitektur.",
"quick_start": "Velg egnet pakkeformat avhengig av din distribusjon:"
},
"download_helper_server_docker": {
"download_ghcr": "ghcr.io",
"download_dockerhub": "Docker Hub",
"title": "Selv-hostet med Docker"
"title": "Selv-hostet med Docker",
"description": "Installer enkelt på Windows, Linux eller macOS ved bruk av en Docker-container."
},
"download_helper_desktop_macos": {
"download_homebrew_cask": "Homebrew Cask",
"download_zip": "Portable (.zip)",
"title_x64": "macOS for Intel",
"download_dmg": "Last ned installasjonsprogram (.dmg)"
"download_dmg": "Last ned installasjonsprogram (.dmg)",
"title_arm64": "macOS for Apple Silicon",
"description_x64": "For Intel-baserte Mac-er med macOS Monterey eller nyere.",
"description_arm64": "For Apple Silicon Mac-er som de med M1- og M2-chiper.",
"quick_start": "For å installere via Homebrew:"
},
"final_cta": {
"get_started": "Kom i gang"
"get_started": "Kom i gang",
"title": "Klar for å begynne med Trilium Notes?",
"description": "Skap din personlige kunnskapsbase med kraftig funksjonalitet og fullt personvern."
},
"components": {
"link_learn_more": "Lær mer..."
@@ -108,7 +150,8 @@
"platform_small": "for {{platform}}",
"linux_small": "for Linux",
"platform_big": "v{{version}} for {{platform}}",
"linux_big": "v{{version}} for Linux"
"linux_big": "v{{version}} for Linux",
"more_platforms": "Flere plattformer og serveroppsett"
},
"footer": {
"copyright_and_the": " og ",
@@ -118,16 +161,40 @@
"download_tar_x64": "x64 (.tar.xz)",
"download_tar_arm64": "ARM (.tar.xz)",
"download_nixos": "NixOS modul",
"title": "Selv-hostet på Linux"
"title": "Selv-hostet på Linux",
"description": "Installer Trilium Notes på din egen server eller VPS, kompatibel med de fleste distribusjoner."
},
"download_helper_server_hosted": {
"title": "Betalt hosting",
"download_triliumcc": "Alternativt sjekk trilium.cc"
"download_triliumcc": "Alternativt sjekk trilium.cc",
"description": "Trilium Notes driftet på PikaPods, en betalt tjeneste for enkel tilgang og administrasjon. Ikke direkte tilknyttet Trilium-teamet.",
"download_pikapod": "Installer på PikaPods"
},
"faq": {
"title": "Ofte stilte spørsmål"
"title": "Ofte stilte spørsmål",
"mobile_question": "Finnes det en mobil applikasjon?",
"mobile_answer": "Foreløpig er det ikke noe offisiell mobil applikasjon. Men hvis du har en serverinstans kan du koble til denne med en nettleser, og også installere den som en progressiv web-app. For Android finnes det en uoffisiell applikasjon med navn TriliumDroid som også fungerer offline (samme som en skrivebordsklient).",
"database_question": "Hvor lagres dataene?",
"database_answer": "Alle notater lagres i en SQLite-database i en programmappe. Årsaken til at Trilium bruker database i stedet for rene tekstfiler er både ytelse og at visse funksjoner ellers ville vært vanskelig å implementere, slik som klonede notater (samme notat flere steder). For å finne programmappen, åpne \"om\"-vinduet i programmet.",
"server_question": "Trenger jeg en server for å bruke Trilium?",
"server_answer": "Nei, serveren tillater tilgang via nettleser og håndterer synkronisering hvis du har flere enheter. For å komme i gang er det nok å laste ned skrivebordsprogrammet og begynne med det.",
"scaling_question": "Hvor godt skalerer programmet med store mengder notater?",
"scaling_answer": "Avhengig av bruk burde programmet kunne håndtere minst 100.000 notater uten problemer. Merk at synkroniseringen noen ganger kan feile ved opplasting av mange store filer (1GB per fil) siden Trilium er ment for å være en kunnskapsbase mer enn et fillager (som for eksempel NextCloud).",
"network_share_question": "Kan jeg dele databasen min over nettverksdeling?",
"network_share_answer": "Nei, det er stort sett ikke en god ide å dele en SQLite-database over nettverksdeling. Selv om det kan fungere, er det sjanser for at databasen kan bli ødelagt grunnet problemer med fillåsing over nettverk.",
"security_question": "Hvordan er mine data beskyttet?",
"security_answer": "Som standard blir ikke notater kryptert og kan leses direkte fra databasen. Når et notat er markert kryptert, blir det kryptert med AES-128-CBC."
},
"404": {
"title": "404: Siden ble ikke funnet"
"title": "404: Siden ble ikke funnet",
"description": "Siden ble ikke funnet. Den kan ha blitt slettet eller adressen er feil."
},
"contribute": {
"title": "Andre måter å bidra",
"way_translate": "Oversett programmet til ditt språk via <Link>Weblate</Link>.",
"way_community": "Ta del i felleskapet på <Discussions>GitHub Discussions</Discussions> eller på <Matrix>Matrix</Matrix>.",
"way_reports": "Meld feil via <Link>GitHub issues</Link>.",
"way_document": "Hjelp oss å forbedre dokumentasjonen ved å fortelle om mangler, eller bidra med veiledninger, Ofte Stilte Spørsmål eller tutorials.",
"way_market": "Spre ordet: Del Trilium Notes med venner, på blogger eller i sosiale media."
}
}

16
docs/README-de.md vendored
View File

@@ -96,8 +96,8 @@ Unsere Dokumentation ist verfügbar in mehreren Formaten:
für eine sicherere Anmeldung
* [Synchronisierung](https://docs.triliumnotes.org/user-guide/setup/synchronization)
mit einem selbst gehosteten Synchronisierungsserver
* there are [3rd party services for hosting synchronisation
server](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
* Es gibt [Drittanbieter-Dienste für das Hosting von
Synchronisationsservern](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
* [Freigabe](https://docs.triliumnotes.org/user-guide/advanced-usage/sharing)
(Veröffentlichung) von Notizen im öffentlichen Internet
* Starke
@@ -105,10 +105,10 @@ Unsere Dokumentation ist verfügbar in mehreren Formaten:
mit Granularität pro Notiz
* Skizzieren von Diagrammen basierend auf [Excalidraw](https://excalidraw.com/)
(Notiztyp „Canvas“)
* [Relation
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
[note/link maps](https://docs.triliumnotes.org/user-guide/note-types/note-map)
for visualizing notes and their relations
* [Beziehungskarten](https://docs.triliumnotes.org/user-guide/note-types/relation-map)
und
[Notiz-/Link-Karten](https://docs.triliumnotes.org/user-guide/note-types/note-map)
zur Visualisierung von Notizen und ihren Beziehungen
* Mindmaps, basierend auf [Mind Elixir](https://docs.mind-elixir.com/)
* [Geokarten](https://docs.triliumnotes.org/user-guide/collections/geomap) mit
Standortmarkierungen und GPX-Tracks
@@ -132,8 +132,8 @@ Unsere Dokumentation ist verfügbar in mehreren Formaten:
einfachen Speichern von Webinhalten
* Anpassbare Benutzeroberfläche (Seitenleisten-Schaltflächen, benutzerdefinierte
Widgets, ...)
* [Metrics](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics),
along with a Grafana Dashboard.
* [Metriken](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics)
zusammen mit einem Grafana-Dashboard.
✨ Weitere Informationen zu TriliumNext findet man in den folgenden
Ressourcen/Communities von Drittanbietern:

23
docs/README-hi.md vendored
View File

@@ -74,13 +74,13 @@ Our documentation is available in multiple formats:
* Rich WYSIWYG note editor including e.g. tables, images and
[math](https://docs.triliumnotes.org/user-guide/note-types/text) with markdown
[autoformat](https://docs.triliumnotes.org/user-guide/note-types/text/markdown-formatting)
* Support for editing [notes with source
code](https://docs.triliumnotes.org/user-guide/note-types/code), including
syntax highlighting
* Fast and easy [navigation between
notes](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-navigation),
full text search and [note
hoisting](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-hoisting)
* [सोर्स कोड वाले
नोट्स](https://docs.triliumnotes.org/user-guide/note-types/code) को एडिट करने
की सुविधा, जिसमें सिंटैक्स हाइलाइटिंग (syntax highlighting) भी शामिल है
* तेज़ और आसान [नोट्स के बीच
नेविगेशन](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-navigation),
फुल टेक्स्ट सर्च और [नोट
होइस्टिंग](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-hoisting)
* Seamless [note
versioning](https://docs.triliumnotes.org/user-guide/concepts/notes/note-revisions)
* Note
@@ -98,16 +98,15 @@ Our documentation is available in multiple formats:
server](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
* [Sharing](https://docs.triliumnotes.org/user-guide/advanced-usage/sharing)
(publishing) notes to public internet
* Strong [note
encryption](https://docs.triliumnotes.org/user-guide/concepts/notes/protected-notes)
with per-note granularity
* प्रति-नोट granularity के साथ मजबूत [नोट
एन्क्रिप्शन](https://docs.triliumnotes.org/user-guide/concepts/notes/protected-notes)
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type
"canvas")
* [Relation
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
[note/link maps](https://docs.triliumnotes.org/user-guide/note-types/note-map)
for visualizing notes and their relations
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
* [Mind Elixir](https://docs.mind-elixir.com/) पर आधारित माइंड मैप्स
* [Geo maps](https://docs.triliumnotes.org/user-guide/collections/geomap) with
location pins and GPX tracks
* [Scripting](https://docs.triliumnotes.org/user-guide/scripts) - see [Advanced
@@ -157,7 +156,7 @@ compatible with the latest zadam/trilium version of
versions of TriliumNext/Trilium have their sync versions incremented which
prevents direct migration.
## 💬 Discuss with us
## 💬 हमारे साथ चर्चा करें
Feel free to join our official conversations. We would love to hear what
features, suggestions, or issues you may have!

View File

@@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.101.1",
"appVersion": "0.101.3",
"files": [
{
"isClone": false,
@@ -61,6 +61,58 @@
"attachments": [],
"dirFileName": "Release Notes",
"children": [
{
"isClone": false,
"noteId": "IlBzLeN3MJhw",
"notePath": [
"hD3V4hiu2VW4",
"IlBzLeN3MJhw"
],
"title": "v0.101.3",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "template",
"value": "wyurrlcDl416",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",
"dataFileName": "v0.101.3.md",
"attachments": []
},
{
"isClone": false,
"noteId": "vcBthaXcwAm6",
"notePath": [
"hD3V4hiu2VW4",
"vcBthaXcwAm6"
],
"title": "v0.101.2",
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "template",
"value": "wyurrlcDl416",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",
"dataFileName": "v0.101.2.md",
"attachments": []
},
{
"isClone": false,
"noteId": "AgUcrU9nFXuW",
@@ -69,7 +121,7 @@
"AgUcrU9nFXuW"
],
"title": "v0.101.1",
"notePosition": 10,
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -95,7 +147,7 @@
"uYwlZ594eyJu"
],
"title": "v0.101.0",
"notePosition": 20,
"notePosition": 40,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -121,7 +173,7 @@
"iPGKEk7pwJXK"
],
"title": "v0.100.0",
"notePosition": 30,
"notePosition": 50,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -147,7 +199,7 @@
"7HKMTjmopLcM"
],
"title": "v0.99.5",
"notePosition": 40,
"notePosition": 60,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -173,7 +225,7 @@
"RMBaNYPsRpIr"
],
"title": "v0.99.4",
"notePosition": 50,
"notePosition": 70,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -199,7 +251,7 @@
"yuroLztFfpu5"
],
"title": "v0.99.3",
"notePosition": 60,
"notePosition": 80,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -225,7 +277,7 @@
"z207sehwMJ6C"
],
"title": "v0.99.2",
"notePosition": 70,
"notePosition": 90,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -251,7 +303,7 @@
"WGQsXq2jNyTi"
],
"title": "v0.99.1",
"notePosition": 80,
"notePosition": 100,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -277,7 +329,7 @@
"cyw2Yue9vXf3"
],
"title": "v0.99.0",
"notePosition": 90,
"notePosition": 110,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -303,7 +355,7 @@
"QOJwjruOUr4k"
],
"title": "v0.98.1",
"notePosition": 100,
"notePosition": 120,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -329,7 +381,7 @@
"PLUoryywi0BC"
],
"title": "v0.98.0",
"notePosition": 110,
"notePosition": 130,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -355,7 +407,7 @@
"lvOuiWsLDv8F"
],
"title": "v0.97.2",
"notePosition": 120,
"notePosition": 140,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -381,7 +433,7 @@
"OtFZ6Nd9vM3n"
],
"title": "v0.97.1",
"notePosition": 130,
"notePosition": 150,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -407,7 +459,7 @@
"SJZ5PwfzHSQ1"
],
"title": "v0.97.0",
"notePosition": 140,
"notePosition": 160,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -433,7 +485,7 @@
"mYXFde3LuNR7"
],
"title": "v0.96.0",
"notePosition": 150,
"notePosition": 170,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -459,7 +511,7 @@
"jthwbL0FdaeU"
],
"title": "v0.95.0",
"notePosition": 160,
"notePosition": 180,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -485,7 +537,7 @@
"7HGYsJbLuhnv"
],
"title": "v0.94.1",
"notePosition": 170,
"notePosition": 190,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -511,7 +563,7 @@
"Neq53ujRGBqv"
],
"title": "v0.94.0",
"notePosition": 180,
"notePosition": 200,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -537,7 +589,7 @@
"VN3xnce1vLkX"
],
"title": "v0.93.0",
"notePosition": 190,
"notePosition": 210,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -555,7 +607,7 @@
"WRaBfQqPr6qo"
],
"title": "v0.92.7",
"notePosition": 200,
"notePosition": 220,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -581,7 +633,7 @@
"a2rwfKNmUFU1"
],
"title": "v0.92.6",
"notePosition": 210,
"notePosition": 230,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -599,7 +651,7 @@
"fEJ8qErr0BKL"
],
"title": "v0.92.5-beta",
"notePosition": 220,
"notePosition": 240,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -617,7 +669,7 @@
"kkkZQQGSXjwy"
],
"title": "v0.92.4",
"notePosition": 230,
"notePosition": 250,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -635,7 +687,7 @@
"vAroNixiezaH"
],
"title": "v0.92.3-beta",
"notePosition": 240,
"notePosition": 260,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -653,7 +705,7 @@
"mHEq1wxAKNZd"
],
"title": "v0.92.2-beta",
"notePosition": 250,
"notePosition": 270,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -671,7 +723,7 @@
"IykjoAmBpc61"
],
"title": "v0.92.1-beta",
"notePosition": 260,
"notePosition": 280,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -689,7 +741,7 @@
"dq2AJ9vSBX4Y"
],
"title": "v0.92.0-beta",
"notePosition": 270,
"notePosition": 290,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -707,7 +759,7 @@
"3a8aMe4jz4yM"
],
"title": "v0.91.6",
"notePosition": 280,
"notePosition": 300,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -725,7 +777,7 @@
"8djQjkiDGESe"
],
"title": "v0.91.5",
"notePosition": 290,
"notePosition": 310,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -743,7 +795,7 @@
"OylxVoVJqNmr"
],
"title": "v0.91.4-beta",
"notePosition": 300,
"notePosition": 320,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -761,7 +813,7 @@
"tANGQDvnyhrj"
],
"title": "v0.91.3-beta",
"notePosition": 310,
"notePosition": 330,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -779,7 +831,7 @@
"hMoBfwSoj1SC"
],
"title": "v0.91.2-beta",
"notePosition": 320,
"notePosition": 340,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -797,7 +849,7 @@
"a2XMSKROCl9z"
],
"title": "v0.91.1-beta",
"notePosition": 330,
"notePosition": 350,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -815,7 +867,7 @@
"yqXFvWbLkuMD"
],
"title": "v0.90.12",
"notePosition": 340,
"notePosition": 360,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -833,7 +885,7 @@
"veS7pg311yJP"
],
"title": "v0.90.11-beta",
"notePosition": 350,
"notePosition": 370,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -851,7 +903,7 @@
"sq5W9TQxRqMq"
],
"title": "v0.90.10-beta",
"notePosition": 360,
"notePosition": 380,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -869,7 +921,7 @@
"yFEGVCUM9tPx"
],
"title": "v0.90.9-beta",
"notePosition": 370,
"notePosition": 390,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -887,7 +939,7 @@
"o4wAGqOQuJtV"
],
"title": "v0.90.8",
"notePosition": 380,
"notePosition": 400,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -920,7 +972,7 @@
"i4A5g9iOg9I0"
],
"title": "v0.90.7-beta",
"notePosition": 390,
"notePosition": 410,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -938,7 +990,7 @@
"ThNf2GaKgXUs"
],
"title": "v0.90.6-beta",
"notePosition": 400,
"notePosition": 420,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -956,7 +1008,7 @@
"G4PAi554kQUr"
],
"title": "v0.90.5-beta",
"notePosition": 410,
"notePosition": 430,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -983,7 +1035,7 @@
"zATRobGRCmBn"
],
"title": "v0.90.4",
"notePosition": 420,
"notePosition": 440,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1001,7 +1053,7 @@
"sCDLf8IKn3Iz"
],
"title": "v0.90.3",
"notePosition": 430,
"notePosition": 450,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1019,7 +1071,7 @@
"VqqyBu4AuTjC"
],
"title": "v0.90.2-beta",
"notePosition": 440,
"notePosition": 460,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1037,7 +1089,7 @@
"RX3Nl7wInLsA"
],
"title": "v0.90.1-beta",
"notePosition": 450,
"notePosition": 470,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1055,7 +1107,7 @@
"GyueACukPWjk"
],
"title": "v0.90.0-beta",
"notePosition": 460,
"notePosition": 480,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1073,7 +1125,7 @@
"kzjHexDTTeVB"
],
"title": "v0.48",
"notePosition": 470,
"notePosition": 490,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1140,7 +1192,7 @@
"wyurrlcDl416"
],
"title": "Release Template",
"notePosition": 480,
"notePosition": 500,
"prefix": null,
"isExpanded": false,
"type": "text",

View File

@@ -0,0 +1,22 @@
# v0.101.2
> [!NOTE]
> If you are interested in an [official mobile application](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/7447)  ([#7447](https://github.com/TriliumNext/Trilium/issues/7447)) or [multi-user support](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/4956) ([#4956](https://github.com/TriliumNext/Trilium/issues/4956)), consider offering financial support via IssueHunt (see links).
> [!IMPORTANT]
> If you enjoyed this release, consider showing a token of appreciation by:
>
> * Pressing the “Star” button on [GitHub](https://github.com/TriliumNext/Trilium) (top-right).
> * Considering a one-time or recurrent donation to the [lead developer](https://github.com/eliandoran) via [GitHub Sponsors](https://github.com/sponsors/eliandoran) or [PayPal](https://paypal.me/eliandoran).
## 🐞 Bugfixes
* [SQL Console: cannot copy table data](https://github.com/TriliumNext/Trilium/pull/8268) by @SiriusXT
* [Title is not selected when creating a note via the launcher](https://github.com/TriliumNext/Trilium/pull/8292) by @SiriusXT
* [Popup editor closing after inserting a note link](https://github.com/TriliumNext/Trilium/pull/8224) by @SiriusXT
* [New Mermaid diagrams do not save content](https://github.com/TriliumNext/Trilium/pull/8220) by @lzinga
* [Can't scroll mermaid diagram code](https://github.com/TriliumNext/Trilium/issues/8299)
* [Max content width is not respected when switching between note types in the same tab](https://github.com/TriliumNext/Trilium/issues/8065)
* [Crash When a Note Includes Itself](https://github.com/TriliumNext/Trilium/issues/8294)
* [Severe Performance Degradation and Crash Issues Due to Recursive Inclusion in Included Notes](https://github.com/TriliumNext/Trilium/issues/8017)
* [<note> is not a launcher even though it's in the launcher subtree](https://github.com/TriliumNext/Trilium/issues/8218)
* [Archived subnotes of direct children appear in grid view without #includeArchived](https://github.com/TriliumNext/Trilium/issues/8184)

View File

@@ -0,0 +1,24 @@
# v0.101.3
> [!NOTE]
> If you are interested in an [official mobile application](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/7447)  ([#7447](https://github.com/TriliumNext/Trilium/issues/7447)) or [multi-user support](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/4956) ([#4956](https://github.com/TriliumNext/Trilium/issues/4956)), consider offering financial support via IssueHunt (see links).
> [!IMPORTANT]
> If you enjoyed this release, consider showing a token of appreciation by:
>
> * Pressing the “Star” button on [GitHub](https://github.com/TriliumNext/Trilium) (top-right).
> * Considering a one-time or recurrent donation to the [lead developer](https://github.com/eliandoran) via [GitHub Sponsors](https://github.com/sponsors/eliandoran) or [PayPal](https://paypal.me/eliandoran).
This is a re-release of v0.101.2, which had a cache invalidation issue.
## 🐞 Bugfixes
* [SQL Console: cannot copy table data](https://github.com/TriliumNext/Trilium/pull/8268) by @SiriusXT
* [Title is not selected when creating a note via the launcher](https://github.com/TriliumNext/Trilium/pull/8292) by @SiriusXT
* [Popup editor closing after inserting a note link](https://github.com/TriliumNext/Trilium/pull/8224) by @SiriusXT
* [New Mermaid diagrams do not save content](https://github.com/TriliumNext/Trilium/pull/8220) by @lzinga
* [Can't scroll mermaid diagram code](https://github.com/TriliumNext/Trilium/issues/8299)
* [Max content width is not respected when switching between note types in the same tab](https://github.com/TriliumNext/Trilium/issues/8065)
* [Crash When a Note Includes Itself](https://github.com/TriliumNext/Trilium/issues/8294)
* [Severe Performance Degradation and Crash Issues Due to Recursive Inclusion in Included Notes](https://github.com/TriliumNext/Trilium/issues/8017)
* [is not a launcher even though it's in the launcher subtree](https://github.com/TriliumNext/Trilium/issues/8218)
* [Archived subnotes of direct children appear in grid view without #includeArchived](https://github.com/TriliumNext/Trilium/issues/8184)

View File

@@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.101.1",
"appVersion": "0.101.3",
"files": [
{
"isClone": false,
@@ -2426,6 +2426,122 @@
"format": "markdown",
"dataFileName": "Keyboard shortcuts.md",
"attachments": []
},
{
"isClone": false,
"noteId": "wyaGBBQrl4i3",
"notePath": [
"pOsGYCXsbNQG",
"gh7bpGYxajRS",
"Vc8PjrjAGuOp",
"oPVyFC7WL2Lp",
"wyaGBBQrl4i3"
],
"title": "Hiding the subtree",
"notePosition": 40,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "shareAlias",
"value": "hiding-subtree",
"isInheritable": false,
"position": 30
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-hide",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "GTwFsgaA0lCt",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "CtBQqbwXDx1w",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "2FvYrpmOXm29",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "IjZS7iK5EXtb",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "zP3PMqaG71Ct",
"isInheritable": false,
"position": 100
},
{
"type": "relation",
"name": "internalLink",
"value": "hrZ1D00cLbal",
"isInheritable": false,
"position": 110
},
{
"type": "relation",
"name": "internalLink",
"value": "eIg8jdvaoNNd",
"isInheritable": false,
"position": 120
},
{
"type": "relation",
"name": "internalLink",
"value": "TBwsyfadTA18",
"isInheritable": false,
"position": 130
}
],
"format": "markdown",
"dataFileName": "Hiding the subtree.md",
"attachments": [
{
"attachmentId": "d9Sq9avKDptU",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Hiding the subtree_image.png"
},
{
"attachmentId": "VmRtx3vS97v1",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Hiding the subtree_image.png"
}
]
}
]
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,55 @@
# Hiding the subtree
<figure class="image image-style-align-right"><img style="aspect-ratio:328/45;" src="1_Hiding the subtree_image.png" width="328" height="45"><figcaption>An example of a collection with a relatively large number of children that are hidden from the tree.</figcaption></figure>
The tree works well when the notes are structured in a hierarchy so that the number of items stays small. When a note has a large number of notes (in the order of thousands or tens of thousands), two problems arise:
* Navigating between notes becomes cumbersome and the tree itself gets cluttered with a large amount of notes.
* The large amount of notes can slow down the application considerably.
Since v0.102.0, Trilium allows the tree to hide the child notes of particular notes. This works for both <a class="reference-link" href="../../../Collections.md">Collections</a> and normal notes.
## Interaction
When the subtree of a note is hidden, there are a few subtle changes:
* To indicate that the subtree is hidden, the note will not have an expand button and it will display the number of children to the right.
* It's not possible to add a new note directly from the tree.
* For <a class="reference-link" href="../../../Collections.md">Collections</a>, it's best to use the built-in mechanism to create notes (for example by creating a new point on a geo-map, or by adding a new row in a table).
* For normal notes, it's still possible to create children via other means such as using the <a class="reference-link" href="../../../Note%20Types/Text/Links/Internal%20(reference)%20links.md">Internal (reference) links</a> system.
* Notes can be dragged from outside the note, case in which they will be cloned into it.
* Instead of switching to the child notes that were copied, the parent note is highlighted instead.
* A notification will indicate this behavior.
* Similarly, features such as cut/copy and then paste into the note will also work.
## Spotlighting
<figure class="image image-style-align-right"><img style="aspect-ratio:322/83;" src="Hiding the subtree_image.png" width="322" height="83"></figure>
Even if the subtree of a note is hidden, if a child note manages to become active, it will still appear inside the tree in a special state called _spotlighted_.
During this state, the note remains under its normal hierarchy, so that its easy to tell its location. In addition, this means that:
* The note position is clearly visible when using the <a class="reference-link" href="../../Navigation/Search.md">Search</a>.
* The note can still be operated on from the tree, such as adding a <a class="reference-link" href="../../Notes/Cloning%20Notes/Branch%20prefix.md">Branch prefix</a> or moving it outside the collection.
The note appears in italics to indicate its temporary display. When switching to another note, the spotlighted note will disappear.
> [!NOTE]
> Only one note can be highlighted at the time. When working with multiple notes such as dragging them into the collection, no note will be spotlighted. This is intentional to avoid displaying a partial state of the subtree.
## Working with collections
By default, some of the <a class="reference-link" href="../../../Collections.md">Collections</a> will automatically hide their child notes, for example the <a class="reference-link" href="../../../Collections/Kanban%20Board.md">Kanban Board</a> or the <a class="reference-link" href="../../../Collections/Table.md">Table</a>.
The reasoning behind this is that collections are generally opaque to the rest of the notes and they can generate a large amount of sub-notes since they intentionally lack structure (in order to allow easy swapping between views).
Some types of collections have the child notes intentionally shown, for example the legacy ones (Grid and List), but also the <a class="reference-link" href="../../../Collections/Presentation.md">Presentation</a> which requires the tree structure in order to organize and edit the slides.
To toggle this behavior:
* In the <a class="reference-link" href="../New%20Layout.md">New Layout</a>, press the Options button underneath the title and uncheck _Hide child notes in tree_.
* Right click the collection note in the <a class="reference-link" href="../Note%20Tree.md">Note Tree</a> and select _Advanced__Show subtree_.
## Working with normal notes
It's possible to hide the subtree for normal notes as well, not just collections. To do so, right click the note in the <a class="reference-link" href="../Note%20Tree.md">Note Tree</a> and select _Advanced_ → _Hide subtree._

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -44,7 +44,7 @@ No.
These general purpose sync apps are not suitable to sync database files which are open and being worked on by another application. The result is that they will corrupt the database file, resulting in data loss and this message in the Trilium logs:
```plain
```
SqliteError: database disk image is malformed
```

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/source",
"version": "0.101.1",
"version": "0.101.3",
"description": "Build your personal knowledge base with Trilium Notes",
"directories": {
"doc": "docs"
@@ -66,7 +66,7 @@
"jiti": "2.6.1",
"jsonc-eslint-parser": "2.4.2",
"react-refresh": "0.18.0",
"rollup-plugin-webpack-stats": "2.1.8",
"rollup-plugin-webpack-stats": "2.1.9",
"tslib": "2.8.1",
"tsx": "4.21.0",
"typescript": "~5.9.0",
@@ -90,7 +90,7 @@
"url": "https://github.com/TriliumNext/Trilium/issues"
},
"homepage": "https://triliumnotes.org",
"packageManager": "pnpm@10.27.0",
"packageManager": "pnpm@10.28.0",
"pnpm": {
"patchedDependencies": {
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",

View File

@@ -70,6 +70,7 @@
]
},
"dependencies": {
"@ckeditor/ckeditor5-icons": "47.3.0"
"@ckeditor/ckeditor5-icons": "47.3.0",
"mathlive": "0.108.2"
}
}

View File

@@ -1,6 +1,9 @@
import ckeditor from './../theme/icons/math.svg?raw';
import './augmentation.js';
import "../theme/mathform.css";
import 'mathlive';
import 'mathlive/fonts.css';
import 'mathlive/static.css';
export { default as Math } from './math.js';
export { default as MathUI } from './mathui.js';

View File

@@ -55,9 +55,9 @@ export default class MathUI extends Plugin {
this._balloon.showStack( 'main' );
requestAnimationFrame(() => {
this.formView?.mathInputView.fieldView.element?.focus();
});
requestAnimationFrame( () => {
this.formView?.mathInputView.focus();
} );
}
private _createFormView() {
@@ -71,31 +71,37 @@ export default class MathUI extends Plugin {
throw new CKEditorError( 'math-command' );
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const mathConfig = editor.config.get( 'math' )!;
const formView = new MainFormView(
editor.locale,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mathConfig.engine!,
mathConfig.lazyLoad,
{
engine: mathConfig.engine!,
lazyLoad: mathConfig.lazyLoad,
previewUid: this._previewUid,
previewClassName: mathConfig.previewClassName!,
katexRenderOptions: mathConfig.katexRenderOptions!
},
mathConfig.enablePreview,
this._previewUid,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mathConfig.previewClassName!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mathConfig.popupClassName!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mathConfig.katexRenderOptions!
mathConfig.popupClassName!
);
formView.mathInputView.bind( 'value' ).to( mathCommand, 'value' );
formView.displayButtonView.bind( 'isOn' ).to( mathCommand, 'display' );
// Form elements should be read-only when corresponding commands are disabled.
formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', value => !value );
formView.saveButtonView.bind( 'isEnabled' ).to( mathCommand );
formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand );
formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', ( value: boolean ) => !value );
formView.saveButtonView.bind( 'isEnabled' ).to(
mathCommand,
'isEnabled',
formView.mathInputView,
'value',
( commandEnabled, equation ) => {
const normalizedEquation = ( equation ?? '' ).trim();
return commandEnabled && normalizedEquation.length > 0;
}
);
formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand, 'isEnabled' );
// Listen to submit button click
this.listenTo( formView, 'submit', () => {
@@ -115,24 +121,12 @@ export default class MathUI extends Plugin {
} );
// Allow pressing Enter to submit changes, and use Shift+Enter to insert a new line
formView.keystrokes.set('enter', (data, cancel) => {
if (!data.shiftKey) {
formView.fire('submit');
formView.keystrokes.set( 'enter', ( data, cancel ) => {
if ( !data.shiftKey ) {
formView.fire( 'submit' );
cancel();
}
});
// Allow the textarea to be resizable
formView.mathInputView.fieldView.once('render', () => {
const textarea = formView.mathInputView.fieldView.element;
if (!textarea) return;
Object.assign(textarea.style, {
resize: 'both',
height: '100px',
width: '400px',
minWidth: '100%',
});
});
} );
return formView;
}
@@ -162,14 +156,12 @@ export default class MathUI extends Plugin {
} );
if ( this._balloon.visibleView === this.formView ) {
this.formView.mathInputView.fieldView.element?.select();
this.formView.mathInputView.focus();
}
// Show preview element
const previewEl = document.getElementById( this._previewUid );
if ( previewEl && this.formView.previewEnabled ) {
// Force refresh preview
this.formView.mathView?.updateMath();
if ( previewEl && this.formView.mathView ) {
this.formView.mathView.updateMath();
}
this.formView.equation = mathCommand.value ?? '';
@@ -206,8 +198,10 @@ export default class MathUI extends Plugin {
private _removeFormView() {
if ( this._isFormInPanel && this.formView ) {
this.formView.saveButtonView.focus();
// Hide virtual keyboard before removing the form
this.formView.hideKeyboard();
this.formView.saveButtonView.focus();
this._balloon.remove( this.formView );
// Hide preview element

View File

@@ -1,91 +1,59 @@
import { ButtonView, createLabeledTextarea, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type TextareaView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5';
import { ButtonView, FocusCycler, FocusTracker, KeystrokeHandler, LabelView, submitHandler, SwitchButtonView, View, ViewCollection, type FocusableView, type Locale } from 'ckeditor5';
import IconCheck from "@ckeditor/ckeditor5-icons/theme/icons/check.svg?raw";
import IconCancel from "@ckeditor/ckeditor5-icons/theme/icons/cancel.svg?raw";
import { extractDelimiters, hasDelimiters } from '../utils.js';
import MathView from './mathview.js';
import MathView, { type MathViewOptions } from './mathview.js';
import MathInputView from './mathinputview.js';
import '../../theme/mathform.css';
import type { KatexOptions } from '../typings-external.js';
class MathInputView extends LabeledFieldView<TextareaView> {
public value: null | string = null;
public isReadOnly = false;
constructor( locale: Locale ) {
super( locale, createLabeledTextarea );
}
}
export default class MainFormView extends View {
public saveButtonView: ButtonView;
public mathInputView: MathInputView;
public displayButtonView: SwitchButtonView;
public cancelButtonView: ButtonView;
public previewEnabled: boolean;
public previewLabel?: LabelView;
public displayButtonView: SwitchButtonView;
public mathInputView: MathInputView;
public mathView?: MathView;
public override locale: Locale = new Locale();
public lazyLoad: undefined | ( () => Promise<void> );
public focusTracker = new FocusTracker();
public keystrokes = new KeystrokeHandler();
private _focusables = new ViewCollection<FocusableView>();
private _focusCycler: FocusCycler;
constructor(
locale: Locale,
engine:
| 'mathjax'
| 'katex'
| ( (
equation: string,
element: HTMLElement,
display: boolean,
) => void ),
lazyLoad: undefined | ( () => Promise<void> ),
mathViewOptions: MathViewOptions,
previewEnabled = false,
previewUid: string,
previewClassName: Array<string>,
popupClassName: Array<string>,
katexRenderOptions: KatexOptions
popupClassName: Array<string> = []
) {
super( locale );
const t = locale.t;
// Submit button
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', null );
this.saveButtonView.type = 'submit';
// Create views
this.mathInputView = new MathInputView( locale );
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', 'submit' );
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel' );
this.cancelButtonView.delegate( 'execute' ).to( this, 'cancel' );
this.displayButtonView = this._createDisplayButton( t );
// Equation input
this.mathInputView = this._createMathInput();
// Build children
// Display button
this.displayButtonView = this._createDisplayButton();
const children: Array<View> = [
this.mathInputView,
this.displayButtonView
];
// Cancel button
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel', 'cancel' );
if ( previewEnabled ) {
const previewLabel = new LabelView( locale );
previewLabel.text = t( 'Equation preview' );
this.previewEnabled = previewEnabled;
let children = [];
if ( this.previewEnabled ) {
// Preview label
this.previewLabel = new LabelView( locale );
this.previewLabel.text = t( 'Equation preview' );
// Math element
this.mathView = new MathView( engine, lazyLoad, locale, previewUid, previewClassName, katexRenderOptions );
this.mathView = new MathView( locale, mathViewOptions );
this.mathView.bind( 'display' ).to( this.displayButtonView, 'isOn' );
children = [
this.mathInputView,
this.displayButtonView,
this.previewLabel,
this.mathView
];
} else {
children = [
this.mathInputView,
this.displayButtonView
];
children.push( previewLabel, this.mathView );
}
// Add UI elements to template
this._setupSync( previewEnabled );
this.setTemplate( {
tag: 'form',
attributes: {
@@ -107,10 +75,30 @@ export default class MainFormView extends View {
},
children
},
this.saveButtonView,
this.cancelButtonView
{
tag: 'div',
attributes: {
class: [
'ck-math-button-row'
]
},
children: [
this.saveButtonView,
this.cancelButtonView
]
}
]
} );
this._focusCycler = new FocusCycler( {
focusables: this._focusables,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
focusPrevious: 'shift + tab',
focusNext: 'tab'
}
} );
}
public override render(): void {
@@ -121,103 +109,73 @@ export default class MainFormView extends View {
view: this
} );
// Register form elements to focusable elements
const childViews = [
this.mathInputView,
const focusableViews = [
this.mathInputView.latexTextAreaView,
this.displayButtonView,
this.saveButtonView,
this.cancelButtonView
];
childViews.forEach( v => {
focusableViews.forEach( v => {
this._focusables.add( v );
if ( v.element ) {
this._focusables.add( v );
this.focusTracker.add( v.element );
}
} );
// Listen to keypresses inside form element
this.mathInputView.on( 'mathfieldReady', () => {
const mathfieldView = this.mathInputView.mathFieldFocusableView;
if ( mathfieldView.element ) {
if ( this._focusables.has( mathfieldView ) ) {
this._focusables.remove( mathfieldView );
}
this._focusables.add( mathfieldView, 0 );
this.focusTracker.add( mathfieldView.element );
}
} );
if ( this.element ) {
this.keystrokes.listenTo( this.element );
}
}
public get equation(): string {
return this.mathInputView.value ?? '';
}
public set equation( equation: string ) {
const norm = equation.trim();
this.mathInputView.value = norm.length ? norm : null;
if ( this.mathView ) {
this.mathView.value = norm;
}
}
public focus(): void {
this._focusCycler.focusFirst();
}
public get equation(): string {
return this.mathInputView.fieldView.element?.value ?? '';
}
private _setupSync( previewEnabled: boolean ): void {
this.mathInputView.on( 'change:value', () => {
let eq = ( this.mathInputView.value ?? '' ).trim();
public set equation( equation: string ) {
if ( this.mathInputView.fieldView.element ) {
this.mathInputView.fieldView.element.value = equation;
}
if ( this.previewEnabled && this.mathView ) {
this.mathView.value = equation;
}
}
if ( hasDelimiters( eq ) ) {
const params = extractDelimiters( eq );
eq = params.equation;
this.displayButtonView.isOn = params.display;
public focusTracker: FocusTracker = new FocusTracker();
public keystrokes: KeystrokeHandler = new KeystrokeHandler();
private _focusables = new ViewCollection<FocusableView>();
private _focusCycler: FocusCycler = new FocusCycler( {
focusables: this._focusables,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
focusPrevious: 'shift + tab',
focusNext: 'tab'
}
} );
private _createMathInput() {
const t = this.locale.t;
// Create equation input
const mathInput = new MathInputView( this.locale );
const fieldView = mathInput.fieldView;
mathInput.infoText = t( 'Insert equation in TeX format.' );
const onInput = () => {
if ( fieldView.element != null ) {
let equationInput = fieldView.element.value.trim();
// If input has delimiters
if ( hasDelimiters( equationInput ) ) {
// Get equation without delimiters
const params = extractDelimiters( equationInput );
// Remove delimiters from input field
fieldView.element.value = params.equation;
equationInput = params.equation;
// update display button and preview
this.displayButtonView.isOn = params.display;
if ( this.mathInputView.value !== eq ) {
this.mathInputView.value = eq.length ? eq : null;
}
if ( this.previewEnabled && this.mathView ) {
// Update preview view
this.mathView.value = equationInput;
}
this.saveButtonView.isEnabled = !!equationInput;
}
};
fieldView.on( 'render', onInput );
fieldView.on( 'input', onInput );
return mathInput;
if ( previewEnabled && this.mathView && this.mathView.value !== eq ) {
this.mathView.value = eq;
}
} );
}
private _createButton(
label: string,
icon: string,
className: string,
eventName: string | null
) {
private _createButton( label: string, icon: string, className: string, type?: 'submit' | 'button' ): ButtonView {
const button = new ButtonView( this.locale );
button.set( {
@@ -232,16 +190,14 @@ export default class MainFormView extends View {
}
} );
if ( eventName ) {
button.delegate( 'execute' ).to( this, eventName );
if ( type ) {
button.type = type;
}
return button;
}
private _createDisplayButton() {
const t = this.locale.t;
private _createDisplayButton( t: ( str: string ) => string ): SwitchButtonView {
const switchButton = new SwitchButtonView( this.locale );
switchButton.set( {
@@ -256,15 +212,13 @@ export default class MainFormView extends View {
} );
switchButton.on( 'execute', () => {
// Toggle state
switchButton.isOn = !switchButton.isOn;
if ( this.previewEnabled && this.mathView ) {
// Update preview view
this.mathView.display = switchButton.isOn;
}
} );
return switchButton;
}
public hideKeyboard(): void {
this.mathInputView.hideKeyboard();
}
}

View File

@@ -0,0 +1,268 @@
// Math input widget: wraps a MathLive <math-field> and a LaTeX textarea
// and keeps them in sync for the CKEditor 5 math dialog.
import { View, type Locale, type FocusableView } from 'ckeditor5';
import 'mathlive/fonts.css'; // Auto-bundles offline fonts
declare global {
interface Window {
mathVirtualKeyboard?: {
visible: boolean;
show: () => void;
hide: () => void;
addEventListener: ( event: string, cb: () => void ) => void;
removeEventListener: ( event: string, cb: () => void ) => void;
};
}
}
interface MathFieldElement extends HTMLElement {
value: string;
readOnly: boolean;
mathVirtualKeyboardPolicy: string;
inlineShortcuts?: Record<string, string>;
setValue?: ( value: string, options?: { silenceNotifications?: boolean } ) => void;
}
// Wrapper for the MathLive element to make it focusable in CKEditor's UI system
export class MathFieldFocusableView extends View implements FocusableView {
public declare element: HTMLElement | null;
private _view: MathInputView;
constructor( locale: Locale, view: MathInputView ) {
super( locale );
this._view = view;
}
public focus(): void {
this._view.mathfield?.focus();
}
public setElement( el: HTMLElement ): void {
this.element = el;
}
}
// Wrapper for the LaTeX textarea to make it focusable in CKEditor's UI system
export class LatexTextAreaView extends View implements FocusableView {
declare public element: HTMLTextAreaElement;
constructor( locale: Locale ) {
super( locale );
this.setTemplate( { tag: 'textarea', attributes: {
class: [ 'ck', 'ck-textarea', 'ck-latex-textarea' ], spellcheck: 'false', tabindex: 0
} } );
}
public focus(): void {
this.element?.focus();
}
}
// Main view class for the math input
export default class MathInputView extends View {
public declare value: string | null;
public declare isReadOnly: boolean;
public mathfield: MathFieldElement | null = null;
public readonly latexTextAreaView: LatexTextAreaView;
public readonly mathFieldFocusableView: MathFieldFocusableView;
private _destroyed = false;
private _vkGeometryHandler?: () => void;
private _updating = false;
private static _configured = false;
constructor( locale: Locale ) {
super( locale );
this.latexTextAreaView = new LatexTextAreaView( locale );
this.mathFieldFocusableView = new MathFieldFocusableView( locale, this );
this.set( 'value', null );
this.set( 'isReadOnly', false );
this.setTemplate( {
tag: 'div', attributes: { class: [ 'ck', 'ck-math-input' ] },
children: [
{ tag: 'div', attributes: { class: [ 'ck-mathlive-container' ] } },
{ tag: 'label', attributes: { class: [ 'ck-latex-label' ] }, children: [ locale.t( 'LaTeX' ) ] },
{ tag: 'div', attributes: { class: [ 'ck-latex-wrapper' ] }, children: [ this.latexTextAreaView ] }
]
} );
}
public override render(): void {
super.render();
const textarea = this.latexTextAreaView.element;
// Sync changes from the LaTeX textarea to the mathfield and model
this.listenTo( textarea, 'input', () => {
if ( this._updating ) {
return;
}
this._updating = true;
const val = textarea.value;
this.value = val || null;
if ( this.mathfield ) {
if ( val === '' ) {
this.mathfield.remove();
this.mathfield = null;
this._initMathField( false );
} else if ( this.mathfield.value.trim() !== val.trim() ) {
this._setMathfieldValue( val );
}
}
this._updating = false;
} );
// Sync changes from the model (this.value) to the UI elements
this.on( 'change:value', ( _e, _n, val ) => {
if ( this._updating ) {
return;
}
this._updating = true;
const newVal = val ?? '';
if ( textarea.value !== newVal ) {
textarea.value = newVal;
}
if ( this.mathfield ) {
if ( this.mathfield.value.trim() !== newVal.trim() ) {
this._setMathfieldValue( newVal );
}
} else if ( newVal !== '' ) {
this._initMathField( false );
}
this._updating = false;
} );
// Handle read-only state changes
this.on( 'change:isReadOnly', ( _e, _n, val ) => {
textarea.readOnly = val;
if ( this.mathfield ) {
this.mathfield.readOnly = val;
}
} );
// Handle virtual keyboard geometry changes
const vk = window.mathVirtualKeyboard;
if ( vk && !this._vkGeometryHandler ) {
this._vkGeometryHandler = () => {
if ( vk.visible && this.mathfield ) {
this.mathfield.focus();
}
};
vk.addEventListener( 'geometrychange', this._vkGeometryHandler );
}
const initial = this.value ?? '';
if ( textarea.value !== initial ) {
textarea.value = initial;
}
this._loadMathLive();
}
// Loads the MathLive library dynamically
private async _loadMathLive(): Promise<void> {
try {
await import( 'mathlive' );
await customElements.whenDefined( 'math-field' );
if ( this._destroyed ) {
return;
}
if ( !MathInputView._configured ) {
const MathfieldClass = customElements.get( 'math-field' ) as any;
if ( MathfieldClass ) {
MathfieldClass.soundsDirectory = null;
MathfieldClass.plonkSound = null;
MathInputView._configured = true;
}
}
if ( this.element && !this._destroyed ) {
this._initMathField( true );
}
} catch {
const c = this.element?.querySelector( '.ck-mathlive-container' );
if ( c ) {
c.textContent = 'Math editor unavailable';
}
}
}
// Initializes the <math-field> element
private _initMathField( shouldFocus: boolean ): void {
const container = this.element?.querySelector( '.ck-mathlive-container' );
if ( !container ) {
return;
}
if ( this.mathfield ) {
this._setMathfieldValue( this.value ?? '' );
return;
}
const mf = document.createElement( 'math-field' ) as MathFieldElement;
mf.mathVirtualKeyboardPolicy = 'auto';
mf.setAttribute( 'tabindex', '0' );
mf.value = this.value ?? '';
mf.readOnly = this.isReadOnly;
container.appendChild( mf );
// Set shortcuts after mounting (accessing inlineShortcuts requires mounted element)
try {
if ( mf.inlineShortcuts ) {
mf.inlineShortcuts = { ...mf.inlineShortcuts, dx: 'dx', dy: 'dy', dt: 'dt' };
}
} catch {
// Inline shortcut configuration is optional; ignore failures to avoid breaking the math field.
}
mf.addEventListener( 'keydown', ev => {
if ( ev.key === 'Tab' ) {
if ( ev.shiftKey ) {
ev.preventDefault();
} else {
ev.preventDefault();
ev.stopImmediatePropagation();
this.latexTextAreaView.focus();
}
}
}, { capture: true } );
mf.addEventListener( 'input', () => {
if ( this._updating ) {
return;
}
this._updating = true;
const textarea = this.latexTextAreaView.element;
if ( textarea.value.trim() !== mf.value.trim() ) {
textarea.value = mf.value;
}
this.value = mf.value || null;
this._updating = false;
} );
this.mathfield = mf;
this.mathFieldFocusableView.setElement( mf );
this.fire( 'mathfieldReady' );
if ( shouldFocus ) {
requestAnimationFrame( () => mf.focus() );
}
}
// Updates the mathfield value without triggering loops
private _setMathfieldValue( value: string ): void {
if ( !this.mathfield ) {
return;
}
if ( this.mathfield.setValue ) {
this.mathfield.setValue( value, { silenceNotifications: true } );
} else {
this.mathfield.value = value;
}
}
public hideKeyboard(): void {
window.mathVirtualKeyboard?.hide();
}
public focus(): void {
this.mathfield?.focus();
}
public override destroy(): void {
this._destroyed = true;
const vk = window.mathVirtualKeyboard;
if ( vk && this._vkGeometryHandler ) {
vk.removeEventListener( 'geometrychange', this._vkGeometryHandler );
this._vkGeometryHandler = undefined;
}
this.hideKeyboard();
this.mathfield?.remove();
this.mathfield = null;
super.destroy();
}
}

View File

@@ -2,44 +2,44 @@ import { View, type Locale } from 'ckeditor5';
import type { KatexOptions } from '../typings-external.js';
import { renderEquation } from '../utils.js';
/**
* Configuration options for the MathView.
*/
export interface MathViewOptions {
engine: 'mathjax' | 'katex' | ( ( equation: string, element: HTMLElement, display: boolean ) => void );
lazyLoad: undefined | ( () => Promise<void> );
previewUid: string;
previewClassName: Array<string>;
katexRenderOptions: KatexOptions;
}
export default class MathView extends View {
/**
* The LaTeX equation value to render.
* @observable
*/
public declare value: string;
/**
* Whether to render in display mode (centered) or inline.
* @observable
*/
public declare display: boolean;
public previewUid: string;
public previewClassName: Array<string>;
public katexRenderOptions: KatexOptions;
public engine:
| 'mathjax'
| 'katex'
| ( ( equation: string, element: HTMLElement, display: boolean ) => void );
public lazyLoad: undefined | ( () => Promise<void> );
constructor(
engine:
| 'mathjax'
| 'katex'
| ( (
equation: string,
element: HTMLElement,
display: boolean,
) => void ),
lazyLoad: undefined | ( () => Promise<void> ),
locale: Locale,
previewUid: string,
previewClassName: Array<string>,
katexRenderOptions: KatexOptions
) {
/**
* Configuration options passed during initialization.
*/
private options: MathViewOptions;
constructor( locale: Locale, options: MathViewOptions ) {
super( locale );
this.engine = engine;
this.lazyLoad = lazyLoad;
this.previewUid = previewUid;
this.katexRenderOptions = katexRenderOptions;
this.previewClassName = previewClassName;
this.options = options;
this.set( 'value', '' );
this.set( 'display', false );
// Update rendering when state changes.
// Checking isRendered prevents errors during initialization.
this.on( 'change', () => {
if ( this.isRendered ) {
this.updateMath();
@@ -55,19 +55,39 @@ export default class MathView extends View {
}
public updateMath(): void {
if ( this.element ) {
void renderEquation(
this.value,
this.element,
this.engine,
this.lazyLoad,
this.display,
true,
this.previewUid,
this.previewClassName,
this.katexRenderOptions
);
if ( !this.element ) {
return;
}
// Handle empty equations
if ( !this.value || !this.value.trim() ) {
this.element.textContent = '';
this.element.classList.remove( 'ck-math-render-error' );
return;
}
// Clear previous render
this.element.textContent = '';
this.element.classList.remove( 'ck-math-render-error' );
renderEquation(
this.value,
this.element,
this.options.engine,
this.options.lazyLoad,
this.display,
true, // isPreview
this.options.previewUid,
this.options.previewClassName,
this.options.katexRenderOptions
).catch( error => {
console.error( 'Math rendering failed:', error );
if ( this.element ) {
this.element.textContent = 'Error rendering equation';
this.element.classList.add( 'ck-math-render-error' );
}
} );
}
public override render(): void {

View File

@@ -3,6 +3,20 @@ import Math from '../src/math';
import AutoformatMath from '../src/autoformatmath';
import { describe, it, expect } from 'vitest';
// Suppress MathLive errors during async cleanup in tests
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', event => {
if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) {
event.preventDefault();
}
});
window.addEventListener('error', event => {
if (event.message?.includes('options') || event.message?.includes('mathlive')) {
event.preventDefault();
}
});
}
describe( 'CKEditor5 Math DLL', () => {
it( 'exports Math', () => {
expect( MathDll ).to.equal( Math );

View File

@@ -2,6 +2,20 @@ import { ClassicEditor, type EditorConfig } from 'ckeditor5';
import MathUI from '../src/mathui';
import { describe, beforeEach, it, afterEach, expect } from "vitest";
// Suppress MathLive errors during async cleanup
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', event => {
if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) {
event.preventDefault();
}
});
window.addEventListener('error', event => {
if (event.message?.includes('options') || event.message?.includes('mathlive')) {
event.preventDefault();
}
});
}
describe( 'Lazy load', () => {
let editorElement: HTMLDivElement;
let editor: ClassicEditor;
@@ -24,11 +38,14 @@ describe( 'Lazy load', () => {
beforeEach( () => {
editorElement = document.createElement( 'div' );
document.body.appendChild( editorElement );
lazyLoadInvoked = false;
} );
afterEach( () => {
afterEach( async () => {
if ( mathUIFeature?.formView ) {
mathUIFeature._hideUI();
}
await new Promise( resolve => setTimeout( resolve, 50 ) );
editorElement.remove();
return editor.destroy();
} );
@@ -37,6 +54,7 @@ describe( 'Lazy load', () => {
await buildEditor( {
math: {
engine: 'katex',
enablePreview: true,
lazyLoad: async () => {
lazyLoadInvoked = true;
}
@@ -44,6 +62,15 @@ describe( 'Lazy load', () => {
} );
mathUIFeature._showUI();
// Trigger render with a non-empty value to bypass empty check optimization
if ( mathUIFeature.formView ) {
mathUIFeature.formView.equation = 'x^2';
}
// Wait for async rendering and lazy loading
await new Promise( resolve => setTimeout( resolve, 100 ) );
expect( lazyLoadInvoked ).to.be.true;
} );
} );

View File

@@ -410,7 +410,7 @@ describe( 'MathUI', () => {
it( 'should bind mainFormView.mathInputView#value to math command value', () => {
const command = editor.commands.get( 'math' );
expect( formView!.mathInputView.value ).to.null;
expect( formView!.mathInputView.value ).to.be.null;
command!.value = 'x^2';
expect( formView!.mathInputView.value ).to.equal( 'x^2' );
@@ -419,10 +419,18 @@ describe( 'MathUI', () => {
it( 'should execute math command on mainFormView#submit event', () => {
const executeSpy = vi.spyOn( editor, 'execute' );
formView!.mathInputView.fieldView.element!.value = 'x^2';
formView!.mathInputView.value = 'x^2';
formView!.fire( 'submit' );
expect(executeSpy.mock.lastCall?.slice(0, 2)).toMatchObject(['math', 'x^2']);
expect( executeSpy.mock.lastCall?.slice( 0, 2 ) ).toMatchObject( [ 'math', 'x^2' ] );
} );
it( 'should update equation value when mathInputView changes', () => {
formView!.mathInputView.value = 'x^2';
expect( formView!.equation ).to.equal( 'x^2' );
formView!.mathInputView.value = '\\frac{1}{2}';
expect( formView!.equation ).to.equal( '\\frac{1}{2}' );
} );
it( 'should hide the balloon on mainFormView#cancel if math command does not have a value', () => {

View File

@@ -1,35 +1,220 @@
/**
* Math Equation Editor Dialog Styles - Compact & Readable
*/
/* === Z-INDEX: MathLive UI above CKEditor === */
.ML__keyboard, .ML__popover, .ML__menu, .ML__suggestions, .ML__autocomplete,
.ML__tooltip, .ML__sr-only, [data-ml-root], #mathlive-suggestion-popover,
.mathlive-suggestions-popover, [data-ml-tooltip], .ML__base {
z-index: calc(var(--ck-z-panel) + 1000) !important;
}
.ML__tooltip, [role="tooltip"], .ML__popover[role="tooltip"], .popover, [data-ml-tooltip] {
z-index: calc(var(--ck-z-panel) + 2000) !important;
position: fixed !important;
}
.ck.ck-balloon-panel, .ck.ck-balloon-panel .ck-balloon-panel__content {
overflow: visible !important;
}
/* === MAIN DIALOG === */
.ck.ck-math-form {
display: flex;
align-items: flex-start;
flex-direction: row;
flex-wrap: nowrap;
padding: var(--ck-spacing-standard);
@media screen and (max-width: 600px) {
flex-wrap: wrap;
& .ck-math-view {
flex-basis: 100%;
& .ck-labeled-view {
flex-basis: 100%;
}
& .ck-label {
flex-basis: 100%;
}
}
& .ck-button {
flex-basis: 50%;
}
}
display: flex;
flex-direction: column;
padding: var(--ck-spacing-standard);
box-sizing: border-box;
max-width: 80vw;
max-height: 80vh;
overflow: visible;
user-select: text;
}
.ck-math-tex.ck-placeholder::before {
display: none !important;
/* Scrollable content - vertical scroll, horizontal visible for tooltips */
.ck-math-view {
overflow-y: auto;
overflow-x: visible;
display: flex;
flex-direction: column;
flex: 1 1 auto;
gap: var(--ck-spacing-standard);
min-height: 0;
width: 100%;
}
.ck.ck-toolbar-container {
z-index: calc(var(--ck-z-panel) + 2);
/* === MATH INPUT === */
.ck.ck-math-input {
display: flex;
flex-direction: column;
gap: var(--ck-spacing-standard);
width: fit-content;
min-width: 100%;
max-width: 100%;
flex: 1 1 auto;
min-height: 0;
overflow: visible !important;
}
/* === MATHLIVE EDITOR === */
.ck.ck-math-input .ck-mathlive-container {
position: relative;
width: 100%;
min-height: 50px;
padding: var(--ck-spacing-small);
border: 1px solid var(--ck-color-input-border);
border-radius: var(--ck-border-radius);
background: var(--ck-color-input-background) !important;
transition: border-color 120ms ease;
overflow: visible !important;
clip-path: none !important;
}
.ck.ck-math-input .ck-mathlive-container:focus-within {
border-color: var(--ck-color-focus-border);
}
/* Position keyboard & menu buttons */
.ck-mathlive-container math-field::part(virtual-keyboard-toggle),
.ck-mathlive-container math-field::part(menu-toggle) {
position: absolute;
top: 8px;
}
.ck-mathlive-container math-field::part(virtual-keyboard-toggle) { right: 40px; }
.ck-mathlive-container math-field::part(menu-toggle) {
right: 8px;
display: flex !important;
visibility: visible !important;
}
/* Math field element */
.ck.ck-math-form math-field {
display: block !important;
width: 100%;
font-size: 1.5em;
background: transparent !important;
color: var(--ck-color-input-text);
border: none !important;
padding: 0;
outline: none !important;
--selection-background-color: rgba(33, 150, 243, 0.2);
--selection-color: inherit;
--contains-highlight-background-color: rgba(0, 0, 0, 0.05);
}
/* === LATEX TEXTAREA === */
.ck.ck-math-input .ck-latex-wrapper {
display: flex;
flex-direction: column;
width: fit-content;
min-width: 100%;
max-width: 100%;
padding: var(--ck-spacing-small);
border: 1px solid var(--ck-color-input-border);
border-radius: var(--ck-border-radius);
background: var(--ck-color-input-background) !important;
transition: border-color 120ms ease;
box-sizing: border-box;
}
.ck.ck-math-input .ck-latex-wrapper:focus-within {
border-color: var(--ck-color-focus-border);
}
.ck.ck-math-input .ck-latex-label {
font-size: 12px;
font-weight: 600;
color: var(--ck-color-text);
opacity: 0.8;
margin: 0 0 var(--ck-spacing-small) 0;
flex-shrink: 0;
}
.ck.ck-math-input .ck-latex-textarea {
width: fit-content;
min-width: 100%;
max-width: 100%;
min-height: 60px;
max-height: calc(80vh - 300px);
resize: both;
overflow: auto;
font-family: 'Courier New', monospace;
font-size: 0.95em;
background: transparent !important;
color: var(--ck-color-input-text);
border: none !important;
padding: 0;
outline: none !important;
box-sizing: border-box;
}
/* === DISPLAY TOGGLE === */
.ck-button-display-toggle {
align-self: flex-start;
padding: var(--ck-spacing-small) var(--ck-spacing-standard);
background: var(--ck-color-input-background);
color: var(--ck-color-text);
border: 1px solid var(--ck-color-input-border);
border-radius: var(--ck-border-radius);
cursor: pointer;
transition: all 0.2s ease;
}
.ck-button-display-toggle:hover { background: var(--ck-color-focus-border); }
/* === PREVIEW === */
.ck-math-preview,
.ck.ck-math-preview {
width: 100%;
min-height: 40px;
max-height: none !important;
height: auto !important;
padding: var(--ck-spacing-small);
background: transparent !important;
border: none !important;
display: block;
text-align: left;
overflow-x: auto !important;
overflow-y: visible !important;
flex-shrink: 0;
}
/* Center equation when in display mode */
.ck-math-preview[data-display="true"],
.ck.ck-math-preview[data-display="true"] {
text-align: center;
}
.ck-math-preview.ck-error, .ck-math-render-error {
border-color: var(--ck-color-error-text);
background: var(--ck-color-base-background);
color: var(--ck-color-error-text);
}
/* === BUTTONS === */
.ck-math-button-row {
display: flex;
gap: var(--ck-spacing-standard);
justify-content: flex-end;
margin-top: var(--ck-spacing-standard);
}
.ck-button-save, .ck-button-cancel {
padding: var(--ck-spacing-small) var(--ck-spacing-standard);
border: 1px solid var(--ck-color-input-border);
border-radius: var(--ck-border-radius);
cursor: pointer;
font-weight: 500;
}
.ck-button-save {
background: var(--ck-color-focus-border);
color: white;
}
.ck-button-cancel {
background: var(--ck-color-input-background);
color: var(--ck-color-text);
}
.ck-button-save:hover { opacity: 0.9; }
.ck-button-cancel:hover { background: var(--ck-color-base-background); }
/* === OVERFLOW FIX: Allow tooltips to escape === */
.ck.ck-balloon-panel,
.ck.ck-balloon-panel .ck-balloon-panel__content,
.ck.ck-math-form,
.ck-math-view,
.ck.ck-math-input,
.ck.ck-math-input .ck-mathlive-container {
overflow: visible !important;
clip-path: none !important;
}

View File

@@ -22,6 +22,9 @@ export default defineConfig( {
include: [
'tests/**/*.[jt]s'
],
exclude: [
'tests/setup.ts'
],
globals: true,
watch: false,
coverage: {

View File

@@ -50,6 +50,11 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, LocaleMapping | null> = {
coreTranslation: () => import("ckeditor5/translations/ja.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ja.js"),
},
pl: {
languageCode: "pl",
coreTranslation: () => import("ckeditor5/translations/pl.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/pl.js"),
},
pt: {
languageCode: "pt",
coreTranslation: () => import("ckeditor5/translations/pt.js"),

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/commons",
"version": "0.101.1",
"version": "0.101.3",
"description": "Shared library between the clients (e.g. browser, Electron) and the server, mostly for type definitions and utility methods.",
"private": true,
"type": "module",

View File

@@ -22,6 +22,9 @@ type Labels = {
pageUrl: string;
dateNote: string;
// Tree specific
subtreeHidden: boolean;
// Search
searchString: string;
ancestorDepth: string;

View File

@@ -18,5 +18,5 @@ export const ALLOWED_PROTOCOLS = [
'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message',
'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp',
'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo',
'mid', 'obsidian'
'logseq', 'mid', 'obsidian'
];

90
pnpm-lock.yaml generated
View File

@@ -104,8 +104,8 @@ importers:
specifier: 0.18.0
version: 0.18.0
rollup-plugin-webpack-stats:
specifier: 2.1.8
version: 2.1.8(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
specifier: 2.1.9
version: 2.1.9(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
tslib:
specifier: 2.8.1
version: 2.8.1
@@ -1073,6 +1073,9 @@ importers:
'@ckeditor/ckeditor5-icons':
specifier: 47.3.0
version: 47.3.0
mathlive:
specifier: 0.108.2
version: 0.108.2
devDependencies:
'@ckeditor/ckeditor5-dev-build-tools':
specifier: 54.2.3
@@ -2129,6 +2132,10 @@ packages:
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
'@cortex-js/compute-engine@0.30.2':
resolution: {integrity: sha512-Zx+iisk9WWdbxjm8EYsneIBszvjfUs7BHNwf1jBtSINIgfWGpHrTTq9vW0J59iGCFt6bOFxbmWyxNMRSmksHMA==}
engines: {node: '>=21.7.3', npm: '>=10.5.0'}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -7076,6 +7083,10 @@ packages:
compare-versions@6.1.1:
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
complex-esm@2.1.1-esm1:
resolution: {integrity: sha512-IShBEWHILB9s7MnfyevqNGxV0A1cfcSnewL/4uPFiSxkcQL4Mm3FxJ0pXMtCXuWLjYz3lRRyk6OfkeDZcjD6nw==}
engines: {node: '>=16.14.2', npm: '>=8.5.0'}
component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
@@ -10269,6 +10280,9 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mathlive@0.108.2:
resolution: {integrity: sha512-GIZkfprGTxrbHckOvwo92ZmOOxdD018BHDzlrEwYUU+pzR5KabhqI1s43lxe/vqXdF5RLiQKgDcuk5jxEjhkYg==}
mathml-tag-names@2.1.3:
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
@@ -12389,8 +12403,8 @@ packages:
resolution: {integrity: sha512-EsoOi8moHN6CAYyTZipxDDVTJn0j2nBCWor4wRU45RQ8ER2qREDykXLr3Ulz6hBh6oBKCFTQIjo21i0FXNo/IA==}
hasBin: true
rollup-plugin-stats@1.5.3:
resolution: {integrity: sha512-0IYVGhsFTjcddpqcElzU7Mi4vmDLihCCTH5QgCCgWpNY1VKMXVoEpxmCmGjivtJKLzI6t5QIicsPBC93UWWN2g==}
rollup-plugin-stats@1.5.4:
resolution: {integrity: sha512-b1hYagYLTyr8mCVUb7e1x9fjxOXFyeWmV9hIr7vYqq/agN+WDaGNzz+KmM3GAx0KGGI2qllOL+zAUi/l39s/Sg==}
engines: {node: '>=18'}
peerDependencies:
rolldown: ^1.0.0-beta.0
@@ -12416,8 +12430,8 @@ packages:
peerDependencies:
rollup: ^3.0.0||^4.0.0
rollup-plugin-webpack-stats@2.1.8:
resolution: {integrity: sha512-agc1OE+QwG3sGeTSdruh16DkxPb6QkgR7I3gntPDFHMXsK1bR2ADHUVod1eoE+epAOqiv3idx/hcSqZAI3a1yg==}
rollup-plugin-webpack-stats@2.1.9:
resolution: {integrity: sha512-ft1vdp3xPjE+zw8A22yCToo5cpymoWCjNDefWNO1awywsDrSDoRJhkoZTENkhJwmfh6oe5ztpGu7PfnJOMXc2g==}
engines: {node: '>=18'}
peerDependencies:
rolldown: ^1.0.0-beta.0
@@ -15285,8 +15299,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-block-quote@47.3.0':
dependencies:
@@ -15297,8 +15309,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-bookmark@47.3.0':
dependencies:
@@ -15361,8 +15371,6 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-code-block@47.3.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
dependencies:
@@ -15536,6 +15544,8 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-easy-image@47.3.0':
dependencies:
@@ -15573,6 +15583,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-inline@47.3.0':
dependencies:
@@ -15582,8 +15594,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-multi-root@47.3.0':
dependencies:
@@ -15606,8 +15616,6 @@ snapshots:
'@ckeditor/ckeditor5-table': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-emoji@47.3.0':
dependencies:
@@ -15633,6 +15641,8 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.3.0
'@ckeditor/ckeditor5-engine': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-essentials@47.3.0':
dependencies:
@@ -15664,8 +15674,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-export-word@47.3.0':
dependencies:
@@ -15690,6 +15698,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-font@47.3.0':
dependencies:
@@ -15699,8 +15709,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-footnotes@47.3.0':
dependencies:
@@ -15731,8 +15739,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-heading@47.3.0':
dependencies:
@@ -15764,6 +15770,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-html-embed@47.3.0':
dependencies:
@@ -15823,8 +15831,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-indent@47.3.0':
dependencies:
@@ -15859,6 +15865,8 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-link@47.3.0':
dependencies:
@@ -15885,6 +15893,8 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-list@47.3.0':
dependencies:
@@ -15937,8 +15947,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-mention@47.3.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)':
dependencies:
@@ -15948,8 +15956,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-merge-fields@47.3.0':
dependencies:
@@ -15962,8 +15968,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-minimap@47.3.0':
dependencies:
@@ -15972,8 +15976,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-operations-compressor@47.3.0':
dependencies:
@@ -16093,8 +16095,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-restricted-editing@47.3.0':
dependencies:
@@ -16216,8 +16216,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-template@47.3.0':
dependencies:
@@ -16499,6 +16497,11 @@ snapshots:
'@colors/colors@1.5.0': {}
'@cortex-js/compute-engine@0.30.2':
dependencies:
complex-esm: 2.1.1-esm1
decimal.js: 10.6.0
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@@ -22305,6 +22308,8 @@ snapshots:
compare-versions@6.1.1: {}
complex-esm@2.1.1-esm1: {}
component-emitter@1.3.1: {}
compress-commons@6.0.2:
@@ -22991,8 +22996,7 @@ snapshots:
decimal.js@10.5.0: {}
decimal.js@10.6.0:
optional: true
decimal.js@10.6.0: {}
decko@1.2.0: {}
@@ -26338,6 +26342,10 @@ snapshots:
math-intrinsics@1.1.0: {}
mathlive@0.108.2:
dependencies:
'@cortex-js/compute-engine': 0.30.2
mathml-tag-names@2.1.3: {}
mdast-util-find-and-replace@3.0.2:
@@ -28808,7 +28816,7 @@ snapshots:
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.29
optional: true
rollup-plugin-stats@1.5.3(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
rollup-plugin-stats@1.5.4(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
optionalDependencies:
rolldown: 1.0.0-beta.29
rollup: 4.52.0
@@ -28841,9 +28849,9 @@ snapshots:
'@rollup/pluginutils': 5.1.4(rollup@4.52.0)
rollup: 4.52.0
rollup-plugin-webpack-stats@2.1.8(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
rollup-plugin-webpack-stats@2.1.9(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
dependencies:
rollup-plugin-stats: 1.5.3(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
rollup-plugin-stats: 1.5.4(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
optionalDependencies:
rolldown: 1.0.0-beta.29
rollup: 4.52.0

View File

@@ -0,0 +1,33 @@
import { readFileSync } from "fs";
import { join } from "path";
const projectRoot = join(__dirname, '..');
const filesToCheck = [
'package.json',
'apps/server/package.json',
'apps/client/package.json',
'apps/desktop/package.json',
'packages/commons/package.json',
]
function main() {
const expectedVersion = process.argv[2];
if (!expectedVersion) {
console.error('Expected version argument is missing.');
process.exit(1);
}
for (const fileToCheck of filesToCheck) {
const packageJsonPath = join(projectRoot, fileToCheck);
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const version = packageJson.version;
if (version !== expectedVersion) {
console.error(`Version mismatch in ${fileToCheck}: expected ${expectedVersion}, found ${version}`);
process.exit(1);
}
}
console.log('All versions are consistent:', expectedVersion);
}
main();