Compare commits

..

166 Commits

Author SHA1 Message Date
perfectra1n
280697f2f7 Revert "feat(etapi): resolve suggestions for norms from gemini"
This reverts commit 0650be664d.
2026-01-21 16:37:02 -08:00
perfectra1n
0650be664d feat(etapi): resolve suggestions for norms from gemini 2026-01-21 16:33:42 -08:00
perfectra1n
60c61f553a feat(etapi): put filtering for revisions mainly in the db layer 2026-01-21 16:30:37 -08:00
perfectra1n
022c967781 feat(etapi): add revisions route and "undelete" route to etapi 2026-01-21 16:25:17 -08:00
Elian Doran
227be184ac fix(shortcuts): overlap on azerty due to key matches 2026-01-21 22:34:36 +02:00
Elian Doran
d677f65eeb chore(deps): update dependency happy-dom to v20.3.4 (#8433) 2026-01-21 19:32:41 +02:00
Elian Doran
92f86bcca2 chore(deps): update dependency @smithy/middleware-retry to v4.4.26 (#8442) 2026-01-21 19:31:35 +02:00
Elian Doran
2015068d9e fix(deps): update dependency @zumer/snapdom to v2.0.2 (#8443) 2026-01-21 19:30:35 +02:00
renovate[bot]
8a280c2f9d chore(deps): update dependency happy-dom to v20.3.4 2026-01-21 17:07:19 +00:00
Elian Doran
34fd6f9502 chore(deps): update dependency lightningcss to v1.31.1 (#8444) 2026-01-21 19:05:45 +02:00
Elian Doran
02acb36e47 chore(deps): update dependency node-abi to v4.26.0 (#8445) 2026-01-21 19:03:13 +02:00
Elian Doran
280c0e0348 fix(deps): update dependency @preact/signals to v2.6.0 (#8446) 2026-01-21 18:27:23 +02:00
Elian Doran
8528f0d848 fix(deps): update dependency i18next to v25.8.0 (#8447) 2026-01-21 18:26:53 +02:00
renovate[bot]
96b1efcfdc chore(deps): update dependency lightningcss to v1.31.1 2026-01-21 06:17:33 +00:00
renovate[bot]
794e03b2cb fix(deps): update dependency i18next to v25.8.0 2026-01-21 01:37:06 +00:00
renovate[bot]
a285c46b97 fix(deps): update dependency @preact/signals to v2.6.0 2026-01-21 01:36:23 +00:00
renovate[bot]
4da6294ef2 chore(deps): update dependency node-abi to v4.26.0 2026-01-21 01:35:35 +00:00
renovate[bot]
16ed9a7e8e fix(deps): update dependency @zumer/snapdom to v2.0.2 2026-01-21 01:34:07 +00:00
renovate[bot]
798efbc22f chore(deps): update dependency @smithy/middleware-retry to v4.4.26 2026-01-21 01:33:18 +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
144 changed files with 4435 additions and 3123 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

@@ -27,14 +27,14 @@
"@mermaid-js/layout-elk": "0.2.0",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.5.1",
"@preact/signals": "2.6.0",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"@triliumnext/split.js": "workspace:*",
"@zumer/snapdom": "2.0.1",
"@zumer/snapdom": "2.0.2",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
@@ -44,9 +44,9 @@
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "17.0.0",
"i18next": "25.7.4",
"i18next": "25.8.0",
"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",
"lightningcss": "1.30.2",
"happy-dom": "20.3.4",
"lightningcss": "1.31.1",
"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", () => ({
@@ -61,9 +62,10 @@ describe("shortcuts", () => {
});
describe("keyMatches", () => {
const createKeyboardEvent = (key: string, code?: string) => ({
const createKeyboardEvent = (key: string, code?: string, extraProps: Partial<KeyboardEvent> = {}) => ({
key,
code: code || `Key${key.toUpperCase()}`
code: code || `Key${key.toUpperCase()}`,
...extraProps
} as KeyboardEvent);
it("should match regular letter keys using key code", () => {
@@ -101,17 +103,23 @@ describe("shortcuts", () => {
consoleSpy.mockRestore();
});
it("should match azerty keys", () => {
const event = createKeyboardEvent("A", "KeyQ");
expect(keyMatches(event, "a")).toBe(true);
expect(keyMatches(event, "q")).toBe(false);
});
it("should match letter keys using code when key is a special character (macOS Alt behavior)", () => {
// On macOS, pressing Option/Alt + A produces 'å' as the key, but code is still 'KeyA'
const macOSAltAEvent = createKeyboardEvent("å", "KeyA");
const macOSAltAEvent = createKeyboardEvent("å", "KeyA", { altKey: true });
expect(keyMatches(macOSAltAEvent, "a")).toBe(true);
// Option + H produces '˙'
const macOSAltHEvent = createKeyboardEvent("˙", "KeyH");
const macOSAltHEvent = createKeyboardEvent("˙", "KeyH", { altKey: true });
expect(keyMatches(macOSAltHEvent, "h")).toBe(true);
// Option + S produces 'ß'
const macOSAltSEvent = createKeyboardEvent("ß", "KeyS");
const macOSAltSEvent = createKeyboardEvent("ß", "KeyS", { altKey: true });
expect(keyMatches(macOSAltSEvent, "s")).toBe(true);
});
});
@@ -215,6 +223,15 @@ describe("shortcuts", () => {
consoleSpy.mockRestore();
});
it("matches azerty", () => {
const event = createKeyboardEvent({
key: "a",
code: "KeyQ",
ctrlKey: true
});
expect(matchesShortcut(event, "Ctrl+A")).toBe(true);
});
it("should match Alt+letter shortcuts on macOS where key is a special character", () => {
// On macOS, pressing Option/Alt + A produces 'å' but code remains 'KeyA'
const macOSAltAEvent = createKeyboardEvent({

View File

@@ -215,9 +215,12 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
// For letter keys, use the physical key code for consistency
// On macOS, Option/Alt key produces special characters, so we must use e.code
if (key.length === 1 && key >= 'a' && key <= 'z') {
// e.code is like "KeyA", "KeyB", etc.
const expectedCode = `Key${key.toUpperCase()}`;
return e.code === expectedCode || e.key.toLowerCase() === key.toLowerCase();
if (e.altKey) {
// e.code is like "KeyA", "KeyB", etc.
const expectedCode = `Key${key.toUpperCase()}`;
return e.code === expectedCode || e.key.toLowerCase() === key.toLowerCase();
}
return e.key.toLowerCase() === key.toLowerCase();
}
// For regular keys, check both key and code as fallback

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

@@ -337,6 +337,130 @@ paths:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/notes/{noteId}/revisions:
parameters:
- name: noteId
in: path
required: true
schema:
$ref: "#/components/schemas/EntityId"
get:
description: Returns all revisions for a note identified by its ID
operationId: getNoteRevisions
responses:
"200":
description: list of revisions
content:
application/json; charset=utf-8:
schema:
type: array
items:
$ref: "#/components/schemas/Revision"
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/notes/{noteId}/undelete:
parameters:
- name: noteId
in: path
required: true
schema:
$ref: "#/components/schemas/EntityId"
post:
description: Restore a deleted note. The note must be deleted and must have at least one undeleted parent.
operationId: undeleteNote
responses:
"200":
description: note restored successfully
content:
application/json; charset=utf-8:
schema:
type: object
properties:
success:
type: boolean
example: true
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/notes/history:
get:
description: Returns recent changes including note creations, modifications, and deletions
operationId: getNoteHistory
parameters:
- name: ancestorNoteId
in: query
required: false
description: Limit changes to a subtree identified by this note ID. Defaults to "root" (all notes).
schema:
$ref: "#/components/schemas/EntityId"
responses:
"200":
description: list of recent changes
content:
application/json; charset=utf-8:
schema:
type: array
items:
$ref: "#/components/schemas/RecentChange"
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/revisions/{revisionId}:
parameters:
- name: revisionId
in: path
required: true
schema:
$ref: "#/components/schemas/EntityId"
get:
description: Returns a revision identified by its ID
operationId: getRevisionById
responses:
"200":
description: revision response
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Revision"
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/revisions/{revisionId}/content:
parameters:
- name: revisionId
in: path
required: true
schema:
$ref: "#/components/schemas/EntityId"
get:
description: Returns revision content identified by its ID
operationId: getRevisionContent
responses:
"200":
description: revision content response
content:
text/html:
schema:
type: string
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/branches:
post:
description: >
@@ -1186,3 +1310,93 @@ components:
type: string
description: Human readable error, potentially with more details,
example: Note 'evnnmvHTCgIn' is protected and cannot be modified through ETAPI
Revision:
type: object
description: Revision represents a snapshot of note's title and content at some point in the past.
properties:
revisionId:
$ref: "#/components/schemas/EntityId"
readOnly: true
noteId:
$ref: "#/components/schemas/EntityId"
readOnly: true
type:
type: string
enum:
[
text,
code,
render,
file,
image,
search,
relationMap,
book,
noteMap,
mermaid,
webView,
shortcut,
doc,
contentWidget,
launcher,
]
mime:
type: string
isProtected:
type: boolean
readOnly: true
title:
type: string
blobId:
type: string
description: ID of the blob object which effectively serves as a content hash
dateLastEdited:
$ref: "#/components/schemas/LocalDateTime"
readOnly: true
dateCreated:
$ref: "#/components/schemas/LocalDateTime"
readOnly: true
utcDateLastEdited:
$ref: "#/components/schemas/UtcDateTime"
readOnly: true
utcDateCreated:
$ref: "#/components/schemas/UtcDateTime"
readOnly: true
utcDateModified:
$ref: "#/components/schemas/UtcDateTime"
readOnly: true
contentLength:
type: integer
format: int32
readOnly: true
RecentChange:
type: object
description: Represents a recent change event (creation, modification, or deletion).
properties:
noteId:
$ref: "#/components/schemas/EntityId"
readOnly: true
title:
type: string
description: Title at the time of the change (may be "[protected]" for protected notes)
current_title:
type: string
description: Current title of the note (may be "[protected]" for protected notes)
current_isDeleted:
type: boolean
description: Whether the note is currently deleted
current_deleteId:
type: string
description: Delete ID if the note is deleted
current_isProtected:
type: boolean
description: Whether the note is protected
utcDate:
$ref: "#/components/schemas/UtcDateTime"
description: UTC timestamp of the change
date:
$ref: "#/components/schemas/LocalDateTime"
description: Local timestamp of the change
canBeUndeleted:
type: boolean
description: Whether the note can be undeleted (only present for deleted notes)

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",
@@ -99,7 +99,7 @@
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "25.7.4",
"i18next": "25.8.0",
"i18next-fs-backend": "2.6.1",
"image-type": "6.0.0",
"ini": "6.0.0",
@@ -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

@@ -0,0 +1,77 @@
import { Application } from "express";
import { beforeAll, describe, expect, it } from "vitest";
import supertest from "supertest";
import { createNote, login } from "./utils.js";
import config from "../../src/services/config.js";
let app: Application;
let token: string;
const USER = "etapi";
let createdNoteId: string;
describe("etapi/get-note-revisions", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
const buildApp = (await (import("../../src/app.js"))).default;
app = await buildApp();
token = await login(app);
createdNoteId = await createNote(app, token);
// Create a revision by updating the note content
await supertest(app)
.put(`/etapi/notes/${createdNoteId}/content`)
.auth(USER, token, { "type": "basic" })
.set("Content-Type", "text/plain")
.send("Updated content for revision")
.expect(204);
// Force create a revision
await supertest(app)
.post(`/etapi/notes/${createdNoteId}/revision`)
.auth(USER, token, { "type": "basic" })
.expect(204);
});
it("gets revisions for a note", async () => {
const response = await supertest(app)
.get(`/etapi/notes/${createdNoteId}/revisions`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBeGreaterThan(0);
const revision = response.body[0];
expect(revision).toHaveProperty("revisionId");
expect(revision).toHaveProperty("noteId", createdNoteId);
expect(revision).toHaveProperty("type");
expect(revision).toHaveProperty("mime");
expect(revision).toHaveProperty("title");
expect(revision).toHaveProperty("isProtected");
expect(revision).toHaveProperty("blobId");
expect(revision).toHaveProperty("utcDateCreated");
});
it("returns empty array for note with no revisions", async () => {
// Create a new note without any revisions
const newNoteId = await createNote(app, token, "Brand new content");
const response = await supertest(app)
.get(`/etapi/notes/${newNoteId}/revisions`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
// New notes may or may not have revisions depending on settings
});
it("returns 404 for non-existent note", async () => {
const response = await supertest(app)
.get("/etapi/notes/nonexistentnote/revisions")
.auth(USER, token, { "type": "basic" })
.expect(404);
expect(response.body.code).toStrictEqual("NOTE_NOT_FOUND");
});
});

View File

@@ -0,0 +1,71 @@
import { Application } from "express";
import { beforeAll, describe, expect, it } from "vitest";
import supertest from "supertest";
import { createNote, login } from "./utils.js";
import config from "../../src/services/config.js";
let app: Application;
let token: string;
const USER = "etapi";
let createdNoteId: string;
let revisionId: string;
describe("etapi/get-revision", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
const buildApp = (await (import("../../src/app.js"))).default;
app = await buildApp();
token = await login(app);
createdNoteId = await createNote(app, token, "Initial content");
// Update content to create a revision
await supertest(app)
.put(`/etapi/notes/${createdNoteId}/content`)
.auth(USER, token, { "type": "basic" })
.set("Content-Type", "text/plain")
.send("Updated content")
.expect(204);
// Force create a revision
await supertest(app)
.post(`/etapi/notes/${createdNoteId}/revision`)
.auth(USER, token, { "type": "basic" })
.expect(204);
// Get the revision ID
const revisionsResponse = await supertest(app)
.get(`/etapi/notes/${createdNoteId}/revisions`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(revisionsResponse.body.length).toBeGreaterThan(0);
revisionId = revisionsResponse.body[0].revisionId;
});
it("gets revision metadata by ID", async () => {
const response = await supertest(app)
.get(`/etapi/revisions/${revisionId}`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(response.body).toHaveProperty("revisionId", revisionId);
expect(response.body).toHaveProperty("noteId", createdNoteId);
expect(response.body).toHaveProperty("type", "text");
expect(response.body).toHaveProperty("mime", "text/html");
expect(response.body).toHaveProperty("title", "Hello");
expect(response.body).toHaveProperty("isProtected", false);
expect(response.body).toHaveProperty("blobId");
expect(response.body).toHaveProperty("utcDateCreated");
expect(response.body).toHaveProperty("utcDateModified");
});
it("returns 404 for non-existent revision", async () => {
const response = await supertest(app)
.get("/etapi/revisions/nonexistentrevision")
.auth(USER, token, { "type": "basic" })
.expect(404);
expect(response.body.code).toStrictEqual("REVISION_NOT_FOUND");
});
});

View File

@@ -0,0 +1,94 @@
import { Application } from "express";
import { beforeAll, describe, expect, it } from "vitest";
import supertest from "supertest";
import { createNote, login } from "./utils.js";
import config from "../../src/services/config.js";
let app: Application;
let token: string;
const USER = "etapi";
let createdNoteId: string;
describe("etapi/note-history", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
const buildApp = (await (import("../../src/app.js"))).default;
app = await buildApp();
token = await login(app);
// Create a note to ensure there's some history
createdNoteId = await createNote(app, token, "History test content");
// Create a revision to ensure history has entries
await supertest(app)
.post(`/etapi/notes/${createdNoteId}/revision`)
.auth(USER, token, { "type": "basic" })
.expect(204);
});
it("gets recent changes history", async () => {
const response = await supertest(app)
.get("/etapi/notes/history")
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBeGreaterThan(0);
// Check that history entries have expected properties
const entry = response.body[0];
expect(entry).toHaveProperty("noteId");
expect(entry).toHaveProperty("title");
expect(entry).toHaveProperty("utcDate");
expect(entry).toHaveProperty("date");
expect(entry).toHaveProperty("current_isDeleted");
expect(entry).toHaveProperty("current_isProtected");
});
it("filters history by ancestor note", async () => {
const response = await supertest(app)
.get("/etapi/notes/history?ancestorNoteId=root")
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
// All results should be descendants of root (which is everything)
});
it("returns empty array for non-existent ancestor", async () => {
const response = await supertest(app)
.get("/etapi/notes/history?ancestorNoteId=nonexistentancestor")
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
// Should be empty since no notes are descendants of a non-existent note
expect(response.body.length).toBe(0);
});
it("includes canBeUndeleted for deleted notes", async () => {
// Create and delete a note
const noteToDeleteId = await createNote(app, token, "Note to delete for history test");
await supertest(app)
.delete(`/etapi/notes/${noteToDeleteId}`)
.auth(USER, token, { "type": "basic" })
.expect(204);
// Check history - deleted note should appear with canBeUndeleted property
const response = await supertest(app)
.get("/etapi/notes/history")
.auth(USER, token, { "type": "basic" })
.expect(200);
const deletedEntry = response.body.find(
(entry: any) => entry.noteId === noteToDeleteId && entry.current_isDeleted === true
);
// Deleted entries should have canBeUndeleted property
if (deletedEntry) {
expect(deletedEntry).toHaveProperty("canBeUndeleted");
}
});
});

View File

@@ -0,0 +1,64 @@
import { Application } from "express";
import { beforeAll, describe, expect, it } from "vitest";
import supertest from "supertest";
import { createNote, login } from "./utils.js";
import config from "../../src/services/config.js";
let app: Application;
let token: string;
const USER = "etapi";
let createdNoteId: string;
let revisionId: string;
describe("etapi/revision-content", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
const buildApp = (await (import("../../src/app.js"))).default;
app = await buildApp();
token = await login(app);
createdNoteId = await createNote(app, token, "Initial revision content");
// Update content to ensure we have content in the revision
await supertest(app)
.put(`/etapi/notes/${createdNoteId}/content`)
.auth(USER, token, { "type": "basic" })
.set("Content-Type", "text/plain")
.send("Content after first update")
.expect(204);
// Force create a revision
await supertest(app)
.post(`/etapi/notes/${createdNoteId}/revision`)
.auth(USER, token, { "type": "basic" })
.expect(204);
// Get the revision ID
const revisionsResponse = await supertest(app)
.get(`/etapi/notes/${createdNoteId}/revisions`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(revisionsResponse.body.length).toBeGreaterThan(0);
revisionId = revisionsResponse.body[0].revisionId;
});
it("gets revision content", async () => {
const response = await supertest(app)
.get(`/etapi/revisions/${revisionId}/content`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(response.headers["content-type"]).toMatch(/text\/html/);
expect(response.text).toBeTruthy();
});
it("returns 404 for non-existent revision content", async () => {
const response = await supertest(app)
.get("/etapi/revisions/nonexistentrevision/content")
.auth(USER, token, { "type": "basic" })
.expect(404);
expect(response.body.code).toStrictEqual("REVISION_NOT_FOUND");
});
});

View File

@@ -0,0 +1,103 @@
import { Application } from "express";
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
import supertest from "supertest";
import { login } from "./utils.js";
import config from "../../src/services/config.js";
import { randomInt } from "crypto";
let app: Application;
let token: string;
const USER = "etapi";
describe("etapi/undelete-note", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
const buildApp = (await (import("../../src/app.js"))).default;
app = await buildApp();
token = await login(app);
});
it("undeletes a deleted note", async () => {
// Create a note
const noteId = `testNote${randomInt(10000)}`;
await supertest(app)
.post("/etapi/create-note")
.auth(USER, token, { "type": "basic" })
.send({
"noteId": noteId,
"parentNoteId": "root",
"title": "Note to delete and restore",
"type": "text",
"content": "Content to restore"
})
.expect(201);
// Verify note exists
await supertest(app)
.get(`/etapi/notes/${noteId}`)
.auth(USER, token, { "type": "basic" })
.expect(200);
// Delete the note
await supertest(app)
.delete(`/etapi/notes/${noteId}`)
.auth(USER, token, { "type": "basic" })
.expect(204);
// Verify note is deleted (should return 404)
await supertest(app)
.get(`/etapi/notes/${noteId}`)
.auth(USER, token, { "type": "basic" })
.expect(404);
// Undelete the note
const response = await supertest(app)
.post(`/etapi/notes/${noteId}/undelete`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(response.body).toHaveProperty("success", true);
// Verify note is restored
const restoredResponse = await supertest(app)
.get(`/etapi/notes/${noteId}`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(restoredResponse.body.title).toStrictEqual("Note to delete and restore");
});
it("returns 404 for non-existent note", async () => {
const response = await supertest(app)
.post("/etapi/notes/nonexistentnote/undelete")
.auth(USER, token, { "type": "basic" })
.expect(404);
expect(response.body.code).toStrictEqual("NOTE_NOT_FOUND");
});
it("returns 400 when trying to undelete a non-deleted note", async () => {
// Create a note
const noteId = `testNote${randomInt(10000)}`;
await supertest(app)
.post("/etapi/create-note")
.auth(USER, token, { "type": "basic" })
.send({
"noteId": noteId,
"parentNoteId": "root",
"title": "Note not deleted",
"type": "text",
"content": "Content"
})
.expect(201);
// Try to undelete a note that isn't deleted
const response = await supertest(app)
.post(`/etapi/notes/${noteId}/undelete`)
.auth(USER, token, { "type": "basic" })
.expect(400);
expect(response.body.code).toStrictEqual("NOTE_NOT_DELETED");
});
});

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

@@ -121,6 +121,16 @@ function getAndCheckAttribute(attributeId: string) {
}
}
function getAndCheckRevision(revisionId: string) {
const revision = becca.getRevision(revisionId);
if (revision) {
return revision;
} else {
throw new EtapiError(404, "REVISION_NOT_FOUND", `Revision '${revisionId}' not found.`);
}
}
function validateAndPatch(target: any, source: any, allowedProperties: ValidatorMap) {
for (const key of Object.keys(source)) {
if (!(key in allowedProperties)) {
@@ -152,5 +162,6 @@ export default {
getAndCheckNote,
getAndCheckBranch,
getAndCheckAttribute,
getAndCheckAttachment
getAndCheckAttachment,
getAndCheckRevision
};

View File

@@ -2,6 +2,7 @@ import type BAttachment from "../becca/entities/battachment.js";
import type BAttribute from "../becca/entities/battribute.js";
import type BBranch from "../becca/entities/bbranch.js";
import type BNote from "../becca/entities/bnote.js";
import type BRevision from "../becca/entities/brevision.js";
function mapNoteToPojo(note: BNote) {
return {
@@ -64,9 +65,28 @@ function mapAttachmentToPojo(attachment: BAttachment) {
};
}
function mapRevisionToPojo(revision: BRevision) {
return {
revisionId: revision.revisionId,
noteId: revision.noteId,
type: revision.type,
mime: revision.mime,
isProtected: revision.isProtected,
title: revision.title,
blobId: revision.blobId,
dateLastEdited: revision.dateLastEdited,
dateCreated: revision.dateCreated,
utcDateLastEdited: revision.utcDateLastEdited,
utcDateCreated: revision.utcDateCreated,
utcDateModified: revision.utcDateModified,
contentLength: revision.contentLength
};
}
export default {
mapNoteToPojo,
mapBranchToPojo,
mapAttributeToPojo,
mapAttachmentToPojo
mapAttachmentToPojo,
mapRevisionToPojo
};

View File

@@ -0,0 +1,205 @@
import becca from "../becca/becca.js";
import sql from "../services/sql.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import noteService from "../services/notes.js";
import TaskContext from "../services/task_context.js";
import protectedSessionService from "../services/protected_session.js";
import utils from "../services/utils.js";
import type { Router } from "express";
import type { NoteRow, RecentChangeRow } from "@triliumnext/commons";
function register(router: Router) {
// GET /etapi/notes/history - must be registered before /etapi/notes/:noteId routes
eu.route(router, "get", "/etapi/notes/history", (req, res, next) => {
const ancestorNoteId = (req.query.ancestorNoteId as string) || "root";
let recentChanges: RecentChangeRow[];
if (ancestorNoteId === "root") {
// Optimized path: no ancestor filtering needed, fetch directly from DB
recentChanges = sql.getRows<RecentChangeRow>(`
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
revisions.title,
revisions.utcDateCreated AS utcDate,
revisions.dateCreated AS date
FROM revisions
JOIN notes USING(noteId)
UNION ALL
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateCreated AS utcDate,
notes.dateCreated AS date
FROM notes
UNION ALL
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateModified AS utcDate,
notes.dateModified AS date
FROM notes
WHERE notes.isDeleted = 1
ORDER BY utcDate DESC
LIMIT 500`);
} else {
// Use recursive CTE to find all descendants, then filter at DB level
// This pushes filtering to the database for much better performance
recentChanges = sql.getRows<RecentChangeRow>(`
WITH RECURSIVE descendants(noteId) AS (
SELECT ?
UNION
SELECT branches.noteId
FROM branches
JOIN descendants ON branches.parentNoteId = descendants.noteId
)
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
revisions.title,
revisions.utcDateCreated AS utcDate,
revisions.dateCreated AS date
FROM revisions
JOIN notes USING(noteId)
WHERE notes.noteId IN (SELECT noteId FROM descendants)
UNION ALL
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateCreated AS utcDate,
notes.dateCreated AS date
FROM notes
WHERE notes.noteId IN (SELECT noteId FROM descendants)
UNION ALL
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateModified AS utcDate,
notes.dateModified AS date
FROM notes
WHERE notes.isDeleted = 1 AND notes.noteId IN (SELECT noteId FROM descendants)
ORDER BY utcDate DESC
LIMIT 500`, [ancestorNoteId]);
}
for (const change of recentChanges) {
if (change.current_isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
change.title = protectedSessionService.decryptString(change.title) || "[protected]";
change.current_title = protectedSessionService.decryptString(change.current_title) || "[protected]";
} else {
change.title = change.current_title = "[protected]";
}
}
if (change.current_isDeleted) {
const deleteId = change.current_deleteId;
const undeletedParentBranchIds = noteService.getUndeletedParentBranchIds(change.noteId, deleteId);
// note (and the subtree) can be undeleted if there's at least one undeleted parent (whose branch would be undeleted by this op)
change.canBeUndeleted = undeletedParentBranchIds.length > 0;
}
}
res.json(recentChanges);
});
// GET /etapi/notes/:noteId/revisions - List all revisions for a note
eu.route(router, "get", "/etapi/notes/:noteId/revisions", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const revisions = becca.getRevisionsFromQuery(
`SELECT revisions.*, LENGTH(blobs.content) AS contentLength
FROM revisions
JOIN blobs USING (blobId)
WHERE noteId = ?
ORDER BY utcDateCreated DESC`,
[note.noteId]
);
res.json(revisions.map((revision) => mappers.mapRevisionToPojo(revision)));
});
// POST /etapi/notes/:noteId/undelete - Restore a deleted note
eu.route(router, "post", "/etapi/notes/:noteId/undelete", (req, res, next) => {
const { noteId } = req.params;
const noteRow = sql.getRow<NoteRow | null>("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (!noteRow) {
throw new eu.EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
}
if (!noteRow.isDeleted || !noteRow.deleteId) {
throw new eu.EtapiError(400, "NOTE_NOT_DELETED", `Note '${noteId}' is not deleted.`);
}
const undeletedParentBranchIds = noteService.getUndeletedParentBranchIds(noteId, noteRow.deleteId);
if (undeletedParentBranchIds.length === 0) {
throw new eu.EtapiError(400, "CANNOT_UNDELETE", `Cannot undelete note '${noteId}' - no undeleted parent found.`);
}
const taskContext = new TaskContext("no-progress-reporting", "undeleteNotes", null);
noteService.undeleteNote(noteId, taskContext);
res.json({ success: true });
});
// GET /etapi/revisions/:revisionId - Get revision metadata
eu.route(router, "get", "/etapi/revisions/:revisionId", (req, res, next) => {
const revision = eu.getAndCheckRevision(req.params.revisionId);
if (revision.isProtected) {
throw new eu.EtapiError(400, "REVISION_IS_PROTECTED", `Revision '${req.params.revisionId}' is protected and cannot be read through ETAPI.`);
}
res.json(mappers.mapRevisionToPojo(revision));
});
// GET /etapi/revisions/:revisionId/content - Get revision content
eu.route(router, "get", "/etapi/revisions/:revisionId/content", (req, res, next) => {
const revision = eu.getAndCheckRevision(req.params.revisionId);
if (revision.isProtected) {
throw new eu.EtapiError(400, "REVISION_IS_PROTECTED", `Revision '${req.params.revisionId}' is protected and content cannot be read through ETAPI.`);
}
const filename = utils.formatDownloadTitle(revision.title, revision.type, revision.mime);
res.setHeader("Content-Disposition", utils.getContentDisposition(filename));
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Content-Type", revision.mime);
res.send(revision.getContent());
});
}
export default {
register
};

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

@@ -12,6 +12,7 @@ import etapiMetricsRoute from "../etapi/metrics.js";
import etapiNoteRoutes from "../etapi/notes.js";
import etapiSpecRoute from "../etapi/spec.js";
import etapiSpecialNoteRoutes from "../etapi/special_notes.js";
import etapiRevisionsRoutes from "../etapi/revisions.js";
import auth from "../services/auth.js";
import openID from '../services/open_id.js';
import { isElectron } from "../services/utils.js";
@@ -361,6 +362,8 @@ function register(app: express.Application) {
etapiAttachmentRoutes.register(router);
etapiAttributeRoutes.register(router);
etapiBranchRoutes.register(router);
// Register revisions routes BEFORE notes routes so /etapi/notes/history is matched before /etapi/notes/:noteId
etapiRevisionsRoutes.register(router);
etapiNoteRoutes.register(router);
etapiSpecialNoteRoutes.register(router);
etapiSpecRoute.register(router);

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();

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