Compare commits

..

148 Commits

Author SHA1 Message Date
renovate[bot]
4da6294ef2 chore(deps): update dependency node-abi to v4.26.0 2026-01-21 01:35:35 +00:00
Elian Doran
60c789b6c7 fix(client): production affected by cache of index JS 2026-01-20 16:33:46 +02:00
Elian Doran
f83d95136d fix(codemirror): ctrl+enter generates newline 2026-01-20 16:11:40 +02:00
Elian Doran
f96ed0af26 Revert "fix(shortcuts): triggering in bubbling phase, not capturing"
This reverts commit 711828d6b4.
2026-01-20 15:16:51 +02:00
Elian Doran
1539664026 Revert "test(client): fix broken tests after change in shortcut behaviour"
This reverts commit e33950e000.
2026-01-20 15:16:32 +02:00
Elian Doran
8aff775d0e chore(deps): update pnpm to v10.28.1 (#8435) 2026-01-20 12:12:26 +02:00
Elian Doran
94248eafe9 chore(deps): update dependency vite-plugin-static-copy to v3.1.5 (#8434) 2026-01-20 12:12:06 +02:00
Elian Doran
02335bba3f chore(deps): update dependency turnish to v1.8.0 (#8437) 2026-01-20 12:11:38 +02:00
renovate[bot]
dad9578b83 chore(deps): update dependency turnish to v1.8.0 2026-01-20 01:16:12 +00:00
renovate[bot]
c043788b09 chore(deps): update pnpm to v10.28.1 2026-01-20 01:15:00 +00:00
renovate[bot]
e5bc416b46 chore(deps): update dependency vite-plugin-static-copy to v3.1.5 2026-01-20 01:14:50 +00:00
Elian Doran
c97f52da36 docs(user): mention breaking change update for scripts 2026-01-19 19:02:54 +02:00
Elian Doran
1661c3292a fix(deps): update dependency jquery to v4 (#8424) 2026-01-19 18:53:11 +02:00
Elian Doran
4e80c07630 Translations update from Hosted Weblate (#8427) 2026-01-19 18:42:06 +02:00
Elian Doran
d43309947e fix(client): polyfill removed jQuery methods 2026-01-19 18:39:22 +02:00
Baris Konag
c7cc702c4a Translated using Weblate (Turkish)
Currently translated at 10.5% (16 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/tr/
2026-01-19 17:22:17 +01:00
Baris Konag
c304753ffc Translated using Weblate (Turkish)
Currently translated at 4.3% (17 of 388 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/tr/
2026-01-19 17:22:17 +01:00
Toto Yullian
da59c14231 Translated using Weblate (Indonesian)
Currently translated at 18.1% (21 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/id/
2026-01-19 17:22:16 +01:00
Hosted Weblate
e0b3e41c9e 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-19 17:22:15 +01:00
Elian Doran
5d1a63bce0 fix(print): crash when printing presentation 2026-01-19 14:55:33 +02:00
Elian Doran
84cf4ef4a3 fix(print): only first page is shown 2026-01-19 13:51:27 +02:00
Elian Doran
ec4b6f0a90 chore(deps): update dependency happy-dom to v20.3.3 (#8426) 2026-01-19 08:48:02 +02:00
renovate[bot]
60dbdbeb71 chore(deps): update dependency happy-dom to v20.3.3 2026-01-19 02:05:32 +00:00
Elian Doran
418a546583 fix(print): clicking inside print preview hides it 2026-01-18 22:39:09 +02:00
Elian Doran
e6380b87b6 fix(print): some collections not rendered at all 2026-01-18 22:26:13 +02:00
Elian Doran
1d3d214101 fix(print): not printing at all 2026-01-18 22:05:16 +02:00
Elian Doran
a38067560b SQL console improvements (#8418) 2026-01-18 18:52:14 +02:00
Elian Doran
eac7235199 chore(sql_console): address requested changes 2026-01-18 18:38:14 +02:00
Elian Doran
4c55e857b8 fix(server): build failing due to import 2026-01-18 18:33:48 +02:00
Elian Doran
e33950e000 test(client): fix broken tests after change in shortcut behaviour 2026-01-18 18:22:21 +02:00
Elian Doran
fc0ccbfcf5 chore(sql_console): address requested changes 2026-01-18 18:19:35 +02:00
Elian Doran
4a82bbb035 docs(user): update SQL console based on new interaction 2026-01-18 18:06:30 +02:00
Elian Doran
57d894e765 feat(sql_console): enable read-only for saved notes 2026-01-18 17:47:09 +02:00
Elian Doran
97dfad419c docs(user): refresh photo for SQL console 2026-01-18 17:35:01 +02:00
Elian Doran
bfc521fdc0 fix(sql_console): enforce vertical layout 2026-01-18 17:11:03 +02:00
Elian Doran
197fa90176 style(sql_console): improve style for frozen cell 2026-01-18 13:09:46 +02:00
Elian Doran
0844914e11 style(sql_console): improve style for highlighted range 2026-01-18 13:05:18 +02:00
Elian Doran
c376b0bbe2 style(sql_console): improve filter spacing 2026-01-18 12:35:42 +02:00
Elian Doran
4491086c55 style(sql_console): improve header & footer inputs 2026-01-18 12:34:00 +02:00
Elian Doran
791697369d style(sql_console): remove background for footer 2026-01-18 12:22:53 +02:00
Elian Doran
28d0bfd229 chore(sql_console): reducing padding in footer 2026-01-18 12:12:05 +02:00
Elian Doran
8182a04eae fix(sql_console): not refreshing when switching between notes 2026-01-18 12:09:01 +02:00
Elian Doran
711828d6b4 fix(shortcuts): triggering in bubbling phase, not capturing 2026-01-18 12:07:23 +02:00
Elian Doran
69e88c1d9f chore(sql_console): set gutter color 2026-01-18 11:48:34 +02:00
Elian Doran
748b87da9a feat(sql_console): improve display for statements 2026-01-18 11:34:31 +02:00
Elian Doran
94dca4cd87 feat(sql_console): report errors inline 2026-01-18 11:13:31 +02:00
Elian Doran
7179701e0f feat(sql_console): improve no results 2026-01-18 10:58:32 +02:00
Elian Doran
af5061646c feat(sql_console): add not yet executed message 2026-01-18 10:36:44 +02:00
Elian Doran
9c4163ad3a feat(sql_console): page size selector 2026-01-18 10:24:55 +02:00
Elian Doran
46c3f5296a chore(sql_console): full-height table 2026-01-18 10:20:15 +02:00
Elian Doran
ebadcfd844 feat(sql_console): enable pagination 2026-01-18 10:14:55 +02:00
Elian Doran
b7d4947462 Merge remote-tracking branch 'origin/main' into feature/sql_console_improvements 2026-01-18 10:08:48 +02:00
Elian Doran
a599526dea chore(deps): update dependency @smithy/middleware-retry to v4.4.24 (#8420) 2026-01-18 08:34:59 +02:00
Elian Doran
c4f166fe12 chore(deps): update dependency webdriverio to v9.23.2 (#8421) 2026-01-18 08:34:19 +02:00
Elian Doran
a8ae91aa3b Translations update from Hosted Weblate (#8425) 2026-01-18 08:33:02 +02:00
Elian Doran
82b3692acb fix(deps): update dependency better-sqlite3 to v12.6.2 (#8422) 2026-01-18 08:32:12 +02:00
Hosted Weblate
432c054b68 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-18 06:25:42 +00:00
Elian Doran
dcd8bfa255 fix(deps): update dependency mind-elixir to v5.6.1 (#8423) 2026-01-18 08:25:28 +02:00
renovate[bot]
c287a2ae97 fix(deps): update dependency mind-elixir to v5.6.1 2026-01-18 05:39:59 +00:00
renovate[bot]
8fdadb3798 fix(deps): update dependency jquery to v4 2026-01-18 01:40:46 +00:00
renovate[bot]
3fce4fc66c fix(deps): update dependency better-sqlite3 to v12.6.2 2026-01-18 01:39:14 +00:00
renovate[bot]
d83d7ed106 chore(deps): update dependency webdriverio to v9.23.2 2026-01-18 01:38:26 +00:00
renovate[bot]
1b812f1886 chore(deps): update dependency @smithy/middleware-retry to v4.4.24 2026-01-18 01:37:35 +00:00
Elian Doran
56fcc7adcc chore(sql_console): fix lint warnings 2026-01-17 22:41:55 +02:00
Elian Doran
fb0c7359f1 chore(sql_console): fix typecheck issue 2026-01-17 22:39:32 +02:00
Elian Doran
4c4e5b85e9 chore(sql_console): integrate table reference 2026-01-17 22:38:18 +02:00
Elian Doran
476247beb5 feat(sql_console): increase size for results 2026-01-17 22:29:20 +02:00
Elian Doran
2c87f609f3 feat(sql_console): add filter 2026-01-17 22:13:13 +02:00
Elian Doran
bc79ff6845 feat(sql_console): row numbers 2026-01-17 22:09:24 +02:00
Elian Doran
f10373d54f feat(sql_console): add clipboard 2026-01-17 22:02:47 +02:00
Elian Doran
630d16b722 feat(sql_console): enable sorting 2026-01-17 21:47:02 +02:00
Elian Doran
769f3db21c feat(sql_console): make columns resizable 2026-01-17 21:45:10 +02:00
Elian Doran
c6896a4b33 feat(sql_console): reduce column width 2026-01-17 21:43:21 +02:00
Elian Doran
7c18025098 feat(sql_console): reduce spacing to fit more content 2026-01-17 21:41:06 +02:00
Elian Doran
6ae74b3181 feat(sql_console): make scrolls and headers always visible 2026-01-17 21:35:58 +02:00
Elian Doran
2ecfbbf284 feat(sql_console): improve fit & solve build error 2026-01-17 21:26:44 +02:00
Elian Doran
781de9a1fb feat(sql_console): basic integration of Tabulator 2026-01-17 21:22:22 +02:00
Elian Doran
6972a4b901 fix(note_detail): preview leaks between mermaid & SQL console 2026-01-17 20:58:26 +02:00
Elian Doran
52ed1750ac fix(sql_console): runtime error for inline title 2026-01-17 20:52:53 +02:00
Elian Doran
9010e0b1ce chore(sql_console): reverse preview and code sections 2026-01-17 20:44:03 +02:00
Elian Doran
5053e74447 fix(sql_console): note type switcher showing up 2026-01-17 20:41:45 +02:00
Elian Doran
f294276849 fix(sql_console): full-height not respected 2026-01-17 20:38:44 +02:00
Elian Doran
0740788cc8 chore(sql_console): link stylesheet 2026-01-17 20:32:28 +02:00
Elian Doran
9bac07ce62 chore(sql_console): integrate results into preview of split 2026-01-17 20:29:15 +02:00
Elian Doran
3d8289d394 chore(note_detail): get code editor to show 2026-01-17 20:12:16 +02:00
Elian Doran
5a60fdad8a chore(note_detail): map SQL console to own type widget 2026-01-17 20:09:25 +02:00
Elian Doran
62cca5a96b Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-01-17 18:57:03 +02:00
Elian Doran
74548d638e docs(user): sync 2026-01-17 18:13:40 +02:00
Elian Doran
e0ccf30f4f docs(user): add missing links from Evernote documentation 2026-01-17 17:59:30 +02:00
Elian Doran
d2c6081537 feat(edit-docs): remove data-list-item-id 2026-01-17 17:54:10 +02:00
Elian Doran
bd933f2c4c Translations update from Hosted Weblate (#8416) 2026-01-17 17:43:15 +02:00
Yatrik Patel
1d898d618e Translated using Weblate (Hindi)
Currently translated at 7.7% (30 of 388 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-17 15:40:43 +00:00
Yatrik Patel
11c8e5b3b2 Translated using Weblate (Hindi)
Currently translated at 35.5% (54 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-17 15:40:43 +00:00
Hosted Weblate
fe4c3ffecb 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-17 15:40:42 +00:00
Elian Doran
334a96186e chore(deps): update dependency electron to v40 (#8415) 2026-01-17 17:40:34 +02:00
Elian Doran
34b2df705b Fixes to Markdown export (#8417) 2026-01-17 14:19:36 +02:00
Elian Doran
5a7fc1c8b6 chore(markdown): address requested changes 2026-01-17 13:22:24 +02:00
Elian Doran
fabab6abb1 refactor(export/markdown): spacing issues 2026-01-17 13:16:21 +02:00
Elian Doran
0c9c20c0c5 docs(user): fix escapes 2026-01-17 13:11:53 +02:00
Elian Doran
67cc1113b1 chore(export/markdown): render emphasis with underscore 2026-01-17 13:05:29 +02:00
Elian Doran
3aacd255f4 chore(export/markdown): add test for jQuery-like text inside table 2026-01-17 12:58:24 +02:00
renovate[bot]
ccfda21413 chore(deps): update dependency electron to v40 2026-01-17 10:53:59 +00:00
Elian Doran
46c88506cc chore(deps): update dependency ejs to v4 (#8396) 2026-01-17 12:52:06 +02:00
Elian Doran
51157e1979 fix(export/markdown): error due to namespace usage 2026-01-17 12:47:34 +02:00
Elian Doran
bfb6d975ff fix(export/markdown): type error due to blankReplacement signature change 2026-01-17 12:46:55 +02:00
Elian Doran
aa01bc1457 feat(markdown): switch to turnish instead of turndown 2026-01-17 12:44:30 +02:00
Elian Doran
5600f1b7b1 chore(deps): update dependency node-abi to v4.25.0 (#8414) 2026-01-17 12:17:38 +02:00
Elian Doran
a169db807c fix(server): crashing due to EJS handling 2026-01-17 12:17:15 +02:00
Elian Doran
f40348daff chore(deps): update dependency happy-dom to v20.3.1 (#8405) 2026-01-17 12:09:57 +02:00
renovate[bot]
f63042ef87 chore(deps): update dependency ejs to v4 2026-01-17 10:01:09 +00:00
renovate[bot]
d148c9d1c6 chore(deps): update dependency node-abi to v4.25.0 2026-01-17 10:00:12 +00:00
renovate[bot]
f72929ca13 chore(deps): update dependency happy-dom to v20.3.1 2026-01-17 09:59:13 +00:00
Elian Doran
cc3e3ca4d4 fix(deps): update ckeditor monorepo to v47.4.0 (#8395) 2026-01-17 11:55:54 +02:00
Elian Doran
8fad664a6d Translations update from Hosted Weblate (#8413) 2026-01-16 23:16:08 +02:00
Kf637
f1946c1386 Translated using Weblate (Norwegian Bokmål)
Currently translated at 6.7% (26 of 388 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/nb_NO/
2026-01-16 22:08:01 +01:00
Kf637
ea8bd0136f 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-16 22:08:00 +01:00
Elian Doran
4a3f72ae50 Translations update from Hosted Weblate (#8411) 2026-01-16 19:14:11 +02:00
Hosted Weblate
c944762ef6 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-16 14:25:37 +00:00
Elian Doran
f6924d7fda feat(import/zip): remove extension from title for PDF imports 2026-01-16 16:25:15 +02:00
Elian Doran
3a0880fcd6 feat(import/single): remove extension from title for PDF imports 2026-01-16 12:01:11 +02:00
Elian Doran
df62dc87b2 feat(notes): add default icon for PDFs 2026-01-16 11:47:42 +02:00
Elian Doran
d42679315e refactor(server): use common logic for icons 2026-01-16 11:42:37 +02:00
Elian Doran
2a19be5ab6 refactor(client): extract fnote icon logic in commons 2026-01-16 09:35:51 +02:00
renovate[bot]
33bbe994d7 fix(deps): update ckeditor monorepo to v47.4.0 2026-01-16 06:55:00 +00:00
Elian Doran
04c598caea build(deps): bump diff from 4.0.2 to 8.0.3 (#8390) 2026-01-16 08:50:51 +02:00
Elian Doran
3577688bf9 fix(deps): update dependency @codemirror/view to v6.39.11 (#8392) 2026-01-16 08:50:34 +02:00
Elian Doran
e0439655df chore(deps): update dependency @smithy/middleware-retry to v4.4.23 (#8403) 2026-01-16 08:47:12 +02:00
Elian Doran
84f944f78a chore(deps): update dependency @types/node to v24.10.9 (#8404) 2026-01-16 08:46:51 +02:00
Elian Doran
022b6df959 chore(deps): update dependency stylelint to v17 (#8406) 2026-01-16 08:46:18 +02:00
renovate[bot]
9e3e92669f chore(deps): update dependency stylelint to v17 2026-01-16 00:58:48 +00:00
renovate[bot]
35b96a71fc chore(deps): update dependency @types/node to v24.10.9 2026-01-16 00:51:41 +00:00
renovate[bot]
4b78de6726 chore(deps): update dependency @smithy/middleware-retry to v4.4.23 2026-01-16 00:51:08 +00:00
Elian Doran
4771e02909 chore(deps): update dependency happy-dom to v20.3.0 (#8393) 2026-01-15 08:49:43 +02:00
Elian Doran
03dffdb65f chore(deps): update node.js to v24.13.0 (#8394) 2026-01-15 08:49:01 +02:00
Elian Doran
859a3948cd Translations update from Hosted Weblate (#8398) 2026-01-15 08:09:44 +02:00
Hasan Kara
161aa625e6 Translated using Weblate (Turkish)
Currently translated at 9.2% (14 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/tr/
2026-01-15 04:09:54 +01:00
Hasan Kara
28fd945e80 Translated using Weblate (Turkish)
Currently translated at 3.6% (14 of 388 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/tr/
2026-01-15 04:09:53 +01:00
renovate[bot]
748fb0bf05 chore(deps): update node.js to v24.13.0 2026-01-15 02:00:34 +00:00
renovate[bot]
98e1d0afd9 chore(deps): update dependency happy-dom to v20.3.0 2026-01-15 02:00:27 +00:00
renovate[bot]
9c61ce1835 fix(deps): update dependency @codemirror/view to v6.39.11 2026-01-15 01:59:39 +00:00
dependabot[bot]
c3a5705be0 build(deps): bump diff from 4.0.2 to 8.0.3
Bumps [diff](https://github.com/kpdecker/jsdiff) from 4.0.2 to 8.0.3.
- [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md)
- [Commits](https://github.com/kpdecker/jsdiff/compare/v4.0.2...v8.0.3)

---
updated-dependencies:
- dependency-name: diff
  dependency-version: 8.0.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-14 22:53:04 +00:00
Elian Doran
9d0fa9f7ca Fix sorting (#7878) 2026-01-14 17:18:41 +02:00
Elian Doran
cc4ceb975e fix(tree): not reacting to note reordering (e.g. sort) 2026-01-14 17:15:29 +02:00
Elian Doran
8a6495a0bd Translations update from Hosted Weblate (#8386) 2026-01-14 16:48:46 +02:00
Elian Doran
1d95392d22 docs(user): improve Evernote documentation 2026-01-14 16:06:58 +02:00
Hosted Weblate
93dd08d629 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-14 14:06:53 +00:00
Romain DEP.
a1c0314334 chore(sorting): add test cases for previous commit and increase test coverage 2025-11-28 23:28:14 +01:00
Romain DEP.
3ecdcd9ea0 fix(sorting): BC! give precedence to #top notes over #sortFolderFirst 2025-11-28 23:22:20 +01:00
132 changed files with 3453 additions and 3028 deletions

View File

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

View File

@@ -46,7 +46,7 @@
"globals": "17.0.0",
"i18next": "25.7.4",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.27",
@@ -56,7 +56,7 @@
"mark.js": "8.11.1",
"marked": "17.0.1",
"mermaid": "11.12.2",
"mind-elixir": "5.5.0",
"mind-elixir": "5.6.1",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.2",
@@ -78,9 +78,9 @@
"@types/reveal.js": "5.2.2",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.1.0",
"happy-dom": "20.3.3",
"lightningcss": "1.30.2",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.1.4"
"vite-plugin-static-copy": "3.1.5"
}
}

View File

@@ -1,6 +1,6 @@
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import { SqlExecuteResults } from "@triliumnext/commons";
import { SqlExecuteResponse } from "@triliumnext/commons";
import type { NativeImage, TouchBar } from "electron";
import { ColumnComponent } from "tabulator-tables";
@@ -410,7 +410,7 @@ type EventMappings = {
addNewLabel: CommandData;
addNewRelation: CommandData;
sqlQueryResults: CommandData & {
results: SqlExecuteResults;
response: SqlExecuteResponse;
};
readOnlyTemporarilyDisabled: {
noteContext: NoteContext;
@@ -542,7 +542,6 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
export class AppContext extends Component {
isMainWindow: boolean;
windowId: string;
components: Component[];
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
tabManager!: TabManager;
@@ -551,11 +550,10 @@ export class AppContext extends Component {
lastSearchString?: string;
constructor(isMainWindow: boolean, windowId: string) {
constructor(isMainWindow: boolean) {
super();
this.isMainWindow = isMainWindow;
this.windowId = windowId;
// non-widget/layout components needed for the application
this.components = [];
this.beforeUnloadListeners = [];
@@ -685,7 +683,8 @@ export class AppContext extends Component {
this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener);
}
}
const appContext = new AppContext(window.glob.isMainWindow, window.glob.windowId);
const appContext = new AppContext(window.glob.isMainWindow);
// we should save all outstanding changes before the page/app is closed
$(window).on("beforeunload", () => {

View File

@@ -1,16 +1,17 @@
import utils from "../services/utils.js";
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
import bundleService from "../services/bundle.js";
import dateNoteService from "../services/date_notes.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import linkService from "../services/link.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js";
import toastService from "../services/toast.js";
import utils from "../services/utils.js";
import ws from "../services/ws.js";
import appContext, { type NoteCommandData } from "./app_context.js";
import Component from "./component.js";
import toastService from "../services/toast.js";
import ws from "../services/ws.js";
import bundleService from "../services/bundle.js";
import froca from "../services/froca.js";
import linkService from "../services/link.js";
import { t } from "../services/i18n.js";
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
export default class Entrypoints extends Component {
constructor() {
@@ -142,15 +143,14 @@ export default class Entrypoints extends Component {
}
async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) {
const extraWindowId = utils.randomString(4);
const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope });
if (utils.isElectron()) {
const { ipcRenderer } = utils.dynamicRequire("electron");
ipcRenderer.send("create-extra-window", { extraWindowId, extraWindowHash });
ipcRenderer.send("create-extra-window", { extraWindowHash });
} else {
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=${extraWindowId}${extraWindowHash}`;
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`;
window.open(url, "", "width=1000,height=800");
}
@@ -188,13 +188,8 @@ export default class Entrypoints extends Component {
} else if (note.mime.endsWith("env=backend")) {
await server.post(`script/run/${note.noteId}`);
} else if (note.mime === "text/x-sqlite;schema=trilium") {
const resp = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
if (!resp.success) {
toastService.showError(t("entrypoints.sql-error", { message: resp.error }));
}
await appContext.triggerEvent("sqlQueryResults", { ntxId: ntxId, results: resp.results });
const response = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
await appContext.triggerEvent("sqlQueryResults", { ntxId, response });
}
toastService.showMessage(t("entrypoints.note-executed"));

View File

@@ -11,8 +11,6 @@ import linkService from "../services/link.js";
import type { EventData } from "./app_context.js";
import type FNote from "../entities/fnote.js";
const MAX_SAVED_WINDOWS = 10;
interface TabState {
contexts: NoteContext[];
position: number;
@@ -27,13 +25,6 @@ interface NoteContextState {
viewScope: Record<string, any>;
}
interface WindowState {
windowId: string;
createdAt: number;
closedAt: number;
contexts: NoteContextState[];
}
export default class TabManager extends Component {
public children: NoteContext[];
public mutex: Mutex;
@@ -50,6 +41,9 @@ export default class TabManager extends Component {
this.recentlyClosedTabs = [];
this.tabsUpdate = new SpacedUpdate(async () => {
if (!appContext.isMainWindow) {
return;
}
if (options.is("databaseReadonly")) {
return;
}
@@ -58,21 +52,9 @@ export default class TabManager extends Component {
.map((nc) => nc.getPojoState())
.filter((t) => !!t);
// Update the current windows openNoteContexts in options
const savedWindows = options.getJson("openNoteContexts") || [];
const win = savedWindows.find((w: WindowState) => w.windowId === appContext.windowId);
if (win) {
win.contexts = openNoteContexts;
} else {
savedWindows.push({
windowId: appContext.windowId,
createdAt: Date.now(),
closedAt: 0,
contexts: openNoteContexts
} as WindowState);
}
await options.save("openNoteContexts", JSON.stringify(savedWindows));
await server.put("options", {
openNoteContexts: JSON.stringify(openNoteContexts)
});
});
appContext.addBeforeUnloadListener(this);
@@ -87,13 +69,8 @@ export default class TabManager extends Component {
}
async loadTabs() {
// Get the current windows openNoteContexts
const savedWindows = options.getJson("openNoteContexts") || [];
const currentWin = savedWindows.find(w => w.windowId === appContext.windowId);
const openNoteContexts = currentWin ? currentWin.contexts : undefined;
try {
const noteContextsToOpen = openNoteContexts || [];
const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || [];
// preload all notes at once
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
@@ -142,51 +119,6 @@ export default class TabManager extends Component {
}
});
// Save window contents
if (currentWin as WindowState) {
currentWin.createdAt = Date.now();
currentWin.closedAt = 0;
currentWin.contexts = filteredNoteContexts;
} else {
if (savedWindows?.length >= MAX_SAVED_WINDOWS) {
// Filter out the oldest entry
// 1) Never remove the "main" window
// 2) Prefer removing the oldest closed window (closedAt !== 0)
// 3) If no closed window exists, remove the window with the oldest created window
let oldestClosedIndex = -1;
let oldestClosedTime = Infinity;
let oldestCreatedIndex = -1;
let oldestCreatedTime = Infinity;
savedWindows.forEach((w: WindowState, i: number) => {
if (w.windowId === "main") return;
if (w.closedAt !== 0) {
if (w.closedAt < oldestClosedTime) {
oldestClosedTime = w.closedAt;
oldestClosedIndex = i;
}
} else {
if (w.createdAt < oldestCreatedTime) {
oldestCreatedTime = w.createdAt;
oldestCreatedIndex = i;
}
}
});
const indexToRemove = oldestClosedIndex !== -1 ? oldestClosedIndex : oldestCreatedIndex;
if (indexToRemove !== -1) {
savedWindows.splice(indexToRemove, 1);
}
}
savedWindows.push({
windowId: appContext.windowId,
createdAt: Date.now(),
closedAt: 0,
contexts: filteredNoteContexts
} as WindowState);
}
await options.save("openNoteContexts", JSON.stringify(savedWindows));
// if there's a notePath in the URL, make sure it's open and active
// (useful, for e.g., opening clipped notes from clipper or opening link in an extra window)
if (parsedFromUrl.notePath) {

View File

@@ -1,4 +1,4 @@
import { MIME_TYPES_DICT } from "@triliumnext/commons";
import { getNoteIcon } from "@triliumnext/commons";
import cssClassManager from "../services/css_class_manager.js";
import type { Froca } from "../services/froca-interface.js";
@@ -13,25 +13,6 @@ import type { AttributeType, default as FAttribute } from "./fattribute.js";
const LABEL = "label";
const RELATION = "relation";
export const NOTE_TYPE_ICONS = {
file: "bx bx-file",
image: "bx bx-image",
code: "bx bx-code",
render: "bx bx-extension",
search: "bx bx-file-find",
relationMap: "bx bxs-network-chart",
book: "bx bx-book",
noteMap: "bx bxs-network-chart",
mermaid: "bx bx-selection",
canvas: "bx bx-pen",
webView: "bx bx-globe-alt",
launcher: "bx bx-link",
doc: "bx bxs-file-doc",
contentWidget: "bx bxs-widget",
mindMap: "bx bx-sitemap",
aiChat: "bx bx-bot"
};
/**
* There are many different Note types, some of which are entirely opaque to the
* end user. Those types should be used only for checking against, they are
@@ -582,32 +563,18 @@ export default class FNote {
}
getIcon() {
return `tn-icon ${this.#getIconInternal()}`;
}
#getIconInternal() {
const iconClassLabels = this.getLabels("iconClass");
const workspaceIconClass = this.getWorkspaceIconClass();
if (iconClassLabels && iconClassLabels.length > 0) {
return iconClassLabels[0].value;
} else if (workspaceIconClass) {
return workspaceIconClass;
} else if (this.noteId === "root") {
return "bx bx-home-alt-2";
}
if (this.noteId === "_share") {
return "bx bx-share-alt";
} else if (this.type === "text") {
if (this.isFolder()) {
return "bx bx-folder";
}
return "bx bx-note";
} else if (this.type === "code") {
const correspondingMimeType = MIME_TYPES_DICT.find(m => m.mime === this.mime);
return correspondingMimeType?.icon ?? NOTE_TYPE_ICONS.code;
}
return NOTE_TYPE_ICONS[this.type];
const icon = getNoteIcon({
noteId: this.noteId,
type: this.type,
mime: this.mime,
iconClass: iconClassLabels.length > 0 ? iconClassLabels[0].value : undefined,
workspaceIconClass,
isFolder: this.isFolder.bind(this)
});
return `tn-icon ${icon}`;
}
getColorClass() {

View File

@@ -16,6 +16,17 @@ async function initJQuery() {
const $ = (await import("jquery")).default;
window.$ = $;
window.jQuery = $;
// Polyfill removed jQuery methods for autocomplete.js compatibility
($ as any).isArray = Array.isArray;
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
($ as any).isPlainObject = function(obj: any) {
if (obj == null || typeof obj !== 'object') { return false; }
const proto = Object.getPrototypeOf(obj);
if (proto === null) { return true; }
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor === 'function' && Ctor === Object;
};
}
async function setupGlob() {
@@ -39,22 +50,25 @@ async function loadBootstrapCss() {
}
function loadStylesheets() {
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
const { device, assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
const cssToLoad: string[] = [];
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
cssToLoad.push(`api/fonts`);
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
if (themeCssUrl) {
cssToLoad.push(themeCssUrl);
if (device !== "print") {
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
cssToLoad.push(`api/fonts`);
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
if (themeCssUrl) {
cssToLoad.push(themeCssUrl);
}
if (themeUseNextAsBase === "next") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
} else if (themeUseNextAsBase === "next-dark") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
} else if (themeUseNextAsBase === "next-light") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
}
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
}
if (themeUseNextAsBase === "next") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
} else if (themeUseNextAsBase === "next-dark") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
} else if (themeUseNextAsBase === "next-light") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
}
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
for (const href of cssToLoad) {
const linkEl = document.createElement("link");
@@ -71,7 +85,7 @@ function loadIcons() {
}
function setBodyAttributes() {
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale, isMainWindow } = window.glob;
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
const classesToSet = [
device,
`heading-style-${headingStyle}`,
@@ -79,8 +93,7 @@ function setBodyAttributes() {
`platform-${platform}`,
isElectron && "electron",
hasNativeTitleBar && "native-titlebar",
hasBackgroundEffects && "background-effects",
!isMainWindow && 'extra-window'
hasBackgroundEffects && "background-effects"
].filter(Boolean) as string[];
for (const classToSet of classesToSet) {
@@ -92,10 +105,17 @@ function setBodyAttributes() {
}
async function loadScripts() {
if (glob.device === "mobile") {
await import("./mobile.js");
} else {
await import("./desktop.js");
switch (glob.device) {
case "mobile":
await import("./mobile.js");
break;
case "print":
await import("./print.js");
break;
case "desktop":
default:
await import("./desktop.js");
break;
}
}

View File

@@ -46,8 +46,6 @@ import ScrollPadding from "../widgets/scroll_padding.js";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfo from "../widgets/shared_info.jsx";
import RightPanelContainer from "../widgets/sidebar/RightPanelContainer.jsx";
import SqlResults from "../widgets/sql_result.js";
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
import TabRowWidget from "../widgets/tab_row.js";
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
@@ -163,11 +161,9 @@ export default class DesktopLayout {
.child(<SharedInfo />)
)
.optChild(!isNewLayout, <PromotedAttributes />)
.child(<SqlTableSchemas />)
.child(<NoteDetail />)
.child(<NoteList media="screen" />)
.child(<SearchResult />)
.child(<SqlResults />)
.child(<ScrollPadding />)
)
.child(<ApiLog />)

View File

@@ -29,7 +29,9 @@ async function main() {
const froca = (await import("./services/froca")).default;
const note = await froca.getNote(noteId);
render(<App note={note} noteId={noteId} />, document.body);
const bodyWrapper = document.createElement("div");
render(<App note={note} noteId={noteId} />, bodyWrapper);
document.body.appendChild(bodyWrapper);
}
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {

View File

@@ -8,6 +8,17 @@ async function loadBootstrap() {
}
}
// Polyfill removed jQuery methods for autocomplete.js compatibility
($ as any).isArray = Array.isArray;
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
($ as any).isPlainObject = function(obj: any) {
if (obj == null || typeof obj !== 'object') { return false; }
const proto = Object.getPrototypeOf(obj);
if (proto === null) { return true; }
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor === 'function' && Ctor === Object;
};
(window as any).$ = $;
(window as any).jQuery = $;
await loadBootstrap();

View File

@@ -27,6 +27,10 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId);
} else if (ec.entityName === "options") {
const attributeEntity = ec.entity as FAttributeRow;
if (attributeEntity.name === "openNoteContexts") {
continue; // only noise
}
options.set(attributeEntity.name as OptionNames, attributeEntity.value);
loadResults.addOption(attributeEntity.name as OptionNames);
} else if (ec.entityName === "attachments") {

View File

@@ -1,5 +1,6 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import shortcuts, { isIMEComposing, keyMatches, matchesShortcut } from "./shortcuts.js";
// Mock utils module
vi.mock("./utils.js", () => ({

View File

@@ -14,13 +14,13 @@
--row-moving-background-color: var(--accented-background-color);
--row-text-color: var(--main-text-color);
--row-delimiter-color: var(--more-accented-background-color);
--cell-horiz-padding-size: 8px;
--cell-vert-padding-size: 8px;
--cell-editable-hover-outline-color: var(--main-border-color);
--cell-read-only-text-color: var(--muted-text-color);
--cell-editing-border-color: var(--main-border-color);
--cell-editing-border-width: 2px;
--cell-editing-background-color: var(--ck-color-selector-focused-cell-background);
@@ -40,10 +40,42 @@
border-bottom: var(--col-header-bottom-border);
background: var(--col-header-background-color);
color: var(--col-header-text-color);
}
font-weight: normal;
.tabulator .tabulator-col-content {
padding: 8px 4px !important;
.tabulator-col.tabulator-range-highlight {
background: inherit;
color: inherit;
font-weight: bold;
}
.tabulator-col-content {
padding: 0 !important;
.tabulator-col-title-holder {
padding: 8px 4px;
}
&:has(.tabulator-header-filter) {
.tabulator-col-title-holder {
padding: 4px;
padding-bottom: 0;
}
}
.tabulator-header-filter {
background: var(--main-background-color);
padding: 2px 1px;
input {
background: var(--main-background-color);
color: var(--main-text-color);
border: 1px solid var(--button-border-color);
border-radius: 3px;
outline: none;
padding: 2px;
}
}
}
}
@media (hover: hover) and (pointer: fine) {
@@ -80,7 +112,6 @@
.tabulator-tableholder {
padding-top: 10px;
height: unset !important; /* Don't extend on the full height */
}
/* Rows */
@@ -99,6 +130,14 @@
border-top: none;
border-bottom: 1px solid var(--row-delimiter-color);
color: var(--row-text-color);
&:last-of-type {
border-bottom: none;
}
&.tabulator-range-highlight > .tabulator-cell.tabulator-frozen {
font-weight: bold;
}
}
.tabulator-row.tabulator-row-odd {
@@ -120,11 +159,14 @@
margin-inline-end: var(--cell-editing-border-width);
}
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left,
.tabulator-row .tabulator-cell {
border-inline-end-color: transparent;
}
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
border-inline-end-color: var(--main-border-color);
}
.tabulator-row .tabulator-cell:not(.tabulator-editable) {
color: var(--cell-read-only-text-color);
}
@@ -174,10 +216,6 @@
margin: 0;
}
.tabulator .tabulator-footer {
color: var(--main-text-color);
}
/* Context menus */
.tabulator-popup-container {
@@ -192,8 +230,27 @@
}
/* Footer */
:root .tabulator .tabulator-footer {
border-top: unset;
background: transparent;
color: var(--main-text-color);
border-top: 1px solid var(--main-border-color);
padding: 10px 0;
}
.tabulator-page {
background: var(--button-background-color);
color: var(--button-text-color);
border: 1px solid var(--button-border-color);
border-radius: var(--button-border-radius);
&:hover {
border-color: var(--hover-item-border-color);
color: var(--button-text-color);
}
}
select {
background: var(--button-background-color);
color: var(--input-text-color);
border: 1px solid var(--button-border-color);
}
}

View File

@@ -13,8 +13,7 @@ function injectGlobals() {
uncheckedWindow.$ = $;
uncheckedWindow.WebSocket = () => {};
uncheckedWindow.glob = {
isMainWindow: true,
windowId: "main"
isMainWindow: true
};
}

View File

@@ -1815,7 +1815,11 @@
"configure_launchbar": "Configure Launchbar"
},
"sql_result": {
"no_rows": "No rows have been returned for this query"
"not_executed": "The query has not been executed yet.",
"no_rows": "No rows have been returned for this query",
"failed": "SQL query execution has failed",
"statement_result": "Statement result",
"execute_now": "Execute now"
},
"sql_table_schemas": {
"tables": "Tables"

View File

@@ -36,7 +36,6 @@ interface CustomGlobals {
isProtectedSessionAvailable: boolean;
isDev: boolean;
isMainWindow: boolean;
windowId: string;
maxEntityChangeIdAtLoad: number;
maxEntityChangeSyncIdAtLoad: number;
assetPath: string;

View File

@@ -7,7 +7,6 @@ import Component from "../components/component";
import NoteContext from "../components/note_context";
import FNote from "../entities/fnote";
import attributes from "../services/attributes";
import { isExperimentalFeatureEnabled } from "../services/experimental_features";
import froca from "../services/froca";
import { t } from "../services/i18n";
import { copyImageReferenceToClipboard } from "../services/image";
@@ -101,7 +100,8 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
&& note.isContentAvailable() && isDefaultViewMode;
return isEnabled && <FloatingButton

View File

@@ -265,9 +265,13 @@ function useNoteInfo() {
const [ note, setNote ] = useState<FNote | null | undefined>();
const [ type, setType ] = useState<ExtendedNoteType>();
const [ mime, setMime ] = useState<string>();
const refreshIdRef = useRef(0);
function refresh() {
const refreshId = ++refreshIdRef.current;
getExtendedWidgetType(actualNote, noteContext).then(type => {
if (refreshId !== refreshIdRef.current) return;
setNote(actualNote);
setType(type);
setMime(actualNote?.mime);
@@ -318,6 +322,8 @@ export async function getExtendedWidgetType(note: FNote | null | undefined, note
resultingType = "noteMap";
} else if (type === "text" && (await noteContext?.isReadOnly())) {
resultingType = "readOnlyText";
} else if (note.isTriliumSqlite()) {
resultingType = "sqlConsole";
} else if ((type === "code" || type === "mermaid") && (await noteContext?.isReadOnly())) {
resultingType = "readOnlyCode";
} else if (type === "text") {
@@ -342,9 +348,8 @@ export function checkFullHeight(noteContext: NoteContext | undefined, type: Exte
// https://github.com/zadam/trilium/issues/2522
const isBackendNote = noteContext?.noteId === "_backendLog";
const isSqlNote = noteContext.note?.mime === "text/x-sqlite;schema=trilium";
const isFullHeightNoteType = type && TYPE_MAPPINGS[type].isFullHeight;
return (!noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
return (!noteContext?.hasNoteList() && isFullHeightNoteType)
|| noteContext?.viewScope?.viewMode === "attachments"
|| isBackendNote;
}
@@ -358,8 +363,8 @@ function showToast(type: "printing" | "exporting_pdf", progress: number = 0) {
});
}
function handlePrintReport(printReport: PrintReport) {
if (printReport.type === "collection" && printReport.ignoredNoteIds.length > 0) {
function handlePrintReport(printReport?: PrintReport) {
if (printReport?.type === "collection" && printReport.ignoredNoteIds.length > 0) {
toast.showPersistent({
id: "print-report",
icon: "bx bx-collection",

View File

@@ -1,7 +1,7 @@
import "./NoteList.css";
import { WebSocketMessage } from "@triliumnext/commons";
import { VNode } from "preact";
import { Component, VNode } from "preact";
import { lazy, Suspense } from "preact/compat";
import { useEffect, useRef, useState } from "preact/hooks";
@@ -120,7 +120,9 @@ export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePa
}
const ComponentToRender = viewType && props && isEnabled && (
props.media === "print" ? ViewComponents[viewType].print : ViewComponents[viewType].normal
props.media === "print"
? ViewComponents[viewType].print ?? ViewComponents[viewType].normal
: ViewComponents[viewType].normal
);
return (

View File

@@ -4,6 +4,10 @@
height: 100%;
user-select: none;
padding: 0 5px 0 10px;
.tabulator-tableholder {
height: unset !important;
}
}
.table-view-container {
@@ -68,4 +72,4 @@
inset-inline-start: 0;
font-size: 1.5em;
transform: translateY(-50%);
}
}

View File

@@ -1,18 +1,20 @@
import { useContext, useEffect, useLayoutEffect, useRef } from "preact/hooks";
import { EventCallBackMethods, Module, Options, Tabulator as VanillaTabulator } from "tabulator-tables";
import "tabulator-tables/dist/css/tabulator.css";
import "../../../../src/stylesheets/table.css";
import { ParentComponent, renderReactWidget } from "../../react/react_utils";
import { JSX } from "preact/jsx-runtime";
import { isValidElement, RefObject } from "preact";
import { useContext, useEffect, useLayoutEffect, useRef } from "preact/hooks";
import { JSX } from "preact/jsx-runtime";
import { EventCallBackMethods, Module, Options, Tabulator as VanillaTabulator } from "tabulator-tables";
import { ParentComponent, renderReactWidget } from "../../react/react_utils";
interface TableProps<T> extends Omit<Options, "data" | "footerElement" | "index"> {
tabulatorRef: RefObject<VanillaTabulator>;
tabulatorRef?: RefObject<VanillaTabulator>;
className?: string;
data?: T[];
modules?: (new (table: VanillaTabulator) => Module)[];
events?: Partial<EventCallBackMethods>;
index: keyof T;
index?: keyof T;
footerElement?: string | HTMLElement | JSX.Element;
onReady?: () => void;
}
@@ -43,7 +45,9 @@ export default function Tabulator<T>({ className, columns, data, modules, tabula
tabulator.on("tableBuilt", () => {
tabulatorRef.current = tabulator;
externalTabulatorRef.current = tabulator;
if (externalTabulatorRef) {
externalTabulatorRef.current = tabulator;
}
onReady?.();
});
@@ -62,12 +66,15 @@ export default function Tabulator<T>({ className, columns, data, modules, tabula
for (const [ eventName, handler ] of Object.entries(events)) {
tabulator.off(eventName as keyof EventCallBackMethods, handler);
}
}
};
}, Object.values(events ?? {}));
// Change in data.
useEffect(() => { tabulatorRef.current?.setData(data) }, [ data ]);
useEffect(() => { columns && tabulatorRef.current?.setColumns(columns)}, [ data]);
useEffect(() => { tabulatorRef.current?.setData(data); }, [ data ]);
useEffect(() => {
if (!columns) return;
tabulatorRef.current?.setColumns(columns);
}, [ columns ]);
return (
<div ref={containerRef} className={className} />

View File

@@ -7,6 +7,7 @@ import { ComponentChild } from "preact";
import { useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import { Trans } from "react-i18next";
import FNote from "../../entities/fnote";
import { ViewScope } from "../../services/link";
import { formatDateTime } from "../../utils/formatters";
import NoteIcon from "../note_icon";
@@ -22,12 +23,12 @@ const supportedNoteTypes = new Set<NoteType>([
export default function InlineTitle() {
const { note, parentComponent, viewScope } = useNoteContext();
const type = useNoteProperty(note, "type");
const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope));
const [ shown, setShown ] = useState(shouldShow(note, type, viewScope));
const containerRef = useRef<HTMLDivElement>(null);
const [ titleHidden, setTitleHidden ] = useState(false);
useLayoutEffect(() => {
setShown(shouldShow(note?.noteId, type, viewScope));
setShown(shouldShow(note, type, viewScope));
}, [ note, type, viewScope ]);
useLayoutEffect(() => {
@@ -69,9 +70,10 @@ export default function InlineTitle() {
);
}
function shouldShow(noteId: string | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
function shouldShow(note: FNote | null | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
if (viewScope?.viewMode !== "default") return false;
if (noteId?.startsWith("_options")) return true;
if (note?.noteId?.startsWith("_options")) return true;
if (note?.isTriliumSqlite()) return false;
return type && supportedNoteTypes.has(type);
}

View File

@@ -39,7 +39,7 @@ export default function NoteTypeSwitcher() {
const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]);
const { builtinTemplates, collectionTemplates } = useBuiltinTemplates();
return (currentNoteType && supportedNoteTypes.has(currentNoteType) &&
return (currentNoteType && supportedNoteTypes.has(currentNoteType) && !note?.isTriliumSqlite() &&
<div
className="note-type-switcher"
onWheel={onWheelHorizontalScroll}

View File

@@ -1,6 +1,6 @@
import "./StatusBar.css";
import { Locale, NoteType } from "@triliumnext/commons";
import { Locale, NOTE_TYPE_ICONS, NoteType } from "@triliumnext/commons";
import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx";
import { type ComponentChildren, RefObject } from "preact";
@@ -9,7 +9,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "p
import { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
import FNote, { NOTE_TYPE_ICONS } from "../../entities/fnote";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import { t } from "../../services/i18n";
import { ViewScope } from "../../services/link";

View File

@@ -1232,7 +1232,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
refreshCtx.noteIdsToUpdate.add(noteId);
}
if (refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0) {
const hasNotesToUpdateOrReload = refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0;
const hasNoteReorderingChange = loadResults.getNoteReorderings().length > 0;
if (hasNotesToUpdateOrReload || hasNoteReorderingChange) {
await this.#executeTreeUpdates(refreshCtx, loadResults);
}
@@ -1393,6 +1395,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
for (const parentNoteId of loadResults.getNoteReorderings()) {
for (const node of this.getNodesByNoteId(parentNoteId)) {
console.log("Reordering ", node);
if (node.isLoaded()) {
this.sortChildren(node);
}

View File

@@ -12,7 +12,7 @@ import { TypeWidgetProps } from "./type_widgets/type_widget";
* A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one,
* for protected session or attachment information.
*/
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat";
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat" | "sqlConsole";
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element | undefined);
type NoteTypeView = () => (Promise<{ default: TypeWidget } | TypeWidget> | TypeWidget);
@@ -140,5 +140,10 @@ export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
view: () => import("./type_widgets/AiChat"),
className: "ai-chat-widget-container",
isFullHeight: true
},
sqlConsole: {
view: () => import("./type_widgets/SqlConsole"),
className: "sql-console-widget-container",
isFullHeight: true
}
};

View File

@@ -0,0 +1,18 @@
.no-items {
display: flex;
align-items: center;
justify-content: center;
flex-grow: 1;
flex-direction: column;
padding: 0.75em;
color: var(--muted-text-color);
height: 100%;
.tn-icon {
font-size: 3em;
}
button {
margin-top: 1em;
}
}

View File

@@ -0,0 +1,21 @@
import "./NoItems.css";
import { ComponentChildren } from "preact";
import Icon from "./Icon";
interface NoItemsProps {
icon: string;
text: string;
children?: ComponentChildren;
}
export default function NoItems({ icon, text, children }: NoItemsProps) {
return (
<div className="no-items">
<Icon icon={icon} />
{text}
{children}
</div>
);
}

View File

@@ -184,7 +184,8 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: N
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: NoteActionsCustomInnerProps) {
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
&& note.isContentAvailable() && isDefaultViewMode;
return isEnabled && <ActionButton

View File

@@ -1,11 +1,14 @@
import { useEffect, useRef, useState } from "preact/hooks";
import { useNoteContext } from "./react/hooks";
export default function ScrollPadding() {
const { note, parentComponent, ntxId, viewScope } = useNoteContext();
const ref = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState<number>(10);
const isEnabled = ["text", "code"].includes(note?.type ?? "") && viewScope?.viewMode === "default";
const isEnabled = ["text", "code"].includes(note?.type ?? "")
&& viewScope?.viewMode === "default"
&& !note?.isTriliumSqlite();
const refreshHeight = () => {
if (!ref.current) return;
@@ -37,6 +40,6 @@ export default function ScrollPadding() {
style={{ height }}
onClick={() => parentComponent.triggerCommand("scrollToEnd", { ntxId })}
/>
: <div></div>
)
: <div />
);
}

View File

@@ -40,22 +40,4 @@ body.experimental-feature-new-layout #right-pane {
.gutter-vertical + .card .card-header {
padding-top: 0;
}
.no-items {
display: flex;
align-items: center;
justify-content: center;
flex-grow: 1;
flex-direction: column;
padding: 0.75em;
color: var(--muted-text-color);
.tn-icon {
font-size: 3em;
}
button {
margin-top: 1em;
}
}
}

View File

@@ -3,7 +3,7 @@ import "./RightPanelContainer.css";
import Split from "@triliumnext/split.js";
import { VNode } from "preact";
import { useState, useEffect, useRef, useCallback } from "preact/hooks";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import { WidgetsByParent } from "../../services/bundle";
@@ -12,7 +12,7 @@ import options from "../../services/options";
import { DEFAULT_GUTTER_SIZE } from "../../services/resizer";
import Button from "../react/Button";
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionJson } from "../react/hooks";
import Icon from "../react/Icon";
import NoItems from "../react/NoItems";
import LegacyRightPanelWidget from "../right_panel_widget";
import HighlightsList from "./HighlightsList";
import PdfAttachments from "./pdf/PdfAttachments";
@@ -47,14 +47,15 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
items.length > 0 ? (
items
) : (
<div className="no-items">
<Icon icon="bx bx-sidebar" />
{t("right_pane.empty_message")}
<NoItems
icon="bx bx-sidebar"
text={t("right_pane.empty_message")}
>
<Button
text={t("right_pane.empty_button")}
triggerCommand="toggleRightPane"
/>
</div>
</NoItems>
)
)}
</div>

View File

@@ -1,7 +0,0 @@
.sql-result-widget {
padding: 15px;
}
.sql-console-result-container td {
white-space: preserve;
}

View File

@@ -1,63 +0,0 @@
import { SqlExecuteResults } from "@triliumnext/commons";
import { useNoteContext, useTriliumEvent } from "./react/hooks";
import "./sql_result.css";
import { useState } from "preact/hooks";
import Alert from "./react/Alert";
import { t } from "../services/i18n";
export default function SqlResults() {
const { note, ntxId } = useNoteContext();
const [ results, setResults ] = useState<SqlExecuteResults>();
useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, results }) => {
if (eventNtxId !== ntxId) return;
setResults(results);
})
const isEnabled = note?.mime === "text/x-sqlite;schema=trilium";
return (
<div className={`sql-result-widget ${!isEnabled ? "hidden-ext" : ""}`}>
{isEnabled && (
results?.length === 1 && Array.isArray(results[0]) && results[0].length === 0 ? (
<Alert type="info">
{t("sql_result.no_rows")}
</Alert>
) : (
<div className="sql-console-result-container selectable-text">
{results?.map(rows => {
// inserts, updates
if (typeof rows === "object" && !Array.isArray(rows)) {
return <pre>{JSON.stringify(rows, null, "\t")}</pre>
}
// selects
return <SqlResultTable rows={rows} />
})}
</div>
)
)}
</div>
)
}
function SqlResultTable({ rows }: { rows: object[] }) {
if (!rows.length) return;
return (
<table className="table table-striped">
<thead>
<tr>
{Object.keys(rows[0]).map(key => <th>{key}</th>)}
</tr>
</thead>
<tbody>
{rows.map(row => (
<tr>
{Object.values(row).map(cell => <td>{cell}</td>)}
</tr>
))}
</tbody>
</table>
)
}

View File

@@ -1,43 +0,0 @@
.sql-table-schemas-widget {
padding: 12px;
padding-inline-end: 10%;
contain: none !important;
}
.sql-table-schemas > .dropdown {
display: inline-block !important;
}
.sql-table-schemas button.btn {
padding: 0.25rem 0.4rem;
font-size: 0.875rem;
line-height: 0.5;
border: 1px solid var(--button-border-color);
border-radius: var(--button-border-radius);
background: var(--button-background-color);
color: var(--button-text-color);
cursor: pointer;
}
.sql-console-result-container {
width: 100%;
font-size: smaller;
margin-top: 10px;
flex-grow: 1;
overflow: auto;
min-height: 0;
}
.table-schema td {
padding: 5px;
}
.dropdown .table-schema {
font-family: var(--monospace-font-family);
font-size: .85em;
}
/* Data type */
.dropdown .table-schema td:nth-child(2) {
color: var(--muted-text-color);
}

View File

@@ -1,46 +0,0 @@
import { useEffect, useState } from "preact/hooks";
import { t } from "../services/i18n";
import { useNoteContext } from "./react/hooks";
import "./sql_table_schemas.css";
import { SchemaResponse } from "@triliumnext/commons";
import server from "../services/server";
import Dropdown from "./react/Dropdown";
export default function SqlTableSchemas() {
const { note } = useNoteContext();
const [ schemas, setSchemas ] = useState<SchemaResponse[]>();
useEffect(() => {
server.get<SchemaResponse[]>("sql/schema").then(setSchemas);
}, []);
const isEnabled = note?.mime === "text/x-sqlite;schema=trilium" && schemas;
return (
<div className={`sql-table-schemas-widget ${!isEnabled ? "hidden-ext" : ""}`}>
{isEnabled && (
<>
{t("sql_table_schemas.tables")}{": "}
<span class="sql-table-schemas">
{schemas.map(({ name, columns }) => (
<>
<Dropdown text={name} noSelectButtonStyle hideToggleArrow
>
<table className="table-schema">
{columns.map(column => (
<tr>
<td>{column.name}</td>
<td>{column.type}</td>
</tr>
))}
</table>
</Dropdown>
{" "}
</>
))}
</span>
</>
)}
</div>
)
}

View File

@@ -0,0 +1,81 @@
.sql-console-widget-container {
.note-detail-split.split-vertical {
flex-direction: column-reverse;
}
.note-detail-split-preview {
overflow: auto;
}
.gutter {
background-color: var(--accented-background-color) !important;
}
.sql-result-widget {
height: 100%;
> .sql-console-result-container {
width: 100%;
height: 100%;
font-size: smaller;
flex-grow: 1;
overflow: auto;
min-height: 0;
> .tabulator {
--cell-vert-padding-size: 4px;
> .tabulator-tableholder {
padding: 0;
}
> .tabulator-footer,
> .tabulator-footer .tabulator-footer-contents {
padding: 2px 4px;
}
}
}
}
.sql-table-schemas-widget {
padding: 12px;
padding-inline-end: 10%;
contain: none !important;
.sql-table-schemas {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
}
> .dropdown {
display: inline-block !important;
}
button.btn {
padding: 0.25rem 0.4rem;
font-size: 0.875rem;
line-height: 0.5;
border: 1px solid var(--button-border-color);
border-radius: var(--button-border-radius);
background: var(--button-background-color);
color: var(--button-text-color);
cursor: pointer;
}
.table-schema td {
padding: 5px;
}
.dropdown .table-schema {
font-family: var(--monospace-font-family);
font-size: .85em;
}
/* Data type */
.dropdown .table-schema td:nth-child(2) {
color: var(--muted-text-color);
}
}
}

View File

@@ -0,0 +1,176 @@
import "./SqlConsole.css";
import { SchemaResponse, SqlExecuteResponse } from "@triliumnext/commons";
import { useEffect, useState } from "preact/hooks";
import { ClipboardModule, EditModule, ExportModule, FilterModule, FormatModule, FrozenColumnsModule, KeybindingsModule, PageModule, ResizeColumnsModule, SelectRangeModule, SelectRowModule, SortModule } from "tabulator-tables";
import { t } from "../../services/i18n";
import server from "../../services/server";
import Tabulator from "../collections/table/tabulator";
import Button from "../react/Button";
import Dropdown from "../react/Dropdown";
import { useTriliumEvent } from "../react/hooks";
import NoItems from "../react/NoItems";
import SplitEditor from "./helpers/SplitEditor";
import { TypeWidgetProps } from "./type_widget";
export default function SqlConsole(props: TypeWidgetProps) {
return (
<SplitEditor
noteType="code"
{...props}
editorBefore={<SqlTableSchemas {...props} />}
previewContent={<SqlResults key={props.note.noteId} {...props} />}
forceOrientation="vertical"
splitOptions={{
sizes: [ 70, 30 ]
}}
/>
);
}
function SqlResults({ ntxId }: TypeWidgetProps) {
const [ response, setResponse ] = useState<SqlExecuteResponse>();
useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, response }) => {
if (eventNtxId !== ntxId) return;
setResponse(response);
});
// Not yet executed.
if (response === undefined) {
return (
<NoItems
icon="bx bx-data"
text={t("sql_result.not_executed")}
>
<Button
text={t("sql_result.execute_now")}
triggerCommand="runActiveNote"
/>
</NoItems>
);
}
// Executed but failed.
if (response && !response.success) {
return (
<NoItems
icon="bx bx-error"
text={t("sql_result.failed")}
>
<pre className="sql-error-message selectable-text">{response.error}</pre>
</NoItems>
);
}
// Zero results.
if (response?.results.length === 1 && Array.isArray(response.results[0]) && response.results[0].length === 0) {
return (
<NoItems
icon="bx bx-rectangle"
text={t("sql_result.no_rows")}
/>
);
}
return (
<div className="sql-result-widget">
<div className="sql-console-result-container selectable-text">
{response?.results.map((rows, index) => {
// inserts, updates
if (typeof rows === "object" && !Array.isArray(rows)) {
return (
<NoItems
key={index}
icon="bx bx-play"
text={t("sql_result.statement_result")}
>
<pre key={index}>{JSON.stringify(rows, null, "\t")}</pre>
</NoItems>
);
}
// selects
return <SqlResultTable key={index} rows={rows} />;
})}
</div>
</div>
);
}
function SqlResultTable({ rows }: { rows: object[] }) {
if (!rows.length) return;
return (
<Tabulator
layout="fitDataFill"
modules={[ ResizeColumnsModule, SortModule, SelectRangeModule, ClipboardModule, KeybindingsModule, EditModule, ExportModule, SelectRowModule, FormatModule, FrozenColumnsModule, FilterModule, PageModule ]}
selectableRange
clipboard="copy"
clipboardCopyRowRange="range"
clipboardCopyConfig={{
rowHeaders: false,
columnHeaders: false
}}
pagination
paginationSize={15}
paginationSizeSelector
paginationCounter="rows"
height="100%"
columns={[
{
title: "#",
formatter: "rownum",
width: 60,
hozAlign: "right",
frozen: true
},
...Object.keys(rows[0]).map(key => ({
title: key,
field: key,
width: 250,
minWidth: 100,
widthGrow: 1,
resizable: true,
headerFilter: true as const
}))
]}
data={rows}
/>
);
}
export function SqlTableSchemas({ note }: TypeWidgetProps) {
const [ schemas, setSchemas ] = useState<SchemaResponse[]>();
useEffect(() => {
server.get<SchemaResponse[]>("sql/schema").then(setSchemas);
}, []);
const isEnabled = note.isTriliumSqlite() && schemas;
return (
<div className={`sql-table-schemas-widget ${!isEnabled ? "hidden-ext" : ""}`}>
{isEnabled && (
<>
{t("sql_table_schemas.tables")}{": "}
<span class="sql-table-schemas">
{schemas.map(({ name, columns }) => (
<Dropdown key={name} text={name} noSelectButtonStyle hideToggleArrow>
<table className="table-schema">
{columns.map(column => (
<tr key={column.name}>
<td>{column.name}</td>
<td>{column.type}</td>
</tr>
))}
</table>
</Dropdown>
))}
</span>
</>
)}
</div>
);
}

View File

@@ -1,13 +1,15 @@
import "./SplitEditor.css";
import Split from "@triliumnext/split.js";
import { ComponentChildren } from "preact";
import { useEffect, useRef } from "preact/hooks";
import { DEFAULT_GUTTER_SIZE } from "../../../services/resizer";
import utils, { isMobile } from "../../../services/utils";
import ActionButton, { ActionButtonProps } from "../../react/ActionButton";
import Admonition from "../../react/Admonition";
import { useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
import "./SplitEditor.css";
import Split from "@triliumnext/split.js";
import { DEFAULT_GUTTER_SIZE } from "../../../services/resizer";
import { EditableCode, EditableCodeProps } from "../code/Code";
import { ComponentChildren } from "preact";
import ActionButton, { ActionButtonProps } from "../../react/ActionButton";
export interface SplitEditorProps extends EditableCodeProps {
className?: string;
@@ -15,6 +17,8 @@ export interface SplitEditorProps extends EditableCodeProps {
splitOptions?: Split.Options;
previewContent: ComponentChildren;
previewButtons?: ComponentChildren;
editorBefore?: ComponentChildren;
forceOrientation?: "horizontal" | "vertical";
}
/**
@@ -26,13 +30,14 @@ export interface SplitEditorProps extends EditableCodeProps {
* - Can display errors to the user via {@link setError}.
* - Horizontal or vertical orientation for the editor/preview split, adjustable via the switch split orientation button floating button.
*/
export default function SplitEditor({ note, error, splitOptions, previewContent, previewButtons, className, ...editorProps }: SplitEditorProps) {
const splitEditorOrientation = useSplitOrientation();
export default function SplitEditor({ note, error, splitOptions, previewContent, previewButtons, className, editorBefore, forceOrientation, ...editorProps }: SplitEditorProps) {
const splitEditorOrientation = useSplitOrientation(forceOrientation);
const [ readOnly ] = useNoteLabelBoolean(note, "readOnly");
const containerRef = useRef<HTMLDivElement>(null);
const editor = (!readOnly &&
<div className="note-detail-split-editor-col">
{editorBefore}
<div className="note-detail-split-editor">
<EditableCode
note={note}
@@ -74,12 +79,12 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
}, [ readOnly, splitEditorOrientation ]);
return (
<div ref={containerRef} className={`note-detail-split note-detail-printable ${"split-" + splitEditorOrientation} ${readOnly ? "split-read-only" : ""} ${className ?? ""}`}>
<div ref={containerRef} className={`note-detail-split note-detail-printable ${`split-${splitEditorOrientation}`} ${readOnly ? "split-read-only" : ""} ${className ?? ""}`}>
{splitEditorOrientation === "horizontal"
? <>{editor}{preview}</>
: <>{preview}{editor}</>}
? <>{editor}{preview}</>
: <>{preview}{editor}</>}
</div>
)
);
}
export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
@@ -88,11 +93,12 @@ export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
className="tn-tool-button"
noIconActionClass
titlePosition="top"
/>
/>;
}
function useSplitOrientation() {
function useSplitOrientation(forceOrientation?: "horizontal" | "vertical") {
const [ splitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
if (forceOrientation) return forceOrientation;
if (isMobile()) return "vertical";
if (!splitEditorOrientation) return "horizontal";
return splitEditorOrientation as "horizontal" | "vertical";

View File

@@ -93,7 +93,15 @@ export default defineConfig(() => ({
print: join(__dirname, "src", "print.tsx")
},
output: {
entryFileNames: "src/[name].js",
entryFileNames: (chunk) => {
// We enforce a hash in the main index file to avoid caching issues, this only works because we have the HTML entry point.
if (chunk.name === "index") {
return "src/[name]-[hash].js";
}
// For EJS-rendered pages (e.g. login) we need to have a stable name.
return "src/[name].js";
},
chunkFileNames: "src/[name]-[hash].js",
assetFileNames: "src/[name]-[hash].[ext]",
manualChunks: {

View File

@@ -23,7 +23,7 @@
},
"dependencies": {
"@electron/remote": "2.1.3",
"better-sqlite3": "12.6.0",
"better-sqlite3": "12.6.2",
"electron-debug": "4.1.0",
"electron-dl": "4.0.0",
"electron-squirrel-startup": "1.0.1",
@@ -35,7 +35,7 @@
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.1",
"electron": "39.2.7",
"electron": "40.0.0",
"@electron-forge/cli": "7.11.1",
"@electron-forge/maker-deb": "7.11.1",
"@electron-forge/maker-dmg": "7.11.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 B

View File

@@ -6,7 +6,6 @@ import sqlInit from "@triliumnext/server/src/services/sql_init.js";
import windowService from "@triliumnext/server/src/services/window.js";
import tray from "@triliumnext/server/src/services/tray.js";
import options from "@triliumnext/server/src/services/options.js";
import electronDebug from "electron-debug";
import electronDl from "electron-dl";
import { PRODUCT_NAME } from "./app-info";
@@ -70,12 +69,10 @@ async function main() {
globalShortcut.unregisterAll();
});
app.on("second-instance", async (event, commandLine) => {
app.on("second-instance", (event, commandLine) => {
const lastFocusedWindow = windowService.getLastFocusedWindow();
if (commandLine.includes("--new-window")) {
const randomString = (await import("@triliumnext/server/src/services/utils.js")).randomString;
const extraWindowId = randomString(4);
windowService.createExtraWindow(extraWindowId, "");
windowService.createExtraWindow("");
} else if (lastFocusedWindow) {
if (lastFocusedWindow.isMinimized()) {
lastFocusedWindow.restore();
@@ -127,8 +124,7 @@ async function onReady() {
}
});
}
await normalizeOpenNoteContexts();
tray.createTray();
} else {
await windowService.createSetupWindow();
@@ -137,30 +133,6 @@ async function onReady() {
await windowService.registerGlobalShortcuts();
}
/**
* Some windows may have closed abnormally, leaving closedAt as 0 in openNoteContexts.
* This function normalizes those timestamps to the current time for correct sorting/filtering.
*/
async function normalizeOpenNoteContexts() {
const savedWindows = options.getOptionJson("openNoteContexts") || [];
const now = Date.now();
let changed = false;
for (const win of savedWindows) {
if (win.windowId !== "main" && win.closedAt === 0) {
win.closedAt = now;
changed = true;
}
}
if (changed) {
const { default: cls } = (await import("@triliumnext/server/src/services/cls.js"));
cls.wrap(() => {
options.setOption("openNoteContexts", JSON.stringify(savedWindows));
})();
}
}
function getElectronLocale() {
const uiLocale = options.getOptionOrNull("locale");
const formattingLocale = options.getOptionOrNull("formattingLocale");

View File

@@ -4,7 +4,7 @@
"description": "Standalone tool to dump contents of Trilium document.db file into a directory tree of notes",
"private": true,
"dependencies": {
"better-sqlite3": "12.6.0",
"better-sqlite3": "12.6.2",
"mime-types": "3.0.2",
"sanitize-filename": "1.6.3",
"tsx": "4.21.0",

View File

@@ -5,14 +5,14 @@
"description": "Desktop version of Trilium which imports the demo database (presented to new users at start-up) or the user guide and other documentation and saves the modifications for committing.",
"dependencies": {
"archiver": "7.0.1",
"better-sqlite3": "12.6.0"
"better-sqlite3": "12.6.2"
},
"devDependencies": {
"@triliumnext/client": "workspace:*",
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.1",
"electron": "39.2.7",
"electron": "40.0.0",
"fs-extra": "11.3.3"
},
"scripts": {

View File

@@ -185,6 +185,9 @@ async function exportData(noteId: string, format: ExportFormat, outputPath: stri
return components.join("/");
});
// Remove data-list-item-id created by CKEditor for lists
content = content.replace(/ data-list-item-id="[^"]*"/g, "");
return content;
function findAttachment(targetAttachmentId: string) {

View File

@@ -43,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/i);
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"]) {

View File

@@ -59,7 +59,7 @@ export default class App {
// Wait for the page to load.
if (url === "/") {
await expect(this.noteTree).toContainText("Trilium Integration Test");
await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test");
if (!preserveTabs) {
await this.closeAllTabs();
}

View File

@@ -1,4 +1,4 @@
FROM node:24.12.0-bullseye-slim AS builder
FROM node:24.13.0-bullseye-slim AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.12.0-bullseye-slim
FROM node:24.13.0-bullseye-slim
# Install only runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \

View File

@@ -1,4 +1,4 @@
FROM node:24.12.0-alpine AS builder
FROM node:24.13.0-alpine AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.12.0-alpine
FROM node:24.13.0-alpine
# Install runtime dependencies
RUN apk add --no-cache su-exec shadow

View File

@@ -1,4 +1,4 @@
FROM node:24.12.0-alpine AS builder
FROM node:24.13.0-alpine AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.12.0-alpine
FROM node:24.13.0-alpine
# Create a non-root user with configurable UID/GID
ARG USER=trilium
ARG UID=1001

View File

@@ -1,4 +1,4 @@
FROM node:24.12.0-bullseye-slim AS builder
FROM node:24.13.0-bullseye-slim AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.12.0-bullseye-slim
FROM node:24.13.0-bullseye-slim
# Create a non-root user with configurable UID/GID
ARG USER=trilium
ARG UID=1001

View File

@@ -1,5 +1,5 @@
{
"dependencies": {
"better-sqlite3": "12.6.0"
"better-sqlite3": "12.6.2"
}
}

View File

@@ -29,7 +29,7 @@
"proxy-nginx-subdir": "docker run --name trilium-nginx-subdir --rm --network=host -v ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:latest"
},
"dependencies": {
"better-sqlite3": "12.6.0",
"better-sqlite3": "12.6.2",
"html-to-text": "9.0.5",
"node-html-parser": "7.0.2",
"sucrase": "3.35.1"
@@ -82,8 +82,8 @@
"csrf-csrf": "3.2.2",
"debounce": "3.0.0",
"debug": "4.4.3",
"ejs": "3.1.10",
"electron": "39.2.7",
"ejs": "4.0.1",
"electron": "40.0.0",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
@@ -126,7 +126,7 @@
"swagger-jsdoc": "6.2.8",
"time2fa": "1.4.2",
"tmp": "0.2.5",
"turndown": "7.2.2",
"turnish": "1.8.0",
"unescape": "1.0.1",
"vite": "7.3.1",
"ws": "8.19.0",

View File

@@ -1,25 +1,27 @@
import express from "express";
import path from "path";
import favicon from "serve-favicon";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import compression from "compression";
import config from "./services/config.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";
import assets from "./routes/assets.js";
import routes from "./routes/routes.js";
import custom from "./routes/custom.js";
import error_handlers from "./routes/error_handlers.js";
import { startScheduledCleanup } from "./services/erase.js";
import sql_init from "./services/sql_init.js";
import { auth } from "express-openid-connect";
import openID from "./services/open_id.js";
import { t } from "i18next";
import eventService from "./services/events.js";
import log from "./services/log.js";
import "./services/handlers.js";
import "./becca/becca_loader.js";
import compression from "compression";
import cookieParser from "cookie-parser";
import ejs from "ejs";
import express from "express";
import { auth } from "express-openid-connect";
import helmet from "helmet";
import { t } from "i18next";
import path from "path";
import favicon from "serve-favicon";
import assets from "./routes/assets.js";
import custom from "./routes/custom.js";
import error_handlers from "./routes/error_handlers.js";
import routes from "./routes/routes.js";
import config from "./services/config.js";
import { startScheduledCleanup } from "./services/erase.js";
import log from "./services/log.js";
import openID from "./services/open_id.js";
import { RESOURCE_DIR } from "./services/resource_dir.js";
import sql_init from "./services/sql_init.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";
export default async function buildApp() {
const app = express();
@@ -33,7 +35,7 @@ export default async function buildApp() {
// view engine setup
app.set("views", path.join(assetsDir, "views"));
app.engine("ejs", (await import("ejs")).renderFile);
app.engine("ejs", (filePath, options, callback) => ejs.renderFile(filePath, options, callback));
app.set("view engine", "ejs");
app.use((req, res, next) => {

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 B

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -1,36 +1,43 @@
<p>The SQL Console is Trilium's built-in database editor.</p>
<p>It can be accessed by going to the <a href="#root/_help_Vc8PjrjAGuOp">global menu</a>
<p>It can be accessed by going to the&nbsp;<a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a>&nbsp;
Advanced → Open SQL Console.</p>
<p>
<img src="SQL Console_image.png">
</p>
<h3>Interaction</h3>
<ul>
<li>
<p>Hovering the mouse over one of the tables listed at the top of the document
will show the columns and their data type.</p>
</li>
<li>
<p>Only one SQL statement can be run at once.</p>
</li>
<li>
<p>To run the statement, press the
<img src="3_SQL Console_image.png">icon.</p>
</li>
<li>
<p>For queries that return a result, the data will displayed in a table.</p>
<p>
<img src="1_SQL Console_image.png">
</p>
</li>
<li>Hovering the mouse over one of the tables listed at the top of the document
will show the columns and their data type.</li>
<li>Only one SQL statement can be run at once.</li>
<li>To run the statement, press the <em>Execute</em> icon.</li>
<li>For queries that return a result, the data will displayed in a table.</li>
<li>For statements (e.g. <code spellcheck="false">INSERT</code>, <code spellcheck="false">UPDATE</code>),
the number of affected rows is displayed.</li>
</ul>
<figure class="image">
<img style="aspect-ratio:1124/571;" src="2_SQL Console_image.png"
width="1124" height="571">
</figure>
<h3>Interacting with the table</h3>
<p>After executing a query, a table with the results will be displayed:</p>
<ul>
<li>Clicking on a column allows sorting ascending or descending.</li>
<li>Underneath each column there is an input field which allows filtering
by text.</li>
<li>Press <kbd>Ctrl</kbd>+<kbd>C</kbd> to copy the current cell to clipboard.</li>
<li>Multiple cells can be selected by dragging or by holding <kbd>Shift</kbd> +
arrow keys</li>
<li>Results are paginated for performance reasons. The controls at the bottom
of the table can be used to navigate through pages.</li>
</ul>
<h3>Saved SQL console</h3>
<p>SQL queries or commands can be saved into a dedicated note.</p>
<p>To do so, simply write the query and press the
<img src="2_SQL Console_image.png">button. Once saved, the note will appear in&nbsp;<a href="#root/_help_l0tKav7yLHGF">Day Notes</a>.</p>
<ul>
<li>The SQL expression will not be displayed by default, but it can still
be viewed by going to the note context menu and selecting <em>Note source</em>.</li>
<li>The expression cannot be modified. If needed, recreate it by copying the
statement back into the SQL console and then saving it again.</li>
</ul>
<img src="1_SQL Console_image.png">button. Once saved, the note will appear in&nbsp;<a class="reference-link"
href="#root/_help_l0tKav7yLHGF">Day Notes</a>.</p>
<p>The note can be locked for editing by pressing the <em>Lock</em> button
in the note actions section near the title bar (on the&nbsp;<a class="reference-link"
href="#root/_help_IjZS7iK5EXtb">New Layout</a>, or in the&nbsp;<a class="reference-link"
href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>&nbsp;area if using the old
layout). When editing is locked, the SQL statement is hidden from view.</p>

View File

@@ -38,17 +38,17 @@ class="image">
</th>
<td>
<ul>
<li data-list-item-id="e26b4ce9ba4e9dfe224d04e0f341925ed">Table of contents.</li>
<li data-list-item-id="e9707fdfa2c92d66690cf932f7e647253">Syntax highlight of code blocks, provided a language is selected (does
<li>Table of contents.</li>
<li>Syntax highlight of code blocks, provided a language is selected (does
not work if “Auto-detected” is enabled).</li>
<li data-list-item-id="e84420a10c6d64bd107edb6e867c91d4b">Rendering for math equations.</li>
<li data-list-item-id="e10834dcd0619d77ae2e94d3695bedf58"><a href="#root/_help_nBAXQFj20hS1">Including notes</a> (only if the included
<li>Rendering for math equations.</li>
<li><a href="#root/_help_nBAXQFj20hS1">Including notes</a> (only if the included
notes are also shared).</li>
</ul>
</td>
<td>
<ul>
<li data-list-item-id="e41cc4139377f9f88d653d1eb8ca47bb4">Inline Mermaid diagrams are not rendered.</li>
<li>Inline Mermaid diagrams are not rendered.</li>
</ul>
</td>
</tr>
@@ -57,12 +57,12 @@ class="image">
</th>
<td>
<ul>
<li data-list-item-id="e291ae6d5130677b4c99f7c3bdbe974b4">Basic support (displaying the contents of the note in a monospace font).</li>
<li>Basic support (displaying the contents of the note in a monospace font).</li>
</ul>
</td>
<td>
<ul>
<li data-list-item-id="e0270680bbdd7a129306e61e11691e36d">No syntax highlight.</li>
<li>No syntax highlight.</li>
</ul>
</td>
</tr>
@@ -95,12 +95,12 @@ class="image">
</th>
<td>
<ul>
<li data-list-item-id="ea031e1d4149eb443ace756234490c5a4">The child notes are displayed in a fixed format.&nbsp;</li>
<li>The child notes are displayed in a fixed format.&nbsp;</li>
</ul>
</td>
<td>
<ul>
<li data-list-item-id="ea4a9d424aec2afbaecc07bbf64b7bebd">More advanced view types such as the calendar view are not supported.</li>
<li>More advanced view types such as the calendar view are not supported.</li>
</ul>
</td>
</tr>
@@ -109,12 +109,12 @@ class="image">
</th>
<td>
<ul>
<li data-list-item-id="e582d283f2b1b30cbe5ae35d8e01b2bf2">The diagram is displayed as a vector image.</li>
<li>The diagram is displayed as a vector image.</li>
</ul>
</td>
<td>
<ul>
<li data-list-item-id="e33268686446e3c217077201bb5964364">No further interaction supported.</li>
<li>No further interaction supported.</li>
</ul>
</td>
</tr>
@@ -123,12 +123,12 @@ class="image">
</th>
<td>
<ul>
<li data-list-item-id="e443dd0e97c30cb12c77e8906a71569ea">The diagram is displayed as a vector image.</li>
<li>The diagram is displayed as a vector image.</li>
</ul>
</td>
<td>
<ul>
<li data-list-item-id="efe151ef3f3826c825416417525fb5fb2">No further interaction supported.</li>
<li>No further interaction supported.</li>
</ul>
</td>
</tr>
@@ -144,7 +144,7 @@ class="image">
<td>The diagram is displayed as a vector image.</td>
<td>
<ul>
<li data-list-item-id="ed3b4fb473042f6e32b4502d4fa11a767">No further interaction supported.</li>
<li>No further interaction supported.</li>
</ul>
</td>
</tr>
@@ -160,7 +160,7 @@ class="image">
<td>Basic interaction (downloading the file).</td>
<td>
<ul>
<li data-list-item-id="ed87e836a39d127ebcbb33e9e59045afb">No further interaction supported.</li>
<li>No further interaction supported.</li>
</ul>
</td>
</tr>
@@ -392,8 +392,8 @@ for (const attr of parentNote.attributes) {
<p>Indicates to web crawlers that the page should not be indexed of this
note by:</p>
<ul>
<li data-list-item-id="e6baa9f60bf59d085fd31aa2cce07a0e7">Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li>
<li data-list-item-id="ec0d067db136ef9794e4f1033405880b7">Setting the <code>noindex, follow</code> meta tag.</li>
<li>Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li>
<li>Setting the <code>noindex, follow</code> meta tag.</li>
</ul>
</td>
</tr>

View File

@@ -1,16 +1,79 @@
<p>Trilium can import ENEX files which are used by Evernote for backup/export.
One ENEX file represents content (notes and resources) of one notebook.</p>
<p>Trilium can import ENEX files, which are used by Evernote for backup/export.
One ENEX file represents the content (notes and resources) of one notebook.</p>
<h2>Export ENEX from Evernote</h2>
<p>To export ENEX file, you need to have a <em>legacy</em> desktop version
of Evernote (i.e. not web/mobile). Right click on notebook and select export
and follow the wizard.</p>
<p>To export ENEX files from Evernote, you can use:</p>
<ul>
<li>Evernote desktop application. See Evernote&nbsp;<a href="https://help.evernote.com/hc/en-us/articles/209005557-Export-Notes-and-Notebooks-as-ENEX-or-HTML">documentation</a>.
Note that the limitation of this method is that you can only export 100
notes at a time or one notebook at a time.</li>
<li>A third-party&nbsp;<a href="https://github.com/vzhd1701/evernote-backup">evernote-backup</a> CLI
tool. This tool can export all of your notebooks in bulk.</li>
</ul>
<h2>Import ENEX in Trilium</h2>
<p>Once you have ENEX file, you can import it to Trilium. Right click on
some note (to which you want to import the file), click on "Import" and
select the ENEX file.</p>
<p>After importing the ENEX file, go over the imported notes and resources
to be sure the import went well, and you didn't lose any data.</p>
<p>Once you have your ENEX files, do the following to import them in Trilium:</p>
<ol>
<li>In the Trilium note tree, right-click the note under which you want to
import one or more of your ENEX files. The notes in the files will be imported
as child notes of the selected note.</li>
<li>Click&nbsp;Import into note.</li>
<li>Choose your ENEX file or files and click&nbsp;Import.</li>
<li>During the import, you will see "Import in progress" message. If the import
is successful, the message will change to “Import finished successfully”
and then disappear.</li>
<li>We recommend you to check the imported notes and their attachments to
verify that you havent lost any data.</li>
</ol>
<p>A non-exhaustive list of what the importer preserves:</p>
<ul>
<li>Attachments</li>
<li>The hierarchy of headings (these are shifted to start with H2 because
H1 is reserved for note title, see&nbsp;<a href="#root/_help_Gr6xFaF6ioJ5">Headings</a>)</li>
<li>Tables</li>
<li>Bulleted lists</li>
<li>Numbered lists</li>
<li>Bold</li>
<li>Italics</li>
<li>Strikethrough</li>
<li>Highlights</li>
<li>Font colors</li>
<li>Soft line breaks</li>
<li>External links</li>
</ul>
<p>However, we do not guarantee that all of your formatting will be imported
100% correctly.</p>
<h2>Limitations</h2>
<p>All resources (except for images) are created as note's attachments.</p>
<p>HTML inside ENEX files is not exactly valid so some formatting maybe broken
or lost. You can report major problems into <a href="https://github.com/TriliumNext/Trilium/issues">Trilium issue tracker</a>.</p>
<ul>
<li>The size limit of one import is 250Mb. If the total size of your files
is larger, you can increase the&nbsp;<a href="#root/_help_WOcw2SLH6tbX">upload limit</a>,
or divide your files, and run the import as many times as necessary.</li>
<li>All resources (except for images) are created as notes attachments.</li>
<li>If you have HTML inside ENEX files, the HTML formatting may be broken
or lost after import in Trilium. See&nbsp;<a class="reference-link" href="#root/_help_wy8So3yZZlH9">Reporting issues</a>.</li>
</ul>
<h3>Internal links</h3>
<p>The importer cannot transform Evernote internal links into Trilium internal
links because Evernote internal note IDs are not preserved in ENEX files.</p>
<p>If you want to restore the internal links in Trilium after you import
all of your ENEX files, you can use or adapt this custom script:&nbsp;
<a
class="reference-link" href="#root/_help_dj3j8dG4th4l">Process internal links by title</a>
</p>
<p>The script does the following:</p>
<ol>
<li>It finds all Evernote internal links.</li>
<li>For each one, it checks if its link text matches a note title, and if
yes, it replaces the Evernote link with an internal Trilium link. If not,
it leaves the Evernote link in place.</li>
<li>If it finds more than one note with a matching note title, it leaves the
Evernote link in place.</li>
<li>It outputs the results in a log that you can see in the respective code
note in Trilium.</li>
</ol>
<p>The script has the following limitations:</p>
<ul>
<li>It will not fix links to anchors and links to notes that you renamed in
Evernote after you created the links.</li>
<li>Some note titles might not be well identified, even if they exist. This
is especially the case if the note title contains some special characters.
Should this be problematic, consider&nbsp;<a class="reference-link" href="#root/_help_wy8So3yZZlH9">Reporting issues</a>.</li>
</ul>

View File

@@ -0,0 +1,35 @@
const query = `note.type = "text" and note.content *=* "evernote:///view/"`;
const notes = api.searchForNotes(query);
for (const note of notes) {
api.log(`Processing note ${note.title}...`);
const content = note.getContent();
const $ = api.cheerio.load(content);
$("a").each((i, el) => {
const $el = $(el);
const url = $el.attr("href");
if (!url.startsWith("evernote:///")) return;
const text = $el.text();
const matchingNotes = api.searchForNotes(`note.title = "${text}"`);
if (matchingNotes.length === 0) {
api.log(`No matching notes for "${text}..."`);
return;
}
if (matchingNotes.length > 1) {
api.log(`Found multiple matching notes for "${text}". Skipping.`);
return;
}
const matchingNote = matchingNotes[0];
api.log(`Found matching note: ${matchingNote.title} ${matchingNote.noteId}`);
$el.attr("href", `#root/${matchingNote.noteId}`);
$el.addClass("reference-link");
});
note.setContent($("body").html());
}

View File

@@ -8,39 +8,37 @@
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
<li>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>
<li>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
notes. This works for both&nbsp;<a class="reference-link" href="#root/_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
<li>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.
<li>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>,
<li>For&nbsp;<a class="reference-link" href="#root/_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>
<li>For normal notes, it's still possible to create children via other means
such as using the&nbsp;<a class="reference-link" href="#root/_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>
</li>
<li>Notes can be dragged from outside the note, case in which they will be
cloned into it.
<ul>
<li>Instead of switching to the child notes that were copied, the parent note
is highlighted instead.</li>
<li>A notification will indicate this behavior.</li>
</ul>
</li>
<li>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">
@@ -52,12 +50,11 @@
<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;
<li>The note position is clearly visible when using the&nbsp;<a class="reference-link"
href="#root/_help_eIg8jdvaoNNd">Search</a>.</li>
<li>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>
class="reference-link" href="#root/_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>
@@ -67,29 +64,27 @@
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
<p>By default, some of the&nbsp;<a class="reference-link" href="#root/_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>
href="#root/_help_CtBQqbwXDx1w">Kanban Board</a>&nbsp;or the&nbsp;<a class="reference-link"
href="#root/_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>
href="#root/_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>,
<li>In the&nbsp;<a class="reference-link" href="#root/_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>
<li>Right click the collection note in the&nbsp;<a class="reference-link"
href="#root/_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
To do so, right click the note in the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;and
select <em>Advanced</em><em>Hide subtree.</em>
</p>

View File

@@ -148,10 +148,10 @@
<td>
<p>Which view to display in the calendar:</p>
<ul>
<li data-list-item-id="e2cd230dc41f41fe91ee74d7d1fa87372"><code>timeGridWeek</code> for the <em>week</em> view;</li>
<li data-list-item-id="eee1dba4c6cc51ebd53d0a0dd52044cd6"><code>dayGridMonth</code> for the <em>month</em> view;</li>
<li data-list-item-id="ed8721a76a1865dac882415f662ed45b9"><code>multiMonthYear</code> for the <em>year</em> view;</li>
<li data-list-item-id="edf09a13759102d98dac34c33eb690c05"><code>listMonth</code> for the <em>list</em> view.</li>
<li><code>timeGridWeek</code> for the <em>week</em> view;</li>
<li><code>dayGridMonth</code> for the <em>month</em> view;</li>
<li><code>multiMonthYear</code> for the <em>year</em> view;</li>
<li><code>listMonth</code> for the <em>list</em> view.</li>
</ul>
<p>Any other value will be dismissed and the default view (month) will be
used instead.</p>

View File

@@ -33,12 +33,12 @@
</td>
<td>
<ul>
<li data-list-item-id="e04c84d59d44645ee89b2a8541ed99f90">Headings (section titles, paragraph)</li>
<li data-list-item-id="e39d25bd3d8bd06185b9d259e5827d451">Font size</li>
<li data-list-item-id="e1f7e2a2f4b03449d82bdf5b5c6ea8d44">Bold, italic, underline, strike-through</li>
<li data-list-item-id="e3decae72884f65b4d538151b6a297072">Superscript, subscript</li>
<li data-list-item-id="e59adf00fef65304c163ae190fac5e92a">Font color &amp; background color</li>
<li data-list-item-id="ed3f09156147a2769e91db111c76376e2">Remove formatting</li>
<li>Headings (section titles, paragraph)</li>
<li>Font size</li>
<li>Bold, italic, underline, strike-through</li>
<li>Superscript, subscript</li>
<li>Font color &amp; background color</li>
<li>Remove formatting</li>
</ul>
</td>
</tr>
@@ -47,9 +47,9 @@
</td>
<td>
<ul>
<li data-list-item-id="ee87806a913900d85d8f018af81f41df8">Bulleted lists</li>
<li data-list-item-id="e3ae314e365fa418ca6e0f061d63834c5">Numbered lists</li>
<li data-list-item-id="ee84e08694165f95430046cb34f4cd123">To-do lists</li>
<li>Bulleted lists</li>
<li>Numbered lists</li>
<li>To-do lists</li>
</ul>
</td>
</tr>
@@ -58,8 +58,8 @@
</td>
<td>
<ul>
<li data-list-item-id="e2892dc35a0d4b7ad65daffb8f9404daa">Block quotes</li>
<li data-list-item-id="e7297e3ad1002f8de15aa0bd66c6f3f22">Admonitions</li>
<li>Block quotes</li>
<li>Admonitions</li>
</ul>
</td>
</tr>
@@ -68,10 +68,10 @@
</td>
<td>
<ul>
<li data-list-item-id="eb358a4567d93f66004f4195df2dda05a">Basic tables</li>
<li data-list-item-id="e6135a555d6c63c30e4b84806a4870830">Merging cells</li>
<li data-list-item-id="e29ac76563d0998b28fb1baf94dbdac8c">Styling tables and cells.</li>
<li data-list-item-id="e372446e81fdedada64b8bed89ca93d1a">Table captions</li>
<li>Basic tables</li>
<li>Merging cells</li>
<li>Styling tables and cells.</li>
<li>Table captions</li>
</ul>
</td>
</tr>
@@ -80,9 +80,9 @@
</td>
<td>
<ul>
<li data-list-item-id="eb260b76afcbc07bd9d4ceec4e000e8a0">Inline code</li>
<li data-list-item-id="e9864352286369ebe7b41c1599f498de8">Code blocks</li>
<li data-list-item-id="ee62fb9ed7f349178e8f2a2bd9ec8cd74">Keyboard shortcuts</li>
<li>Inline code</li>
<li>Code blocks</li>
<li>Keyboard shortcuts</li>
</ul>
</td>
</tr>
@@ -91,7 +91,7 @@
</td>
<td>
<ul>
<li data-list-item-id="edf62ec004eff35cfcb7e361deef19aaf">Footnotes</li>
<li>Footnotes</li>
</ul>
</td>
</tr>
@@ -100,7 +100,7 @@
</td>
<td>
<ul>
<li data-list-item-id="ebe6277e643041403489c3ceb30c36f7f">Images</li>
<li>Images</li>
</ul>
</td>
</tr>
@@ -109,8 +109,8 @@
</td>
<td>
<ul>
<li data-list-item-id="e3f988be2f259bb40607cb61541955395">External links</li>
<li data-list-item-id="e3f91cc4f0cccd2c077cc306bacd68ef2">Internal Trilium links</li>
<li>External links</li>
<li>Internal Trilium links</li>
</ul>
</td>
</tr>
@@ -119,7 +119,7 @@
</td>
<td>
<ul>
<li data-list-item-id="eac8015a64bce7b749cc67d1599062007">Include note</li>
<li>Include note</li>
</ul>
</td>
</tr>
@@ -128,12 +128,12 @@
</td>
<td>
<ul>
<li data-list-item-id="e5cdf5d3885ec0ea67f924b4b8fe5c483">Symbols</li>
<li data-list-item-id="e95082e6642ed5b1eec6e4e116b899a40"><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
<li>Symbols</li>
<li><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
</li>
<li data-list-item-id="ecbef6a358a5b8d27f0d3e08bbc750aa9">Mermaid diagrams</li>
<li data-list-item-id="e6e97ee14dd29b7ccf53227107e5dc72d">Horizontal ruler</li>
<li data-list-item-id="e6198c7c535c249faec2e8906775f11de">Page break</li>
<li>Mermaid diagrams</li>
<li>Horizontal ruler</li>
<li>Page break</li>
</ul>
</td>
</tr>
@@ -142,12 +142,12 @@
</td>
<td>
<ul>
<li data-list-item-id="e0c14456cb83d483b07ea432ef9d4728e">Indentation
<li>Indentation
<ul>
<li data-list-item-id="e2029812c5e105c595590f70ee227631e">Markdown import</li>
<li>Markdown import</li>
</ul>
</li>
<li data-list-item-id="ea1ee012286e05190c89c9f4e64cf2036"><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
<li><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
</li>
</ul>
</td>
@@ -157,11 +157,11 @@
</td>
<td>
<ul>
<li data-list-item-id="e1ab173193a533ccf33dccfd0cb916f1f"><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
<li><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
</li>
<li data-list-item-id="e564b978c09fe5adf476b331b1e0640e3"><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
<li><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
</li>
<li data-list-item-id="e756306c31d9beffbba3820b6d1b9bc61"><a class="reference-link" href="#root/_help_5wZallV2Qo1t">Format Painter</a>
<li><a class="reference-link" href="#root/_help_5wZallV2Qo1t">Format Painter</a>
</li>
</ul>
</td>

View File

@@ -19,14 +19,14 @@
<td>
<p>Defines on which events script should run. Possible values are:</p>
<ul>
<li data-list-item-id="e244b14e102cf1b0d4954e8fd455ea77b"><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed),
<li><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed),
but not on mobile.</li>
<li data-list-item-id="ea8f8ca86e7b351dd86108848ccb9103a"><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed),
<li><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed),
on mobile.</li>
<li data-list-item-id="e658488cf1a0862603088ef384e41b8b6"><code>backendStartup</code> - when Trilium backend starts up</li>
<li data-list-item-id="ef40ba992fc450d33a18ca4cb031eca66"><code>hourly</code> - run once an hour. You can use additional label <code>runAtHour</code> to
<li><code>backendStartup</code> - when Trilium backend starts up</li>
<li><code>hourly</code> - run once an hour. You can use additional label <code>runAtHour</code> to
specify at which hour, on the back-end.</li>
<li data-list-item-id="e07458d4f55b6eb42468a5535b8425c5f"><code>daily</code> - run once a day, on the back-end</li>
<li><code>daily</code> - run once a day, on the back-end</li>
</ul>
</td>
</tr>

View File

@@ -0,0 +1,27 @@
<h2>v0.102.0: Upgrade to jQuery 4.0.0</h2>
<p>jQuery 4 removes legacy browser support (such as IE11 support), but it
also removes some APIs that are considered deprecated such as:</p>
<blockquote>
<p><code spellcheck="false">jQuery.isArray</code>, <code spellcheck="false">jQuery.parseJSON</code>,
<code
spellcheck="false">jQuery.trim</code>, <code spellcheck="false">jQuery.type</code>, <code spellcheck="false">jQuery.now</code>,
<code
spellcheck="false">jQuery.isNumeric</code>, <code spellcheck="false">jQuery.isFunction</code>,
<code
spellcheck="false">jQuery.isWindow</code>, <code spellcheck="false">jQuery.camelCase</code>,
<code
spellcheck="false">jQuery.nodeName</code>, <code spellcheck="false">jQuery.cssNumber</code>,
<code
spellcheck="false">jQuery.cssProps</code>, and <code spellcheck="false">jQuery.fx.interval</code>.</p>
<p>Use native equivalents like <code spellcheck="false">Array.isArray()</code>,
<code
spellcheck="false">JSON.parse()</code>, <code spellcheck="false">String.prototype.trim()</code>,
and <code spellcheck="false">Date.now()</code> instead.</p>
</blockquote>
<p>This may affect custom scripts if they (or the custom jQuery libraries
used) depend on the deprecated APIs.</p>
<p>Note that Trilium polyfills <code spellcheck="false">jQuery.isArray</code>,
<code
spellcheck="false">isFunction</code>and <code spellcheck="false">isPlainObject</code> because
they were required by one of our dependencies (the autocomplete).</p>
<p>For more information, consult <a href="https://blog.jquery.com/2026/01/17/jquery-4-0-0/">the official blog post</a>.</p>

View File

@@ -107,10 +107,10 @@ class="ck-table-resized">
</td>
<td>
<ul>
<li data-list-item-id="ec06332efcc3039721606c052f0d913fa">The widget must export a <code>class</code> and not an instance of the class
<li>The widget must export a <code>class</code> and not an instance of the class
(e.g. <code>no new</code>) because it needs to be multiplied for each note,
so that splits work correctly.</li>
<li data-list-item-id="e8da690a2a8df148f6b5fc04ba1611688">Since the <code>class</code> is exported instead of an instance, the <code>parentWidget</code> getter
<li>Since the <code>class</code> is exported instead of an instance, the <code>parentWidget</code> getter
must be <code>static</code>, otherwise the widget is ignored.</li>
</ul>
</td>
@@ -124,7 +124,7 @@ class="ck-table-resized">
</td>
<td>
<ul>
<li data-list-item-id="efe008d361e224f422582552648e1afe7">Although not mandatory, it's best to use a <code>RightPanelWidget</code> instead
<li>Although not mandatory, it's best to use a <code>RightPanelWidget</code> instead
of a <code>BasicWidget</code> or a <code>NoteContextAwareWidget</code>.</li>
</ul>
</td>

View File

@@ -381,8 +381,6 @@
"tooltip": "Trilium Notes",
"close": "Quit Trilium",
"recents": "Recent notes",
"recently-closed-windows": "Recently closed windows",
"tabs-total": "{{number}} tabs total",
"bookmarks": "Bookmarks",
"today": "Open today's journal note",
"new-note": "New note",

View File

@@ -27,6 +27,8 @@
"search-in-subtree": "एक्टिव नोट के सब-ट्री में नोट्स खोजें",
"expand-subtree": "मौजूदा नोट के सब-ट्री को (subtree) एक्सपैंड करें",
"delete-note": "नोट डिलीट करें",
"move-note-up-in-hierarchy": "नोट एक लेवल ऊपर मूव करें"
"move-note-up-in-hierarchy": "नोट एक लेवल ऊपर मूव करें",
"move-note-down-in-hierarchy": "नोट एक लेवल नीचे ले जाएँ",
"dialogs": "डायलॉग्स"
}
}

View File

@@ -9,7 +9,8 @@
"search-in-subtree": "Søk etter notater i det aktive notatets understruktur",
"creating-and-moving-notes": "Lage og flytte notater",
"dialogs": "Dialogbokser",
"other": "Andre"
"other": "Andre",
"expand-subtree": "Utvid undertre for gjeldende notat"
},
"setup_sync-from-desktop": {
"step6-here": "her"

View File

@@ -11,6 +11,11 @@
"move-note-up": "Notu bir üste taşı",
"collapse-tree": "Tüm not ağacını daraltır",
"collapse-subtree": "Geçerli notun alt ağacını daraltır",
"sort-child-notes": "Alt notları sırala"
"sort-child-notes": "Alt notları sırala",
"creating-and-moving-notes": "Notları oluşturma ve yerlerini değiştirme",
"create-note-into": "Aktif nota bağlı alt not oluştur",
"create-note-after": "Aktif nottan sonra yeni bir not oluştur",
"delete-note": "Notu sil",
"move-note-down": "Notu aşağıya kaydır"
}
}

View File

@@ -1,5 +1,5 @@
import type { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons";
import { dayjs } from "@triliumnext/commons";
import { dayjs, getNoteIcon } from "@triliumnext/commons";
import cloningService from "../../services/cloning.js";
import dateUtils from "../../services/date_utils.js";
@@ -24,26 +24,6 @@ import BRevision from "./brevision.js";
const LABEL = "label";
const RELATION = "relation";
// TODO: Deduplicate with fnote
export const NOTE_TYPE_ICONS = {
file: "bx bx-file",
image: "bx bx-image",
code: "bx bx-code",
render: "bx bx-extension",
search: "bx bx-file-find",
relationMap: "bx bxs-network-chart",
book: "bx bx-book",
noteMap: "bx bxs-network-chart",
mermaid: "bx bx-selection",
canvas: "bx bx-pen",
webView: "bx bx-globe-alt",
launcher: "bx bx-link",
doc: "bx bxs-file-doc",
contentWidget: "bx bxs-widget",
mindMap: "bx bx-sitemap",
geoMap: "bx bx-map-alt"
};
interface NotePathRecord {
isArchived: boolean;
isInHoistedSubTree: boolean;
@@ -1698,30 +1678,17 @@ class BNote extends AbstractBeccaEntity<BNote> {
}
getIcon() {
return `tn-icon ${this.#getIconInternal()}`;
}
// TODO: Deduplicate with fnote
#getIconInternal() {
const iconClassLabels = this.getLabels("iconClass");
const icon = getNoteIcon({
noteId: this.noteId,
type: this.type,
mime: this.mime,
iconClass: iconClassLabels.length > 0 ? iconClassLabels[0].value : undefined,
workspaceIconClass: undefined,
isFolder: this.isFolder.bind(this)
});
if (iconClassLabels && iconClassLabels.length > 0) {
return iconClassLabels[0].value;
} else if (this.noteId === "root") {
return "bx bx-home-alt-2";
}
if (this.noteId === "_share") {
return "bx bx-share-alt";
} else if (this.type === "text") {
if (this.isFolder()) {
return "bx bx-folder";
}
return "bx bx-note";
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
return "bx bx-data";
}
return NOTE_TYPE_ICONS[this.type];
return `tn-icon ${icon}`;
}
// TODO: Deduplicate with fnote

View File

@@ -1,48 +0,0 @@
import cls from "../services/cls.js";
import sql from "../services/sql.js";
export default () => {
cls.init(() => {
const row = sql.getRow<{ value: string }>(
`SELECT value FROM options WHERE name = 'openNoteContexts'`
);
if (!row || !row.value) {
return;
}
let parsed: any;
try {
parsed = JSON.parse(row.value);
} catch {
return;
}
// Already in new format (array + windowId), skip
if (
Array.isArray(parsed) &&
parsed.length > 0 &&
parsed[0] &&
typeof parsed[0] === "object" &&
parsed[0].windowId
) {
return;
}
// Old format: just contexts
const migrated = [
{
windowId: "main",
createdAt: 0,
closedAt: 0,
contexts: parsed
}
];
sql.execute(
`UPDATE options SET value = ? WHERE name = 'openNoteContexts'`,
[JSON.stringify(migrated)]
);
});
};

View File

@@ -6,11 +6,6 @@
// Migrations should be kept in descending order, so the latest migration is first.
const MIGRATIONS: (SqlMigration | JsMigration)[] = [
// Migrate openNoteContexts option to the new structured format with window metadata
{
version: 234,
module: async () => import("./0234__migrate_open_note_contexts_format")
},
// Migrate geo map to collection
{
version: 233,

View File

@@ -50,7 +50,6 @@ export function bootstrap(req: Request, res: Response) {
appCssNoteIds: getAppCssNoteIds(),
isDev,
isMainWindow: view === "mobile" ? true : !req.query.extraWindow,
windowId: req.query.extraWindow ?? "main",
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
triliumVersion: packageJson.version,
assetPath,

View File

@@ -4,7 +4,7 @@ import packageJson from "../../package.json" with { type: "json" };
import dataDir from "./data_dir.js";
import { AppInfo } from "@triliumnext/commons";
const APP_DB_VERSION = 234;
const APP_DB_VERSION = 233;
const SYNC_VERSION = 36;
const CLIPPER_PROTOCOL_VERSION = "1.0";

View File

@@ -387,4 +387,58 @@ describe("Markdown export", () => {
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
it("maintains escaped HTML tags", () => {
const html = /*html*/`<p>&lt;div&gt;Hello World&lt;/div&gt;</p>`;
const expected = `\\<div\\>Hello World\\</div\\>`;
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
it("escapes HTML tags inside list", () => {
const html = trimIndentation/*html*/`\
<ul>
<li data-list-item-id="e07fda078f7dd7103a3b9017f49eb1589">
&lt;note&gt; is note.
</li>
</ul>
`;
const expected = trimIndentation`\
* \\<note\\> is note.`;
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
it("exports jQuery code in table properly", () => {
const html = trimIndentation`\
<figure class="table">
<table>
<thead>
<tr>
<th>
Code
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<pre>
<code class="language-text-x-trilium-auto">this.$widget = $("&lt;div&gt;");</code>
</pre>
</td>
</tr>
</tbody>
</table>
</figure>
`;
const expected = trimIndentation`\
<table><thead><tr><th>Code</th></tr></thead><tbody><tr><td><pre><code class="language-text-x-trilium-auto">this.$widget = $("&lt;div&gt;");</code>
</pre></td></tr></tbody></table>`;
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
it("renders emphasis with underscore", () => {
const html = /*html*/`<p>This is <em>underlined</em> text.</p>`;
const expected = `This is _underlined_ text.`;
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
});

View File

@@ -1,9 +1,7 @@
"use strict";
import TurndownService, { type Rule } from "turndown";
import { gfm } from "@triliumnext/turndown-plugin-gfm";
import Turnish, { type Rule } from "turnish";
let instance: TurndownService | null = null;
let instance: Turnish | null = null;
// TODO: Move this to a dedicated file someday.
export const ADMONITION_TYPE_MAPPINGS: Record<string, string> = {
@@ -16,12 +14,12 @@ export const ADMONITION_TYPE_MAPPINGS: Record<string, string> = {
export const DEFAULT_ADMONITION_TYPE = ADMONITION_TYPE_MAPPINGS.note;
const fencedCodeBlockFilter: TurndownService.Rule = {
filter: function (node, options) {
const fencedCodeBlockFilter: Rule = {
filter (node, options) {
return options.codeBlockStyle === "fenced" && node.nodeName === "PRE" && node.firstChild !== null && node.firstChild.nodeName === "CODE";
},
replacement: function (content, node, options) {
replacement (content, node, options) {
if (!node.firstChild || !("getAttribute" in node.firstChild) || typeof node.firstChild.getAttribute !== "function") {
return content;
}
@@ -29,23 +27,25 @@ const fencedCodeBlockFilter: TurndownService.Rule = {
const className = node.firstChild.getAttribute("class") || "";
const language = rewriteLanguageTag((className.match(/language-(\S+)/) || [null, ""])[1]);
return "\n\n" + options.fence + language + "\n" + node.firstChild.textContent + "\n" + options.fence + "\n\n";
return `\n\n${options.fence}${language}\n${node.firstChild.textContent}\n${options.fence}\n\n`;
}
};
function toMarkdown(content: string) {
if (instance === null) {
instance = new TurndownService({
instance = new Turnish({
headingStyle: "atx",
bulletListMarker: "*",
emDelimiter: "_",
codeBlockStyle: "fenced",
blankReplacement(content, node, options) {
if (node.nodeName === "SECTION" && (node as HTMLElement).classList.contains("include-note")) {
return (node as HTMLElement).outerHTML;
blankReplacement(_content, node) {
if (node.nodeName === "SECTION" && node.classList.contains("include-note")) {
return node.outerHTML;
}
// Original implementation as per https://github.com/mixmark-io/turndown/blob/master/src/turndown.js.
return ("isBlock" in node && node.isBlock) ? '\n\n' : ''
}
return ("isBlock" in node && node.isBlock) ? '\n\n' : '';
},
});
// Filter is heavily based on: https://github.com/mixmark-io/turndown/issues/274#issuecomment-458730974
instance.addRule("fencedCodeBlock", fencedCodeBlockFilter);
@@ -59,7 +59,7 @@ function toMarkdown(content: string) {
instance.keep([ "kbd", "sup", "sub" ]);
}
return instance.turndown(content);
return instance.render(content);
}
function rewriteLanguageTag(source: string) {
@@ -85,14 +85,14 @@ function buildImageFilter() {
const ESCAPE_PATTERNS = {
before: /([\\*`[\]_]|(?:^[-+>])|(?:^~~~)|(?:^#{1-6}))/g,
after: /((?:^\d+(?=\.)))/
}
};
const escapePattern = new RegExp('(?:' + ESCAPE_PATTERNS.before.source + '|' + ESCAPE_PATTERNS.after.source + ')', 'g');
const escapePattern = new RegExp(`(?:${ESCAPE_PATTERNS.before.source}|${ESCAPE_PATTERNS.after.source})`, 'g');
function escapeMarkdown (content: string) {
return content.replace(escapePattern, function (match, before, after) {
return before ? '\\' + before : after + '\\'
})
return content.replace(escapePattern, (match, before, after) => {
return before ? `\\${before}` : `${after}\\`;
});
}
function escapeLinkDestination(destination: string) {
@@ -102,10 +102,10 @@ function buildImageFilter() {
}
function escapeLinkTitle (title: string) {
return title.replace(/"/g, '\\"')
return title.replace(/"/g, '\\"');
}
const imageFilter: TurndownService.Rule = {
const imageFilter: Rule = {
filter: "img",
replacement(content, _node) {
const node = _node as HTMLElement;
@@ -117,12 +117,12 @@ function buildImageFilter() {
// TODO: Deduplicate with upstream.
const untypedNode = (node as any);
const alt = escapeMarkdown(cleanAttribute(untypedNode.getAttribute('alt')))
const src = escapeLinkDestination(untypedNode.getAttribute('src') || '')
const title = cleanAttribute(untypedNode.getAttribute('title'))
const titlePart = title ? ' "' + escapeLinkTitle(title) + '"' : ''
const alt = escapeMarkdown(cleanAttribute(untypedNode.getAttribute('alt')));
const src = escapeLinkDestination(untypedNode.getAttribute('src') || '');
const title = cleanAttribute(untypedNode.getAttribute('title'));
const titlePart = title ? ` "${escapeLinkTitle(title)}"` : '';
return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : ''
return src ? `![${alt}](${src}${titlePart})` : '';
}
};
return imageFilter;
@@ -151,7 +151,7 @@ function buildAdmonitionFilter() {
return DEFAULT_ADMONITION_TYPE;
}
const admonitionFilter: TurndownService.Rule = {
const admonitionFilter: Rule = {
filter(node, options) {
return node.nodeName === "ASIDE" && node.classList.contains("admonition");
},
@@ -161,11 +161,11 @@ function buildAdmonitionFilter() {
content = content.replace(/^\n+|\n+$/g, '');
content = content.replace(/^/gm, '> ');
content = `> [!${admonitionType}]\n` + content;
content = `> [!${admonitionType}]\n${content}`;
return "\n\n" + content + "\n\n";
return `\n\n${content}\n\n`;
}
}
};
return admonitionFilter;
}
@@ -178,15 +178,15 @@ function buildAdmonitionFilter() {
*/
function buildInlineLinkFilter(): Rule {
return {
filter: function (node, options) {
filter (node, options) {
return (
options.linkStyle === 'inlined' &&
node.nodeName === 'A' &&
!!node.getAttribute('href')
)
);
},
replacement: function (content, _node) {
replacement (content, _node) {
const node = _node as HTMLElement;
// Return reference links verbatim.
@@ -196,13 +196,13 @@ function buildInlineLinkFilter(): Rule {
// Otherwise treat as normal.
// TODO: Call super() somehow instead of duplicating the implementation.
let href = node.getAttribute('href')
if (href) href = href.replace(/([()])/g, '\\$1')
let title = cleanAttribute(node.getAttribute('title'))
if (title) title = ' "' + title.replace(/"/g, '\\"') + '"'
return '[' + content + '](' + href + title + ')'
let href = node.getAttribute('href');
if (href) href = href.replace(/([()])/g, '\\$1');
let title = cleanAttribute(node.getAttribute('title'));
if (title) title = ` "${title.replace(/"/g, '\\"')}"`;
return `[${content}](${href}${title})`;
}
}
};
}
function buildFigureFilter(): Rule {
@@ -214,7 +214,7 @@ function buildFigureFilter(): Rule {
replacement(content, node) {
return (node as HTMLElement).outerHTML;
}
}
};
}
// Keep in line with https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js.
@@ -224,13 +224,13 @@ function buildListItemFilter(): Rule {
replacement(content, node, options) {
content = content
.trim()
.replace(/\n/gm, '\n ') // indent
let prefix = options.bulletListMarker + ' '
.replace(/\n/gm, '\n '); // indent
let prefix = `${options.bulletListMarker} `;
const parent = node.parentNode as HTMLElement;
if (parent.nodeName === 'OL') {
var start = parent.getAttribute('start')
var index = Array.prototype.indexOf.call(parent.children, node)
prefix = (start ? Number(start) + index : index + 1) + '. '
const start = parent.getAttribute('start');
const index = Array.prototype.indexOf.call(parent.children, node);
prefix = `${start ? Number(start) + index : index + 1}. `;
} else if (parent.classList.contains("todo-list")) {
const isChecked = node.querySelector("input[type=checkbox]:checked");
prefix = (isChecked ? "- [x] " : "- [ ] ");
@@ -239,7 +239,7 @@ function buildListItemFilter(): Rule {
const result = prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
return result;
}
}
};
}
function buildMathFilter(): Rule {
@@ -270,13 +270,13 @@ function buildMathFilter(): Rule {
// Unknown.
return content;
}
}
};
}
// Taken from upstream since it's not exposed.
// https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js
function cleanAttribute(attribute: string | null | undefined) {
return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : ''
return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '';
}
export default {

View File

@@ -314,4 +314,9 @@ $$`;
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
});
it("preserves HTML entities in list", () => {
const input = `* &lt;note&gt; is note.`;
const expected = /*html*/`<ul><li>&lt;note&gt; is note.</li></ul>`;
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
});
});

View File

@@ -1,18 +1,16 @@
"use strict";
import type { NoteType } from "@triliumnext/commons";
import type BNote from "../../becca/entities/bnote.js";
import type TaskContext from "../task_context.js";
import noteService from "../../services/notes.js";
import imageService from "../../services/image.js";
import noteService from "../../services/notes.js";
import { getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
import htmlSanitizer from "../html_sanitizer.js";
import protectedSessionService from "../protected_session.js";
import type TaskContext from "../task_context.js";
import type { File } from "./common.js";
import markdownService from "./markdown.js";
import mimeService from "./mime.js";
import { getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
import importUtils from "./utils.js";
import htmlSanitizer from "../html_sanitizer.js";
import type { File } from "./common.js";
import type { NoteType } from "@triliumnext/commons";
function importSingleFile(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
const mime = mimeService.getMime(file.originalname) || file.mimetype;
@@ -56,13 +54,14 @@ function importImage(file: File, parentNote: BNote, taskContext: TaskContext<"im
function importFile(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
const originalName = file.originalname;
const mime = mimeService.getMime(originalName) || file.mimetype;
const { note } = noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: originalName,
title: getNoteTitle(originalName, mime === "application/pdf"),
content: file.buffer,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
type: "file",
mime: mimeService.getMime(originalName) || file.mimetype
mime
});
note.addLabel("originalFileName", originalName);
@@ -88,7 +87,7 @@ function importCodeNote(taskContext: TaskContext<"importNotes">, file: File, par
title,
content,
type,
mime: mime,
mime,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
@@ -106,7 +105,7 @@ function importCustomType(taskContext: TaskContext<"importNotes">, file: File, p
title,
content,
type,
mime: mime,
mime,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
@@ -214,7 +213,7 @@ function importAttachment(taskContext: TaskContext<"importNotes">, file: File, p
title: file.originalname,
content: file.buffer,
role: "file",
mime: mime
mime
});
taskContext.increaseProgressCount();

View File

@@ -1,26 +1,27 @@
"use strict";
import BAttribute from "../../becca/entities/battribute.js";
import { removeTextFileExtension, newEntityId, getNoteTitle, processStringOrBuffer, unescapeHtml } from "../../services/utils.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import attributeService from "../../services/attributes.js";
import BBranch from "../../becca/entities/bbranch.js";
import { ALLOWED_NOTE_TYPES, type NoteType } from "@triliumnext/commons";
import path from "path";
import protectedSessionService from "../protected_session.js";
import mimeService from "./mime.js";
import treeService from "../tree.js";
import type { Stream } from "stream";
import yauzl from "yauzl";
import htmlSanitizer from "../html_sanitizer.js";
import becca from "../../becca/becca.js";
import BAttachment from "../../becca/entities/battachment.js";
import markdownService from "./markdown.js";
import type TaskContext from "../task_context.js";
import BAttribute from "../../becca/entities/battribute.js";
import BBranch from "../../becca/entities/bbranch.js";
import type BNote from "../../becca/entities/bnote.js";
import type NoteMeta from "../meta/note_meta.js";
import attributeService from "../../services/attributes.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import { getNoteTitle, newEntityId, processStringOrBuffer, removeFileExtension, unescapeHtml } from "../../services/utils.js";
import htmlSanitizer from "../html_sanitizer.js";
import type AttributeMeta from "../meta/attribute_meta.js";
import type { Stream } from "stream";
import { ALLOWED_NOTE_TYPES, type NoteType } from "@triliumnext/commons";
import type NoteMeta from "../meta/note_meta.js";
import protectedSessionService from "../protected_session.js";
import type TaskContext from "../task_context.js";
import treeService from "../tree.js";
import markdownService from "./markdown.js";
import mimeService from "./mime.js";
interface MetaFile {
files: NoteMeta[];
@@ -108,7 +109,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
dataFileName: ""
};
let parent: NoteMeta | undefined = undefined;
let parent: NoteMeta | undefined;
for (let segment of pathSegments) {
if (!cursor?.children?.length) {
@@ -161,7 +162,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
// in case we lack metadata, we treat e.g. "Programming.html" and "Programming" as the same note
// (one data file, the other directory for children)
const filePathNoExt = removeTextFileExtension(filePath);
const filePathNoExt = removeFileExtension(filePath);
if (filePathNoExt in createdPaths) {
return createdPaths[filePathNoExt];
@@ -241,10 +242,10 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
}
const { note } = noteService.createNewNote({
parentNoteId: parentNoteId,
parentNoteId,
title: noteTitle || "",
content: "",
noteId: noteId,
noteId,
type: resolveNoteType(noteMeta?.type),
mime: noteMeta ? noteMeta.mime : "text/html",
prefix: noteMeta?.prefix || "",
@@ -294,12 +295,12 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
noteId: getNewNoteId(noteMeta.noteId)
};
} else {
// don't check for noteMeta since it's not mandatory for notes
return {
noteId: getNoteId(noteMeta, absUrl)
};
}
}
// don't check for noteMeta since it's not mandatory for notes
return {
noteId: getNoteId(noteMeta, absUrl)
};
}
function processTextNoteContent(content: string, noteTitle: string, filePath: string, noteMeta?: NoteMeta) {
@@ -312,9 +313,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => {
if (noteTitle.trim() === text.trim()) {
return ""; // remove whole H1 tag
} else {
return `<h2>${text}</h2>`;
}
}
return `<h2>${text}</h2>`;
});
if (taskContext.data?.safeImport) {
@@ -347,9 +348,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
return `src="api/attachments/${target.attachmentId}/image/${path.basename(url)}"`;
} else if (target.noteId) {
return `src="api/images/${target.noteId}/${path.basename(url)}"`;
} else {
return match;
}
}
return match;
});
content = content.replace(/href="([^"]*)"/g, (match, url) => {
@@ -373,9 +374,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
return `href="#root/${target.noteId}?viewMode=attachments&attachmentId=${target.attachmentId}"`;
} else if (target.noteId) {
return `href="#root/${target.noteId}"`;
} else {
return match;
}
}
return match;
});
if (noteMeta) {
@@ -525,9 +526,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
}
({ note } = noteService.createNewNote({
parentNoteId: parentNoteId,
parentNoteId,
title: noteTitle || "",
content: content,
content,
noteId,
type,
mime,
@@ -536,7 +537,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
// root notePosition should be ignored since it relates to the original document
// now import root should be placed after existing notes into new parent
notePosition: noteMeta && firstNote ? noteMeta.notePosition : undefined,
isProtected: isProtected
isProtected
}));
createdNoteIds.add(note.noteId);
@@ -648,7 +649,7 @@ function streamToBuffer(stream: Stream): Promise<Buffer> {
export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer> {
return new Promise((res, rej) => {
zipfile.openReadStream(entry, function (err, readStream) {
zipfile.openReadStream(entry, (err, readStream) => {
if (err) rej(err);
if (!readStream) throw new Error("Unable to read content.");
@@ -659,7 +660,7 @@ export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise
export function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => Promise<void>) {
return new Promise<void>((res, rej) => {
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, function (err, zipfile) {
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, (err, zipfile) => {
if (err) rej(err);
if (!zipfile) throw new Error("Unable to read zip file.");
@@ -691,9 +692,9 @@ function resolveNoteType(type: string | undefined): NoteType {
if (type && (ALLOWED_NOTE_TYPES as readonly string[]).includes(type)) {
return type as NoteType;
} else {
return "text";
}
}
return "text";
}
export function removeTriliumTags(content: string) {
@@ -702,7 +703,7 @@ export function removeTriliumTags(content: string) {
"<title data-trilium-title>([^<]*)<\/title>"
];
for (const tag of tagsToRemove) {
let re = new RegExp(tag, "gi");
const re = new RegExp(tag, "gi");
content = content.replace(re, "");
}

View File

@@ -72,19 +72,6 @@ function getOptionBool(name: FilterOptionsByType<boolean>): boolean {
return val === "true";
}
function getOptionJson(name: OptionNames) {
const val = getOptionOrNull(name);
if (typeof val !== "string") {
return null;
}
try {
return JSON.parse(val);
} catch (e) {
return null;
}
}
function setOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T]) {
const option = becca.getOption(name);
@@ -150,7 +137,6 @@ export default {
getOption,
getOptionInt,
getOptionBool,
getOptionJson,
setOption,
createOption,
getOptions,

View File

@@ -45,15 +45,8 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts =
"openNoteContexts",
JSON.stringify([
{
windowId: "main",
createdAt: 0,
closedAt: 0,
contexts: [
{
notePath: "root",
active: true
}
]
notePath: "root",
active: true
}
]),
false
@@ -264,15 +257,8 @@ function initStartupOptions() {
"openNoteContexts",
JSON.stringify([
{
windowId: "main",
createdAt: 0,
closedAt: 0,
contexts: [
{
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
active: true
}
]
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
active: true
}
])
);

View File

@@ -147,15 +147,8 @@ async function createInitialDatabase(skipDemoDb?: boolean) {
"openNoteContexts",
JSON.stringify([
{
windowId: "main",
createdAt: 0,
closedAt: 0,
contexts: [
{
notePath: startNoteId,
active: true
}
]
notePath: startNoteId,
active: true
}
])
);

View File

@@ -196,39 +196,6 @@ function updateTrayMenu() {
return menuItems;
}
function buildClosedWindowsMenu() {
const savedWindows = optionService.getOptionJson("openNoteContexts") || [];
const openedWindowIds = windowService.getAllWindowIds();
const closedWindows = savedWindows
.filter(win => !openedWindowIds.includes(win.windowId))
.sort((a, b) => { return a.closedAt - b.closedAt; }); // sort by time in ascending order
const menuItems: Electron.MenuItemConstructorOptions[] = [];
for (let i = closedWindows.length - 1; i >= 0; i--) {
const win = closedWindows[i];
const activeCtx = win.contexts.find(c => c.active === true);
const activateNotePath = (activeCtx ?? win.contexts[0])?.notePath;
const activateNoteId = activateNotePath?.split("/").pop() ?? null;
if (!activateNoteId) continue;
// Get the title of the closed window
const winTitle = (() => {
const raw = becca_service.getNoteTitle(activateNoteId);
const truncated = raw.length > 20 ? `${raw.slice(0, 17)}` : raw;
const tabCount = win.contexts.filter(ctx => ctx.mainNtxId === null).length;
return tabCount > 1 ? `${truncated} (${t("tray.tabs-total", { number: tabCount })})` : truncated;
})();
menuItems.push({
label: winTitle,
type: "normal",
click: () => win.windowId !== "main" ? windowService.createExtraWindow(win.windowId, "") : windowService.createMainWindow()
});
}
return menuItems;
}
const windowVisibilityMenuItems: Electron.MenuItemConstructorOptions[] = [];
// Only call getWindowTitle if windowVisibilityMap has more than one window
@@ -291,12 +258,6 @@ function updateTrayMenu() {
icon: getIconPath("recents"),
submenu: buildRecentNotesMenu()
},
{
label: t("tray.recently-closed-windows"),
type: "submenu",
icon: getIconPath("closed-windows"),
submenu: buildClosedWindowsMenu()
},
{ type: "separator" },
{
label: t("tray.close"),

View File

@@ -1,11 +1,11 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { note, NoteBuilder } from "../test/becca_mocking.js";
import {beforeEach, describe, expect, it, vi} from "vitest";
import {note, NoteBuilder} from "../test/becca_mocking.js";
import becca from "../becca/becca.js";
import BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js";
import tree from "./tree.js";
import cls from "./cls.js";
import { buildNote } from "../test/becca_easy_mocking.js";
import {buildNote} from "../test/becca_easy_mocking.js";
describe("Tree", () => {
let rootNote!: NoteBuilder;
@@ -48,6 +48,23 @@ describe("Tree", () => {
};
});
});
it("sorts notes by title (base case)", () => {
const note = buildNote({
children: [
{title: "1"},
{title: "2"},
{title: "3"},
],
"#sorted": "",
});
cls.init(() => {
tree.sortNotesIfNeeded(note.noteId);
});
const orderedTitles = note.children.map((child) => child.title);
expect(orderedTitles).toStrictEqual(["1", "2", "3"]);
}
)
it("custom sort order is idempotent", () => {
rootNote.label("sorted", "order");
@@ -56,13 +73,15 @@ describe("Tree", () => {
for (let i = 0; i <= 5; i++) {
rootNote.child(note(String(i)).label("order", String(i)));
}
rootNote.child(note("top").label("top"));
rootNote.child(note("bottom").label("bottom"));
// Add a few values which have no defined order.
for (let i = 6; i < 10; i++) {
rootNote.child(note(String(i)));
}
const expectedOrder = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" ];
const expectedOrder = ["top", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "bottom"];
// Sort a few times to ensure that the resulting order is the same.
for (let i = 0; i < 5; i++) {
@@ -78,12 +97,12 @@ describe("Tree", () => {
it("pins to the top and bottom", () => {
const note = buildNote({
children: [
{ title: "bottom", "#bottom": "" },
{ title: "5" },
{ title: "3" },
{ title: "2" },
{ title: "1" },
{ title: "top", "#top": "" }
{title: "bottom", "#bottom": ""},
{title: "5"},
{title: "3"},
{title: "2"},
{title: "1"},
{title: "top", "#top": ""}
],
"#sorted": ""
});
@@ -91,18 +110,18 @@ describe("Tree", () => {
tree.sortNotesIfNeeded(note.noteId);
});
const orderedTitles = note.children.map((child) => child.title);
expect(orderedTitles).toStrictEqual([ "top", "1", "2", "3", "5", "bottom" ]);
expect(orderedTitles).toStrictEqual(["top", "1", "2", "3", "5", "bottom"]);
});
it("pins to the top and bottom in reverse order", () => {
const note = buildNote({
children: [
{ title: "bottom", "#bottom": "" },
{ title: "1" },
{ title: "2" },
{ title: "3" },
{ title: "5" },
{ title: "top", "#top": "" }
{title: "bottom", "#bottom": ""},
{title: "1"},
{title: "2"},
{title: "3"},
{title: "5"},
{title: "top", "#top": ""}
],
"#sorted": "",
"#sortDirection": "desc"
@@ -111,6 +130,50 @@ describe("Tree", () => {
tree.sortNotesIfNeeded(note.noteId);
});
const orderedTitles = note.children.map((child) => child.title);
expect(orderedTitles).toStrictEqual([ "top", "5", "3", "2", "1", "bottom" ]);
expect(orderedTitles).toStrictEqual(["top", "5", "3", "2", "1", "bottom"]);
});
it("keeps folder notes on top when #sortFolderFirst is set, but not above #top", () => {
const note = buildNote({
children: [
{title: "bottom", "#bottom": ""},
{title: "1"},
{title: "2"},
{title: "p1", children: [{title: "1.1"}, {title: "1.2"}]},
{title: "p2", children: [{title: "2.1"}, {title: "2.2"}]},
{title: "3"},
{title: "5"},
{title: "top", "#top": ""}
],
"#sorted": "",
"#sortFoldersFirst": ""
});
cls.init(() => {
tree.sortNotesIfNeeded(note.noteId);
});
const orderedTitles = note.children.map((child) => child.title);
expect(orderedTitles).toStrictEqual(["top", "p1", "p2", "1", "2", "3", "5", "bottom"]);
});
it("sorts notes accordingly when #sortNatural is set", () => {
const note = buildNote({
children: [
{title: "bottom", "#bottom": ""},
{title: "1"},
{title: "2"},
{title: "10"},
{title: "20"},
{title: "3"},
{title: "top", "#top": ""}
],
"#sorted": "",
"#sortNatural": ""
});
cls.init(() => {
tree.sortNotesIfNeeded(note.noteId);
});
const orderedTitles = note.children.map((child) => child.title);
expect(orderedTitles).toStrictEqual(["top", "1", "2", "3", "10", "20", "bottom"]);
}
)
});

View File

@@ -98,15 +98,6 @@ function sortNotes(parentNoteId: string, customSortBy: string = "title", reverse
}
notes.sort((a, b) => {
if (foldersFirst) {
const aHasChildren = a.hasChildren();
const bHasChildren = b.hasChildren();
if ((aHasChildren && !bHasChildren) || (!aHasChildren && bHasChildren)) {
// exactly one note of the two is a directory, so the sorting will be done based on this status
return aHasChildren ? -1 : 1;
}
}
function fetchValue(note: BNote, key: string) {
let rawValue: string | null;
@@ -154,6 +145,16 @@ function sortNotes(parentNoteId: string, customSortBy: string = "title", reverse
return compare(bottomBEl, bottomAEl) * (reverse ? -1 : 1);
}
if (foldersFirst) {
const aHasChildren = a.hasChildren();
const bHasChildren = b.hasChildren();
if ((aHasChildren && !bHasChildren) || (!aHasChildren && bHasChildren)) {
// exactly one note of the two is a directory, so the sorting will be done based on this status
return aHasChildren ? -1 : 1;
}
}
const customAEl = fetchValue(a, customSortBy) ?? fetchValue(a, "title") as string;
const customBEl = fetchValue(b, customSortBy) ?? fetchValue(b, "title") as string;

View File

@@ -1,4 +1,5 @@
import { describe, it, expect } from "vitest";
import { describe, expect,it } from "vitest";
import utils from "./utils.js";
type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>];
@@ -120,7 +121,7 @@ describe("#toObject", () => {
{ testPropA: "keyA", testPropB: "valueA" },
{ testPropA: "keyB", testPropB: "valueB" }
];
const fn: TestListFn = (testListEntry: TestListEntry) => [ testListEntry.testPropA + "_fn", testListEntry.testPropB + "_fn" ];
const fn: TestListFn = (testListEntry: TestListEntry) => [ `${testListEntry.testPropA }_fn`, `${testListEntry.testPropB }_fn` ];
const result = utils.toObject(testList, fn);
expect(result).toStrictEqual({
@@ -240,8 +241,8 @@ describe.todo("#quoteRegex", () => {});
describe.todo("#replaceAll", () => {});
describe("#removeTextFileExtension", () => {
const testCases: TestCase<typeof utils.removeTextFileExtension>[] = [
describe("#removeFileExtension", () => {
const testCases: TestCase<typeof utils.removeFileExtension>[] = [
[ "w/ 'test.md' it should strip '.md'", [ "test.md" ], "test" ],
[ "w/ 'test.markdown' it should strip '.markdown'", [ "test.markdown" ], "test" ],
[ "w/ 'test.html' it should strip '.html'", [ "test.html" ], "test" ],
@@ -252,7 +253,7 @@ describe("#removeTextFileExtension", () => {
testCases.forEach((testCase) => {
const [ desc, fnParams, expected ] = testCase;
it(desc, () => {
const result = utils.removeTextFileExtension(...fnParams);
const result = utils.removeFileExtension(...fnParams);
expect(result).toStrictEqual(expected);
});
});

View File

@@ -1,18 +1,19 @@
"use strict";
import chardet from "chardet";
import stripBom from "strip-bom";
import crypto from "crypto";
import { generator } from "rand-token";
import unescape from "unescape";
import escape from "escape-html";
import sanitize from "sanitize-filename";
import mimeTypes from "mime-types";
import path from "path";
import type NoteMeta from "./meta/note_meta.js";
import log from "./log.js";
import { t } from "i18next";
import mimeTypes from "mime-types";
import { release as osRelease } from "os";
import path from "path";
import { generator } from "rand-token";
import sanitize from "sanitize-filename";
import stripBom from "strip-bom";
import unescape from "unescape";
import log from "./log.js";
import type NoteMeta from "./meta/note_meta.js";
const osVersion = osRelease().split('.').map(Number);
@@ -204,7 +205,7 @@ export function formatDownloadTitle(fileName: string, type: string | null, mime:
return `${fileNameBase}${getExtension()}`;
}
export function removeTextFileExtension(filePath: string) {
export function removeFileExtension(filePath: string) {
const extension = path.extname(filePath).toLowerCase();
switch (extension) {
@@ -216,6 +217,7 @@ export function removeTextFileExtension(filePath: string) {
case ".excalidraw":
case ".mermaid":
case ".mmd":
case ".pdf":
return filePath.substring(0, filePath.length - extension.length);
default:
return filePath;
@@ -226,7 +228,7 @@ export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boo
const trimmedNoteMeta = noteMeta?.title?.trim();
if (trimmedNoteMeta) return trimmedNoteMeta;
const basename = path.basename(removeTextFileExtension(filePath));
const basename = path.basename(removeFileExtension(filePath));
return replaceUnderscoresWithSpaces ? basename.replace(/_/g, " ").trim() : basename;
}
@@ -467,28 +469,28 @@ export function normalizeCustomHandlerPattern(pattern: string | null | undefined
// If already ends with slash, create both versions
if (basePattern.endsWith('/')) {
const withoutSlash = basePattern.slice(0, -1) + '$';
const withoutSlash = `${basePattern.slice(0, -1) }$`;
const withSlash = pattern;
return [withoutSlash, withSlash];
} else {
// Add optional trailing slash
const withSlash = basePattern + '/?$';
return [withSlash];
}
// Add optional trailing slash
const withSlash = `${basePattern }/?$`;
return [withSlash];
}
// For patterns without $, add both versions
if (pattern.endsWith('/')) {
const withoutSlash = pattern.slice(0, -1);
return [withoutSlash, pattern];
} else {
const withSlash = pattern + '/';
return [pattern, withSlash];
}
const withSlash = `${pattern }/`;
return [pattern, withSlash];
}
export function formatUtcTime(time: string) {
return time.replace("T", " ").substring(0, 19)
return time.replace("T", " ").substring(0, 19);
}
// TODO: Deduplicate with client utils
@@ -501,9 +503,9 @@ export function formatSize(size: number | null | undefined) {
if (size < 1024) {
return `${size} KiB`;
} else {
return `${Math.round(size / 102.4) / 10} MiB`;
}
return `${Math.round(size / 102.4) / 10} MiB`;
}
function slugify(text: string) {
@@ -544,7 +546,7 @@ export default {
randomSecureToken,
randomString,
removeDiacritic,
removeTextFileExtension,
removeFileExtension,
replaceAll,
safeExtractMessageAndStackFromError,
sanitizeSqlIdentifier,

View File

@@ -16,45 +16,28 @@ import { formatDownloadTitle, isMac, isWindows } from "./utils.js";
// Prevent the window being garbage collected
let mainWindow: BrowserWindow | null;
let setupWindow: BrowserWindow | null;
let allWindows: BrowserWindow[] = []; // // Used to store all windows, sorted by the order of focus.
interface WindowEntry {
window: BrowserWindow;
windowId: string; // custom window ID
}
let allWindowEntries: WindowEntry[] = [];
function trackWindowFocus(win: BrowserWindow, windowId: string) {
function trackWindowFocus(win: BrowserWindow) {
// We need to get the last focused window from allWindows. If the last window is closed, we return the previous window.
// Therefore, we need to push the window into the allWindows array every time it gets focused.
win.on("focus", () => {
allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed() && w.window !== win);
allWindowEntries.push({ window: win, windowId: windowId });
allWindows = allWindows.filter(w => !w.isDestroyed() && w !== win);
allWindows.push(win);
if (!optionService.getOptionBool("disableTray")) {
electron.ipcMain.emit("reload-tray");
}
});
win.on("closed", () => {
cls.wrap(() => {
const savedWindows = optionService.getOptionJson("openNoteContexts") || [];
const win = savedWindows.find(w => w.windowId === windowId);
if (win) {
win.closedAt = Date.now();
}
optionService.setOption("openNoteContexts", JSON.stringify(savedWindows));
})();
allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed());
allWindows = allWindows.filter(w => !w.isDestroyed());
if (!optionService.getOptionBool("disableTray")) {
electron.ipcMain.emit("reload-tray");
}
});
}
async function createExtraWindow(extraWindowId: string, extraWindowHash: string) {
async function createExtraWindow(extraWindowHash: string) {
const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled");
const { BrowserWindow } = await import("electron");
@@ -73,15 +56,15 @@ async function createExtraWindow(extraWindowId: string, extraWindowHash: string)
});
win.setMenuBarVisibility(false);
win.loadURL(`http://127.0.0.1:${port}/?extraWindow=${extraWindowId}${extraWindowHash}`);
win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1${extraWindowHash}`);
configureWebContents(win.webContents, spellcheckEnabled);
trackWindowFocus(win, extraWindowId);
trackWindowFocus(win);
}
electron.ipcMain.on("create-extra-window", (event, arg) => {
createExtraWindow(arg.extraWindowId, arg.extraWindowHash);
createExtraWindow(arg.extraWindowHash);
});
interface PrintOpts {
@@ -185,8 +168,8 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac
return { browserWindow, printReport };
}
async function createMainWindow(app?: App) {
if (app && "setUserTasks" in app) {
async function createMainWindow(app: App) {
if ("setUserTasks" in app) {
app.setUserTasks([
{
program: process.execPath,
@@ -236,7 +219,7 @@ async function createMainWindow(app?: App) {
mainWindow.on("closed", () => (mainWindow = null));
configureWebContents(mainWindow.webContents, spellcheckEnabled);
trackWindowFocus(mainWindow, "main");
trackWindowFocus(mainWindow);
}
function getWindowExtraOpts() {
@@ -398,15 +381,11 @@ function getMainWindow() {
}
function getLastFocusedWindow() {
return allWindowEntries.length > 0 ? allWindowEntries[allWindowEntries.length - 1]?.window : null;
return allWindows.length > 0 ? allWindows[allWindows.length - 1] : null;
}
function getAllWindows() {
return allWindowEntries.map(e => e.window);
}
function getAllWindowIds(): string[] {
return allWindowEntries.map(e => e.windowId);
return allWindows;
}
export default {
@@ -417,6 +396,5 @@ export default {
registerGlobalShortcuts,
getMainWindow,
getLastFocusedWindow,
getAllWindows,
getAllWindowIds
getAllWindows
};

View File

@@ -1,6 +1,6 @@
import { getNoteIcon, NoteType } from "@triliumnext/commons";
import escape from "escape-html";
import { NOTE_TYPE_ICONS } from "../../../becca/entities/bnote.js";
import type { Blob } from "../../../services/blob-interface.js";
import utils from "../../../services/utils.js";
import sql from "../../sql.js";
@@ -19,7 +19,7 @@ const isCredentials = (attr: SAttribute) => attr.type === "label" && attr.name =
class SNote extends AbstractShacaEntity {
noteId: string;
title: string;
type: string;
type: NoteType;
mime: string;
private blobId: string;
utcDateModified: string;
@@ -38,7 +38,7 @@ class SNote extends AbstractShacaEntity {
this.noteId = noteId;
this.title = isProtected ? "[protected]" : title;
this.type = type;
this.type = type as NoteType;
this.mime = mime;
this.blobId = blobId;
this.utcDateModified = utcDateModified; // used for caching of images
@@ -528,33 +528,22 @@ class SNote extends AbstractShacaEntity {
}
getIcon(filterByPrefix: string[] = []) {
return `tn-icon ${this.#getIconInternal(filterByPrefix)}`;
}
#getIconInternal(filterByPrefix: string[] = []) {
const iconClassLabels = this.getLabels("iconClass").filter(label => {
if (filterByPrefix.length === 0) {
return true;
}
return filterByPrefix.some(prefix => label.value.startsWith(prefix));
});
const icon = getNoteIcon({
noteId: this.noteId,
type: this.type,
mime: this.mime,
workspaceIconClass: undefined,
iconClass: iconClassLabels.length > 0 ? iconClassLabels[0].value : undefined,
isFolder: this.isFolder.bind(this)
});
if (iconClassLabels && iconClassLabels.length > 0) {
return iconClassLabels[0].value;
} else if (this.noteId === "root") {
return "bx bx-home-alt-2";
}
if (this.noteId === "_share") {
return "bx bx-share-alt";
} else if (this.type === "text") {
if (this.isFolder()) {
return "bx bx-folder";
}
return "bx bx-note";
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
return "bx bx-data";
}
return NOTE_TYPE_ICONS[this.type];
return `tn-icon ${icon}`;
}
isFolder() {

Some files were not shown because too many files have changed in this diff Show More