Compare commits

..

165 Commits

Author SHA1 Message Date
Elian Doran
aac4774326 Merge remote-tracking branch 'origin/main' into feature/toc_improvements 2026-03-08 12:20:53 +02:00
Elian Doran
ea7aac2030 v0.102.1 (#8961) 2026-03-08 12:13:12 +02:00
Elian Doran
e7f98f08d0 Merge remote-tracking branch 'origin/main' into stable 2026-03-08 12:12:52 +02:00
Elian Doran
c2adc43780 chore(deps): update dependency @types/multer to v2.1.0 (#8921) 2026-03-07 23:17:45 +02:00
Elian Doran
7eaa5352ba Translations update from Hosted Weblate (#8956) 2026-03-07 23:17:15 +02:00
Patric Siesing
17e3e3187b Translated using Weblate (Swedish)
Currently translated at 4.6% (18 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/sv/
2026-03-07 22:15:57 +01:00
Robert Magnusson
2ad7cd3a49 Translated using Weblate (Swedish)
Currently translated at 4.6% (18 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/sv/
2026-03-07 22:15:56 +01:00
Patric Siesing
39aa8d61c2 Translated using Weblate (Swedish)
Currently translated at 11.3% (18 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/sv/
2026-03-07 22:15:55 +01:00
Robert Magnusson
1a3ea977b7 Translated using Weblate (Swedish)
Currently translated at 11.3% (18 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/sv/
2026-03-07 22:15:54 +01:00
Robert Magnusson
4cd8f9a1e6 Translated using Weblate (Swedish)
Currently translated at 1.0% (18 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/sv/
2026-03-07 22:15:53 +01:00
Hosted Weblate
87ce6d1231 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-03-07 22:15:52 +01:00
Elian Doran
8fdbeacf77 fix(deps): update dependency katex to v0.16.37 (#8935) 2026-03-07 23:15:44 +02:00
Elian Doran
f4f775a1c9 chore(deps): update dependency @smithy/middleware-retry to v4.4.40 (#8945) 2026-03-07 23:13:13 +02:00
Elian Doran
fe1154cb2d chore(deps): update dependency @types/sanitize-html to v2.16.1 (#8946) 2026-03-07 23:12:53 +02:00
Elian Doran
638f479ff3 chore(deps): update dependency eslint to v10.0.3 (#8947) 2026-03-07 23:12:27 +02:00
Elian Doran
70436bdb04 fix(deps): update dependency react-i18next to v16.5.6 (#8949) 2026-03-07 23:12:05 +02:00
Elian Doran
575ecaae07 fix(deps): update dependency tabulator-tables to v6.4.0 (#8950) 2026-03-07 23:11:37 +02:00
Elian Doran
d277e6db94 chore(deps): update actions/upload-artifact action to v7 (#8951) 2026-03-07 08:29:42 +02:00
renovate[bot]
25efcd12d0 chore(deps): update actions/upload-artifact action to v7 2026-03-07 02:18:00 +00:00
renovate[bot]
10129321be fix(deps): update dependency tabulator-tables to v6.4.0 2026-03-07 02:17:54 +00:00
renovate[bot]
72710a8f6b chore(deps): update dependency @types/multer to v2.1.0 2026-03-07 02:17:10 +00:00
renovate[bot]
6a7c5c04d8 fix(deps): update dependency react-i18next to v16.5.6 2026-03-07 02:16:21 +00:00
renovate[bot]
5d89591dea chore(deps): update dependency eslint to v10.0.3 2026-03-07 02:14:21 +00:00
renovate[bot]
a88bf5a87b chore(deps): update dependency @types/sanitize-html to v2.16.1 2026-03-07 02:13:18 +00:00
renovate[bot]
bbe5d3506e chore(deps): update dependency @smithy/middleware-retry to v4.4.40 2026-03-07 02:12:12 +00:00
renovate[bot]
c2993d4e7d fix(deps): update dependency katex to v0.16.37 2026-03-06 21:42:06 +00:00
Elian Doran
17ba479182 chore(deps): update dependency @smithy/middleware-retry to v4.4.39 (#8906) 2026-03-06 19:01:41 +02:00
Elian Doran
a465014bbe fix(deps): update codemirror (#8885) 2026-03-06 19:01:13 +02:00
Elian Doran
5dfe253ef6 chore(deps): update imjasonh/setup-crane action to v0.5 (#8910) 2026-03-06 19:00:14 +02:00
Elian Doran
ae7ca6021f Translations update from Hosted Weblate (#8919) 2026-03-06 18:57:49 +02:00
noobhjy
c389697acd Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-03-06 16:50:15 +00:00
Aleksandr Reid
c13c3e0f4a Translated using Weblate (Russian)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2026-03-06 16:50:14 +00:00
Ulices
82c042d045 Translated using Weblate (Spanish)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-03-06 16:50:14 +00:00
Aleksandr Reid
9145ba1690 Translated using Weblate (Russian)
Currently translated at 100.0% (387 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ru/
2026-03-06 16:50:13 +00:00
Marcel
d60653ee17 Translated using Weblate (German)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/de/
2026-03-06 16:50:12 +00:00
Marcel
dae8613b4e Translated using Weblate (German)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-03-06 16:50:12 +00:00
Aindriú Mac Giolla Eoin
2f8e2c40be Translated using Weblate (Irish)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-03-06 16:50:11 +00:00
Francis C.
d85225a0dc Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-03-06 16:50:11 +00:00
green
0cb66df2b2 Translated using Weblate (Japanese)
Currently translated at 100.0% (1676 of 1676 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-03-06 16:50:10 +00:00
Aleksandr Reid
92e0578cb6 Translated using Weblate (Russian)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ru/
2026-03-06 16:50:09 +00:00
Elian Doran
2eee06786e chore(deps): update dependency lint-staged to v16.3.2 (#8908) 2026-03-06 18:50:00 +02:00
Elian Doran
19053dcb3b fix(deps): update dependency mind-elixir to v5.9.2 (#8909) 2026-03-06 18:49:24 +02:00
JYC333
e10c30c59f fix(deps): update dependency i18next to v25.8.14 (#8922) 2026-03-06 14:12:55 +00:00
Elian Doran
c356159664 fix(deps): update dependency marked to v17.0.4 (#8923) 2026-03-06 15:45:12 +02:00
Elian Doran
579be68ca1 chore(deps): update dependency electron to v40.8.0 (#8924) 2026-03-06 15:28:24 +02:00
Elian Doran
a6326a682e chore(deps): update dependency @types/node to v24.12.0 (#8934) 2026-03-06 15:27:19 +02:00
renovate[bot]
4595a3a5dd fix(deps): update dependency i18next to v25.8.14 2026-03-06 12:42:27 +00:00
renovate[bot]
ee21185e64 chore(deps): update dependency electron to v40.8.0 2026-03-06 12:39:17 +00:00
Elian Doran
6d0676c37d chore(deps): update docker/login-action action to v4 (#8925) 2026-03-06 14:38:39 +02:00
Elian Doran
1d4768a581 chore(deps): update docker/setup-qemu-action action to v4 (#8926) 2026-03-06 14:38:14 +02:00
Elian Doran
d086bb7fcb chore(deps): update dependency multer to v2.1.1 [security] (#8929) 2026-03-06 14:37:33 +02:00
Elian Doran
2607c4a32e fix(deps): update dependency react-i18next to v16.5.5 (#8936) 2026-03-06 14:37:12 +02:00
Elian Doran
624333a2ef chore(deps): update dependency express-rate-limit to v8.3.0 (#8937) 2026-03-06 14:36:43 +02:00
Elian Doran
d4acb37f21 chore(deps): update dependency ejs to v5 (#8938) 2026-03-06 14:36:24 +02:00
Elian Doran
6c1a1e9812 chore(deps): update docker/build-push-action action to v7 (#8939) 2026-03-06 13:19:16 +02:00
Elian Doran
9a13641f9b chore(deps): update docker/metadata-action action to v6 (#8940) 2026-03-06 13:18:33 +02:00
renovate[bot]
699e0624c9 chore(deps): update docker/setup-qemu-action action to v4 2026-03-06 06:58:29 +00:00
renovate[bot]
47ceb0d4d2 chore(deps): update docker/metadata-action action to v6 2026-03-06 06:58:27 +00:00
renovate[bot]
15c42f4a09 chore(deps): update docker/login-action action to v4 2026-03-06 06:58:24 +00:00
renovate[bot]
bf8401bb26 chore(deps): update docker/build-push-action action to v7 2026-03-06 06:58:21 +00:00
renovate[bot]
f234433c63 chore(deps): update dependency ejs to v5 2026-03-06 06:58:18 +00:00
renovate[bot]
1b70101123 chore(deps): update imjasonh/setup-crane action to v0.5 2026-03-06 06:57:50 +00:00
renovate[bot]
d610c63c28 chore(deps): update dependency express-rate-limit to v8.3.0 2026-03-06 06:57:17 +00:00
renovate[bot]
5e820a407f chore(deps): update dependency @types/node to v24.12.0 2026-03-06 06:56:18 +00:00
renovate[bot]
62610979b7 fix(deps): update dependency react-i18next to v16.5.5 2026-03-06 06:55:50 +00:00
renovate[bot]
700e99e854 fix(deps): update dependency mind-elixir to v5.9.2 2026-03-06 06:55:19 +00:00
renovate[bot]
7767116b3d fix(deps): update dependency marked to v17.0.4 2026-03-06 06:54:40 +00:00
renovate[bot]
0206e8247b fix(deps): update codemirror 2026-03-06 06:52:48 +00:00
renovate[bot]
5476fe3df9 chore(deps): update dependency lint-staged to v16.3.2 2026-03-06 06:50:46 +00:00
renovate[bot]
d9a4581d37 chore(deps): update dependency @smithy/middleware-retry to v4.4.39 2026-03-06 06:49:46 +00:00
renovate[bot]
8d9c888481 chore(deps): update dependency multer to v2.1.1 [security] 2026-03-06 06:46:38 +00:00
Elian Doran
11e4b672d1 Fix CI test issues (#8932) 2026-03-06 08:43:51 +02:00
Elian Doran
bace3daadc Update apps/server/src/routes/session_parser.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 08:43:31 +02:00
Elian Doran
dee5380e60 fix(ci): sequential tests ended up run in parallel 2026-03-06 08:20:17 +02:00
Elian Doran
bc6a6fd860 Revert "test(server): reset ws module"
This reverts commit 0212398815.
2026-03-05 23:44:24 +02:00
Elian Doran
e928337fe9 test(server): adjust timeout 2026-03-05 23:40:43 +02:00
Elian Doran
432f86ea4b Revert "test(server): switch to forks with 2 max workers"
This reverts commit 4ac22678df.
2026-03-05 23:37:28 +02:00
Elian Doran
5d2daecee0 test(server): switch to forks with 6 max workers 2026-03-05 23:35:15 +02:00
Elian Doran
7c8eb311af test(server): switch to forks with 3 max workers 2026-03-05 23:31:54 +02:00
Elian Doran
4ac22678df test(server): switch to forks with 2 max workers 2026-03-05 23:25:45 +02:00
Elian Doran
5057c02176 test(server): fix errors due to database already existing 2026-03-05 22:52:26 +02:00
Elian Doran
d301e56216 refactor(server): don't set up other timers on module init 2026-03-05 22:19:04 +02:00
Elian Doran
3c22ab8c9c refactor(server): don't set up session timer on module init 2026-03-05 22:17:19 +02:00
Elian Doran
0212398815 test(server): reset ws module 2026-03-05 22:14:34 +02:00
Elian Doran
db0c515bad test(server): fake timers not restored 2026-03-05 22:11:51 +02:00
Elian Doran
9b4f8c5003 feat(ci/client): HTML output 2026-03-05 22:07:11 +02:00
Elian Doran
85d8c4c8fa feat(ci/server): HTML output 2026-03-05 22:06:46 +02:00
Elian Doran
5afab6938a test(server): reduce max workers to 1 2026-03-05 21:54:30 +02:00
Elian Doran
a437169ad5 test(server): increase hook timeout 2026-03-05 21:20:12 +02:00
Elian Doran
f632d3aeb6 Merge remote-tracking branch 'origin/main' into fix/ci 2026-03-05 21:14:57 +02:00
Elian Doran
513fffcb1a ci(dev): escape test filter 2026-03-05 21:14:21 +02:00
Elian Doran
d3337eab9c Merge branch 'main' into feature/toc_improvements 2026-03-05 21:05:17 +02:00
Elian Doran
8128a8192a refactor(ckeditor): address requested changes 2026-03-05 19:28:52 +02:00
Elian Doran
c80bb9657c fix(mindmap): crashing on auto-switch to dark theme 2026-03-05 19:25:07 +02:00
Elian Doran
65514a6fd7 fix(toc): title is extracted before changes are made 2026-03-05 19:08:56 +02:00
Elian Doran
93a7f8c711 fix(toc): not reacting to attribute changes in CKEditor 2026-03-05 19:03:32 +02:00
Elian Doran
0ca179f990 ci(test): quote command 2026-03-05 18:40:24 +02:00
Elian Doran
9d104015f3 ci(test): quote command 2026-03-05 18:30:08 +02:00
Elian Doran
2c4cf2dcf1 ci(test): separate running of heavy tests to avoid OOM issues 2026-03-05 18:28:27 +02:00
Elian Doran
d2e0124962 chore(deps): update dependency fs-extra to v11.3.4 (#8907) 2026-03-05 16:51:11 +02:00
renovate[bot]
cd59c75c04 chore(deps): update dependency fs-extra to v11.3.4 2026-03-04 01:13:39 +00:00
Elian Doran
caa9143591 chore(deps): update dependency happy-dom to v20.8.3 (#8887) 2026-03-03 22:15:58 +02:00
renovate[bot]
7e53810c02 chore(deps): update dependency happy-dom to v20.8.3 2026-03-03 19:42:03 +00:00
Elian Doran
12efa8dc0b chore(deps): update dependency eslint-plugin-playwright to v2.9.0 (#8886) 2026-03-03 21:21:22 +02:00
Elian Doran
4d0ccac7b5 fix(deps): update dependency node-html-parser to v7.1.0 (#8888) 2026-03-03 21:20:21 +02:00
Elian Doran
8b023a55d0 chore(deps): update dependency copy-webpack-plugin to v14 (#8889) 2026-03-03 21:10:22 +02:00
Elian Doran
b4df5fcbd9 chore(deps): update dependency rollup-plugin-webpack-stats to v3 (#8890) 2026-03-03 20:58:24 +02:00
renovate[bot]
6fbe5718e9 chore(deps): update dependency rollup-plugin-webpack-stats to v3 2026-03-03 18:54:56 +00:00
Elian Doran
908bafca63 Translations update from Hosted Weblate (#8901) 2026-03-03 20:54:00 +02:00
Elian Doran
d7313efd67 fix(ci): migrate all the jank docker ci to use crane instead (#8869) 2026-03-03 20:48:42 +02:00
Микола Копитін
a51e15c9b8 Translated using Weblate (Ukrainian)
Currently translated at 90.0% (1508 of 1675 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/uk/
2026-03-03 18:44:20 +00:00
Hosted Weblate
37e9c7d639 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-03-03 18:44:20 +00:00
Elian Doran
2d00ac4dfb Univer Sheets v0 (#8902) 2026-03-03 20:44:06 +02:00
Elian Doran
6aec7eae00 chore(server): increase sync version to avoid data loss due to unsupported note type 2026-03-03 20:25:33 +02:00
Elian Doran
6bfbc2d35e chore(spreadsheet): use better clean up mechanism 2026-03-03 20:12:54 +02:00
Elian Doran
2ffc854ce6 chore(spreadsheet): mark note type as beta 2026-03-03 19:59:12 +02:00
Elian Doran
ddd4a374e4 chore(client): fix some whitespace issues 2026-03-03 19:52:15 +02:00
Elian Doran
0d6e2fc00f chore(client): fix typecheck 2026-03-03 19:41:51 +02:00
Elian Doran
366a8e8726 fix(spreadsheet): persistence hook on every render 2026-03-03 19:24:04 +02:00
Elian Doran
7f0aa0697a fix(spreadsheet): error due to duplicate unit IDs 2026-03-03 19:20:25 +02:00
Elian Doran
d123ce33b8 feat(spreadsheet): restore from JSON 2026-03-03 19:09:33 +02:00
Elian Doran
55588f5962 feat(spreadsheet): restore from JSON 2026-03-03 19:05:01 +02:00
Elian Doran
f32130d5c2 feat(spreadsheet): allow source to be viewed 2026-03-03 19:00:23 +02:00
Elian Doran
03f4ff9e7c feat(spreadsheet): save spreadsheet to JSON 2026-03-03 19:00:14 +02:00
Elian Doran
6de78c7154 refactor(spreadsheet): make use of hooks 2026-03-03 18:48:45 +02:00
Elian Doran
d331e418d4 feat(spreadsheet): support dark mode 2026-03-03 18:42:26 +02:00
Elian Doran
4ace74bcb8 feat(spreadsheet): make full-width 2026-03-03 18:36:17 +02:00
Elian Doran
1d4a336256 feat(spreadsheet): integrate spreadsheet with full-height 2026-03-03 18:34:46 +02:00
Elian Doran
ee6c192ab9 chore(spreadsheet): create new note type 2026-03-03 18:24:55 +02:00
Elian Doran
b220bdce9c fix(note_list): affected by floating images (closes #8899) 2026-03-03 18:14:43 +02:00
Elian Doran
4d86c6c4f1 feat(import/single): trim extension for audio files + default icon 2026-03-03 16:19:44 +02:00
Elian Doran
4fd68bf12d feat(import/single): trim extension for video files 2026-03-03 14:29:18 +02:00
Elian Doran
3ffe34964f feat(notes): add default icon for videos 2026-03-03 14:26:45 +02:00
Elian Doran
faaf26c174 fix(quick_edit): save indicator not shown 2026-03-03 14:19:24 +02:00
Elian Doran
f9c7518db2 fix(spaced_update): triggering events too often while typing 2026-03-03 14:19:24 +02:00
Elian Doran
8357c2a39c chore(pdfjs): version not updated for releases 2026-03-03 14:19:24 +02:00
renovate[bot]
793dcee562 chore(deps): update dependency copy-webpack-plugin to v14 2026-03-03 02:02:49 +00:00
renovate[bot]
00368fc131 fix(deps): update dependency node-html-parser to v7.1.0 2026-03-03 02:01:49 +00:00
renovate[bot]
f81b686f41 chore(deps): update dependency eslint-plugin-playwright to v2.9.0 2026-03-03 02:00:00 +00:00
Elian Doran
4c5aada5d3 chore(deps): update dependency @types/express-serve-static-core to v5.1.1 (#8346) 2026-03-02 22:42:10 +02:00
Elian Doran
05551cec9e chore(deps): update dependency sax to v1.5.0 (#8875) 2026-03-02 22:41:20 +02:00
Elian Doran
6300a8c8d1 chore(deps): update dependency @redocly/cli to v2.20.2 (#8853) 2026-03-02 22:34:22 +02:00
Elian Doran
ca4d15727d Merge branch 'main' into renovate/express-serve-static-core-5.x 2026-03-02 22:30:43 +02:00
renovate[bot]
2fe076086e chore(deps): update dependency sax to v1.5.0 2026-03-02 20:25:56 +00:00
Elian Doran
56b65ddfae Translations update from Hosted Weblate (#8870) 2026-03-02 22:22:37 +02:00
Hasan Kara
fcf6673825 Translated using Weblate (Turkish)
Currently translated at 16.3% (19 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/tr/
2026-03-02 20:54:15 +01:00
Hasan Kara
9eda264f52 Translated using Weblate (Turkish)
Currently translated at 5.1% (20 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/tr/
2026-03-02 20:54:14 +01:00
Hasan Kara
fe1270c679 Translated using Weblate (Turkish)
Currently translated at 4.2% (71 of 1675 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/tr/
2026-03-02 20:54:13 +01:00
Hasan Kara
679e1ac678 Translated using Weblate (Turkish)
Currently translated at 12.0% (19 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/tr/
2026-03-02 20:54:12 +01:00
ibs-allaow
e309ff2d17 Translated using Weblate (Arabic)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ar/
2026-03-02 20:54:12 +01:00
Francis C.
c910335155 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1675 of 1675 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-03-02 20:54:11 +01:00
Yatrik Patel
5606cde506 Translated using Weblate (Hindi)
Currently translated at 100.0% (1675 of 1675 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-03-02 20:54:10 +01:00
Yatrik Patel
0e2f4f4e13 Translated using Weblate (Hindi)
Currently translated at 38.6% (61 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-03-02 20:54:09 +01:00
renovate[bot]
1f6c6f2acd chore(deps): update dependency @redocly/cli to v2.20.2 2026-03-02 18:09:58 +00:00
Elian Doran
37d2e9f14b fix(deps): update dependency globals to v17.4.0 (#8876) 2026-03-02 16:44:44 +02:00
Adorian Doran
0fcf30a3b8 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-03-02 11:22:08 +02:00
Adorian Doran
8712e7dd16 style/pdf viewer: fix some layout issues in toolbar 2026-03-02 11:21:47 +02:00
renovate[bot]
2ee4e9cc14 fix(deps): update dependency globals to v17.4.0 2026-03-02 01:18:53 +00:00
perfectra1n
b257b75be2 fix(ci): remove fragile jq where possible 2026-03-01 13:49:45 -08:00
perfectra1n
2de2709420 fix(ci): migrate all the jank docker ci to use crane instead 2026-03-01 13:47:18 -08:00
Elian Doran
34ca7912fc Merge remote-tracking branch 'origin/main' into renovate/express-serve-static-core-5.x 2026-02-28 19:11:57 +02:00
Elian Doran
dc3de5bf36 chore(server): address requested changes 2026-02-27 00:05:54 +02:00
Elian Doran
1041bf70e1 test(express-partial-content): fix type errors 2026-02-26 21:11:22 +02:00
Elian Doran
0c6326b678 refactor(server): use strong typing for routes 2026-02-26 21:08:54 +02:00
renovate[bot]
fd805a5279 chore(deps): update dependency @types/express-serve-static-core to v5.1.1 2026-02-26 18:18:55 +00:00
108 changed files with 5421 additions and 1988 deletions

View File

@@ -37,8 +37,35 @@ jobs:
- name: Typecheck
run: pnpm typecheck
- name: Run the unit tests
run: pnpm run test:all
- name: Run the client-side tests
run: pnpm run --filter=client test
- name: Upload client test report
uses: actions/upload-artifact@v7
if: always()
with:
name: client-test-report
path: apps/client/test-output/vitest/html/
retention-days: 30
- name: Run the server-side tests
run: pnpm run --filter=server test
- name: Upload server test report
uses: actions/upload-artifact@v7
if: always()
with:
name: server-test-report
path: apps/server/test-output/vitest/html/
retention-days: 30
- name: Run CKEditor e2e tests
run: |
pnpm run --filter=ckeditor5-mermaid test
pnpm run --filter=ckeditor5-math test
- name: Run the rest of the tests
run: pnpm run --filter=\!client --filter=\!server --filter=\!ckeditor5-mermaid --filter=\!ckeditor5-math test
build_docker:
name: Build Docker image
@@ -63,7 +90,7 @@ jobs:
- name: Trigger server build
run: pnpm run server:build
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
- uses: docker/build-push-action@v7
with:
context: apps/server
cache-from: type=gha
@@ -100,7 +127,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Build and export to Docker
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: apps/server
file: apps/server/${{ matrix.dockerfile }}

View File

@@ -59,7 +59,7 @@ jobs:
run: pnpm run server:build
- name: Build and export to Docker
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: apps/server
file: apps/server/${{ matrix.dockerfile }}
@@ -164,7 +164,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
@@ -175,13 +175,13 @@ jobs:
latest=false
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
@@ -189,7 +189,7 @@ jobs:
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: apps/server
file: apps/server/${{ matrix.dockerfile }}
@@ -229,17 +229,17 @@ jobs:
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
- name: Set up crane
uses: imjasonh/setup-crane@v0.4
uses: imjasonh/setup-crane@v0.5
- name: Login to GHCR
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -247,7 +247,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |

View File

@@ -16,9 +16,9 @@
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.30.3",
"devDependencies": {
"@redocly/cli": "2.19.2",
"@redocly/cli": "2.20.2",
"archiver": "7.0.1",
"fs-extra": "11.3.3",
"fs-extra": "11.3.4",
"js-yaml": "4.1.1",
"react": "19.2.4",
"react-dom": "19.2.4",

View File

@@ -35,6 +35,8 @@
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"@triliumnext/split.js": "workspace:*",
"@univerjs/preset-sheets-core": "0.16.1",
"@univerjs/presets": "0.16.1",
"@zumer/snapdom": "2.0.2",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
@@ -44,29 +46,29 @@
"debounce": "3.0.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.1",
"globals": "17.3.0",
"i18next": "25.8.13",
"globals": "17.4.0",
"i18next": "25.8.14",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.33",
"katex": "0.16.37",
"knockout": "3.5.1",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "17.0.3",
"marked": "17.0.4",
"mermaid": "11.12.3",
"mind-elixir": "5.9.1",
"mind-elixir": "5.9.2",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.4",
"react-i18next": "16.5.4",
"react-i18next": "16.5.6",
"react-window": "2.2.7",
"reveal.js": "5.2.1",
"rrule": "2.8.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
"tabulator-tables": "6.4.0",
"vanilla-js-wheel-zoom": "9.0.4"
},
"devDependencies": {
@@ -79,8 +81,8 @@
"@types/mark.js": "8.11.12",
"@types/reveal.js": "5.2.2",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.7.0",
"copy-webpack-plugin": "14.0.0",
"happy-dom": "20.8.3",
"lightningcss": "1.31.1",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.2.0"

View File

@@ -18,7 +18,7 @@ const RELATION = "relation";
* end user. Those types should be used only for checking against, they are
* not for direct use.
*/
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap";
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "spreadsheet";
export interface NotePathRecord {
isArchived: boolean;

View File

@@ -1,4 +1,5 @@
import { NoteType } from "@triliumnext/commons";
import FNote from "../entities/fnote";
import { ViewTypeOptions } from "../widgets/collections/interface";
@@ -17,7 +18,8 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
render: null,
search: null,
text: null,
webView: null
webView: null,
spreadsheet: null
};
export const byBookType: Record<ViewTypeOptions, string | null> = {
@@ -38,6 +40,6 @@ export function getHelpUrlForNote(note: FNote | null | undefined) {
} else if (note?.hasLabel("textSnippet")) {
return "pwc194wlRzcH";
} else if (note && note.type === "book") {
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""];
}
}

View File

@@ -1,9 +1,9 @@
import { t } from "./i18n.js";
import froca from "./froca.js";
import server from "./server.js";
import type { MenuCommandItem, MenuItem, MenuItemBadge, MenuSeparatorItem } from "../menus/context_menu.js";
import type { NoteType } from "../entities/fnote.js";
import type { MenuCommandItem, MenuItem, MenuItemBadge, MenuSeparatorItem } from "../menus/context_menu.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
import server from "./server.js";
export interface NoteTypeMapping {
type: NoteType;
@@ -26,6 +26,7 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
// The default note type (always the first item)
{ type: "text", mime: "text/html", title: t("note_types.text"), icon: "bx-note" },
{ type: "spreadsheet", mime: "application/json", title: t("note_types.spreadsheet"), icon: "bx-table", isBeta: true },
// Text notes group
{ type: "book", mime: "", title: t("note_types.book"), icon: "bx-book" },
@@ -96,9 +97,9 @@ function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandName
title: nt.title,
command,
type: nt.type,
uiIcon: "bx " + nt.icon,
uiIcon: `bx ${nt.icon}`,
badges: []
}
};
if (nt.isNew) {
menuItem.badges?.push(NEW_BADGE);
@@ -130,7 +131,7 @@ async function getUserTemplates(command?: TreeCommandNames) {
const item: MenuItem<TreeCommandNames> = {
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
command,
type: templateNote.type,
templateNoteId: templateNote.noteId
};
@@ -159,7 +160,7 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
const items: MenuItem<TreeCommandNames>[] = [];
if (title) {
items.push({
title: title,
title,
kind: "header"
});
} else {
@@ -175,7 +176,7 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
const item: MenuItem<TreeCommandNames> = {
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
command,
type: templateNote.type,
templateNoteId: templateNote.noteId
};
@@ -193,7 +194,7 @@ async function isNewTemplate(templateNoteId) {
if (rootCreationDate === undefined) {
// Retrieve the root note creation date
try {
let rootNoteInfo: any = await server.get("notes/root");
const rootNoteInfo: any = await server.get("notes/root");
if ("dateCreated" in rootNoteInfo) {
rootCreationDate = new Date(rootNoteInfo.dateCreated);
}
@@ -208,7 +209,7 @@ async function isNewTemplate(templateNoteId) {
if (creationDate === undefined) {
// The creation date isn't available in the cache, try to retrieve it from the server
try {
const noteInfo: any = await server.get("notes/" + templateNoteId);
const noteInfo: any = await server.get(`notes/${templateNoteId}`);
if ("dateCreated" in noteInfo) {
creationDate = new Date(noteInfo.dateCreated);
creationDateCache.set(templateNoteId, creationDate);
@@ -230,9 +231,8 @@ async function isNewTemplate(templateNoteId) {
const age = (new Date().getTime() - creationDate.getTime()) / DAY_LENGTH;
// Return true if the template is at most NEW_TEMPLATE_MAX_AGE days old
return (age <= NEW_TEMPLATE_MAX_AGE);
} else {
return false;
}
return false;
}
export default {

View File

@@ -12,6 +12,7 @@ export default class SpacedUpdate {
private updateInterval: number;
private changeForbidden?: boolean;
private stateCallback?: StateCallback;
private lastState: SaveState = "saved";
constructor(updater: Callback, updateInterval = 1000, stateCallback?: StateCallback) {
this.updater = updater;
@@ -24,7 +25,7 @@ export default class SpacedUpdate {
scheduleUpdate() {
if (!this.changeForbidden) {
this.changed = true;
this.stateCallback?.("unsaved");
this.onStateChanged("unsaved");
setTimeout(() => this.triggerUpdate());
}
}
@@ -34,12 +35,12 @@ export default class SpacedUpdate {
this.changed = false; // optimistic...
try {
this.stateCallback?.("saving");
this.onStateChanged("saving");
await this.updater();
this.stateCallback?.("saved");
this.onStateChanged("saved");
} catch (e) {
this.changed = true;
this.stateCallback?.("error");
this.onStateChanged("error");
logError(getErrorMessage(e));
throw e;
}
@@ -76,13 +77,13 @@ export default class SpacedUpdate {
}
if (Date.now() - this.lastUpdated > this.updateInterval) {
this.stateCallback?.("saving");
this.onStateChanged("saving");
try {
await this.updater();
this.stateCallback?.("saved");
this.onStateChanged("saved");
this.changed = false;
} catch (e) {
this.stateCallback?.("error");
this.onStateChanged("error");
logError(getErrorMessage(e));
}
this.lastUpdated = Date.now();
@@ -92,6 +93,13 @@ export default class SpacedUpdate {
}
}
onStateChanged(state: SaveState) {
if (state === this.lastState) return;
this.stateCallback?.(state);
this.lastState = state;
}
async allowUpdateWithoutChange(callback: Callback) {
this.changeForbidden = true;

View File

@@ -1535,7 +1535,8 @@
"new-feature": "新建",
"collections": "集合",
"book": "集合",
"ai-chat": "AI聊天"
"ai-chat": "AI聊天",
"spreadsheet": "电子表格"
},
"protect_note": {
"toggle-on": "保护笔记",

View File

@@ -1488,20 +1488,21 @@
"mermaid-diagram": "Mermaid Diagramm",
"canvas": "Leinwand",
"web-view": "Webansicht",
"mind-map": "Mind Map",
"mind-map": "Mindmap",
"file": "Datei",
"image": "Bild",
"launcher": "Starter",
"doc": "Dokument",
"widget": "Widget",
"confirm-change": "Es is nicht empfehlenswert den Notiz-Typ zu ändern, wenn der Inhalt der Notiz nicht leer ist. Möchtest du dennoch fortfahren?",
"confirm-change": "Es ist nicht empfehlenswert den Notiz-Typ zu ändern, wenn der Inhalt der Notiz nicht leer ist. Möchtest du dennoch fortfahren?",
"geo-map": "Geo-Karte",
"beta-feature": "Beta",
"book": "Sammlung",
"ai-chat": "KI Chat",
"ai-chat": "KI-Chat",
"task-list": "Aufgabenliste",
"new-feature": "Neu",
"collections": "Sammlungen"
"collections": "Sammlungen",
"spreadsheet": "Tabelle"
},
"protect_note": {
"toggle-on": "Notiz schützen",

View File

@@ -1582,7 +1582,8 @@
"ai-chat": "AI Chat",
"task-list": "Task List",
"new-feature": "New",
"collections": "Collections"
"collections": "Collections",
"spreadsheet": "Spreadsheet"
},
"protect_note": {
"toggle-on": "Protect the note",

View File

@@ -1548,7 +1548,8 @@
"task-list": "Lista de tareas",
"book": "Colección",
"new-feature": "Nuevo",
"collections": "Colecciones"
"collections": "Colecciones",
"spreadsheet": "Hoja de cálculo"
},
"protect_note": {
"toggle-on": "Proteger la nota",
@@ -1650,7 +1651,8 @@
},
"search_result": {
"no_notes_found": "No se han encontrado notas para los parámetros de búsqueda dados.",
"search_not_executed": "La búsqueda aún no se ha ejecutado. Dé clic en el botón «Buscar» para ver los resultados."
"search_not_executed": "La búsqueda aún no se ha ejecutado.",
"search_now": "Buscar ahora"
},
"spacer": {
"configure_launchbar": "Configurar barra de lanzamiento"

View File

@@ -1571,7 +1571,8 @@
"ai-chat": "Comhrá AI",
"task-list": "Liosta Tascanna",
"new-feature": "Nua",
"collections": "Bailiúcháin"
"collections": "Bailiúcháin",
"spreadsheet": "Scarbhileog"
},
"protect_note": {
"toggle-on": "Cosain an nóta",

View File

@@ -51,7 +51,7 @@
},
"add_link": {
"note": "नोट",
"add_link": "लिंक जोड़ें",
"add_link": "लिंक ऐड करें",
"help_on_links": "लिंक्स पर मदद।",
"search_note": "नोट को नाम से खोजें",
"link_title_mirrors": "लिंक टाइटल नोट के करंट टाइटल के हिसाब से बदलता है",
@@ -112,7 +112,7 @@
"help_on_tree_prefix": "ट्री प्रीफ़िक्स पर मदद",
"prefix": "प्रीफ़िक्स: ",
"save": "सेव करें",
"branch_prefix_saved": "ब्रांच प्रीिक्स सेव कर दिया गया है।",
"branch_prefix_saved": "ब्रांच प्रीफ़िक्स सेव हो चुका है।",
"branch_prefix_saved_multiple": "{{count}} ब्रांचेस के लिए ब्रांच प्रीफ़िक्स सेव कर दिया गया है।",
"affected_branches": "प्रभावित ब्रांचेस ({{count}}):"
},

View File

@@ -600,7 +600,8 @@
"task-list": "タスクリスト",
"new-feature": "New",
"collections": "コレクション",
"ai-chat": "AI チャット"
"ai-chat": "AI チャット",
"spreadsheet": "スプレッドシート"
},
"edited_notes": {
"no_edited_notes_found": "この日の編集されたノートはまだありません...",

View File

@@ -257,7 +257,7 @@
"collapseExpand": "свернуть/развернуть узел",
"notSet": "не установлено",
"goBackForwards": "назад / вперед в истории",
"showJumpToNoteDialog": "показать <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">окно \"Перейти к\"</a>",
"showJumpToNoteDialog": "Перейти к <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"Перейти к\" окно</a>",
"scrollToActiveNote": "прокрутка к активной заметке",
"jumpToParentNote": "переход к родительской заметке",
"collapseWholeTree": "свернуть все дерево заметок",
@@ -471,7 +471,7 @@
"calendar_root": "отмечает заметку, которая должна использоваться в качестве корневой для заметок дня. Только одна должна быть отмечена как таковая.",
"archived": "заметки с этой меткой не будут отображаться в результатах поиска по умолчанию (а также в диалоговых окнах «Перейти к», «Добавить ссылку» и т. д.).",
"exclude_from_export": "заметки (с их поддеревьями) не будут включены ни в один экспорт заметок",
"run": "определяет, при каких событиях должен запускаться скрипт. Возможные значения:\n<ul>\n<li>frontendStartup — при запуске (или обновлении) фронтенда Trilium, но не на мобильном устройстве.</li>\n<li>mobileStartup — при запуске (или обновлении) фронтенда Trilium на мобильном устройстве.</li>\n<li>backendStartup — при запуске бэкенда Trilium.</li>\n<li>hourly — запускать каждый час. Для указания времени можно использовать дополнительную метку <code>runAtHour</code>.</li>\n<li>daily — запускать раз в день.</li>\n</ul>",
"run": "определяет, при каких событиях должен запускаться скрипт. Возможные значения:<ul>\n<li>frontendStartup — при запуске (или обновлении) фронтенда Trilium, но не на мобильном устройстве.</li>\n<li>mobileStartup — при запуске (или обновлении) фронтенда Trilium на мобильном устройстве.</li>\n<li>backendStartup — при запуске бэкенда Trilium.</li>\n<li>hourly — запускать каждый час. Для указания времени можно использовать дополнительную метку <code>runAtHour</code>.</li>\n<li>daily — запускать раз в день.</li></ul>",
"run_on_instance": "Определить, на каком экземпляре Trilium это должно выполняться. По умолчанию — для всех экземпляров.",
"run_at_hour": "В какой час это должно выполняться? Следует использовать вместе с <code>#run=hourly</code>. Можно задать несколько раз для большего количества запусков в течение дня.",
"disable_inclusion": "скрипты с этой меткой не будут включены в выполнение родительского скрипта.",
@@ -594,7 +594,8 @@
"display-week-numbers": "Отображать номера недель",
"hide-weekends": "Скрыть выходные",
"raster": "Растр",
"show-scale": "Показать масштаб"
"show-scale": "Показать масштаб",
"show-labels": "Показать названия маркеров"
},
"editorfeatures": {
"note_completion_enabled": "Включить автодополнение",
@@ -782,7 +783,13 @@
"shared-indicator-tooltip": "Эта заметка опубликована",
"shared-indicator-tooltip-with-url": "Эта заметка доступно публично по адресу: {{- url}}",
"subtree-hidden-moved-description-other": "В дереве, к которому относится эта заметка, скрыты дочерние заметки.",
"subtree-hidden-moved-description-collection": "Эта коллекция скрывает свои дочерние заметки в дереве."
"subtree-hidden-moved-description-collection": "Эта коллекция скрывает свои дочерние заметки в дереве.",
"clone-indicator-tooltip": "У этой заметки {{- count}} родителей: {{- parents}}",
"clone-indicator-tooltip-single": "Эта заметка клонирована (1 дополнительный родитель: {{- parent}})",
"subtree-hidden-moved-title": "Добавлено в {{title}}",
"subtree-hidden-tooltip_one": "{{count}} дочерняя заметка скрыта",
"subtree-hidden-tooltip_few": "Скрыто {{count}} дочерних заметок",
"subtree-hidden-tooltip_many": "Скрыто {{count}} дочерних заметок"
},
"quick-search": {
"no-results": "Результаты не найдены",
@@ -826,7 +833,9 @@
"mind-map": "Mind Map",
"geo-map": "Географическая карта",
"task-list": "Список задач",
"confirm-change": "Не рекомендуется менять тип заметки, если её содержимое не пустое. Вы всё равно хотите продолжить?"
"confirm-change": "Не рекомендуется менять тип заметки, если её содержимое не пустое. Вы всё равно хотите продолжить?",
"ai-chat": "Чат с ИИ",
"spreadsheet": "Электронная таблица"
},
"tree-context-menu": {
"open-in-popup": "Быстрое редактирование",
@@ -1153,7 +1162,8 @@
"search_note_saved": "Заметка с настройкой поиска сохранена в {{- notePathTitle}}",
"unknown_search_option": "Неизвестный параметр поиска {{searchOptionName}}",
"actions_executed": "Действия выполнены.",
"view_options": "Просмотреть опции:"
"view_options": "Просмотреть опции:",
"option": "опция"
},
"ancestor": {
"depth_label": "глубина",
@@ -1403,7 +1413,8 @@
"type_text_to_filter": "Введите текст для фильтрации сочетаний клавиш...",
"reload_app": "Перезагрузить приложение, чтобы применить изменения",
"confirm_reset": "Вы действительно хотите сбросить все сочетания клавиш до значений по умолчанию?",
"set_all_to_default": "Установить все сочетания клавиш по умолчанию"
"set_all_to_default": "Установить все сочетания клавиш по умолчанию",
"no_results": "Не найдено ярлыков, соответствующих '{{filter}}'"
},
"sync_2": {
"timeout_unit": "миллисекунд",
@@ -1713,7 +1724,8 @@
"delete_this_note": "Удалить эту заметку",
"insert_child_note": "Вставить дочернюю заметку",
"note_revisions": "История изменений",
"content_language_switcher": "Язык содержимого: {{language}}"
"content_language_switcher": "Язык содержимого: {{language}}",
"backlinks": "Ссылки"
},
"svg_export_button": {
"button_title": "Экспортировать диаграмму как SVG"
@@ -1790,7 +1802,8 @@
},
"search_result": {
"no_notes_found": "По заданным параметрам поиска заметки не найдены.",
"search_not_executed": "Поиск ещё не выполнен. Нажмите кнопку «Поиск» выше, чтобы увидеть результаты."
"search_not_executed": "Поиск ещё не выполнен.",
"search_now": "Искать сейчас"
},
"empty": {
"search_placeholder": "поиск заметки по ее названию",
@@ -1988,10 +2001,12 @@
"print_report_collection_content_few": "{{count}} заметки в коллекции не удалось распечатать, поскольку они не поддерживаются или защищены.",
"print_report_collection_content_many": "{{count}} заметок в коллекции не удалось распечатать, поскольку они не поддерживаются или защищены.",
"print_report_collection_details_button": "Подробнее",
"print_report_collection_details_ignored_notes": "Пропущенные заметки"
"print_report_collection_details_ignored_notes": "Пропущенные заметки",
"print_report_error_title": "Не удалось напечатать",
"print_report_stack_trace": "Трассировка стека"
},
"book": {
"no_children_help": "В этой коллекции нет дочерних заметок, поэтому отображать нечего. Подробности см. в <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a>.",
"no_children_help": "В этой коллекции нет дочерних заметок, поэтому отображать нечего.",
"drag_locked_title": "Защищено от изменения",
"drag_locked_message": "Перетаскивание не допускается, так как коллекция защищена от редактирования."
},
@@ -2007,7 +2022,9 @@
"rendering_error": "Невозможно отобразить содержимое из-за ошибки."
},
"pagination": {
"total_notes": "{{count}} заметок"
"total_notes": "{{count}} заметок",
"prev_page": "Предыдущая страница",
"next_page": "Следующая страница"
},
"status_bar": {
"attributes_one": "{{count}} атрибут",
@@ -2137,5 +2154,49 @@
},
"platform_indicator": {
"available_on": "Доступно для {{platform}}"
},
"render": {
"setup_title": "Отобразить настраиваемый HTML или Preact JSX в этой заметке",
"setup_create_sample_preact": "Создать образец заметки с помощью Preact",
"setup_create_sample_html": "Создать образец заметки с помощью HTML",
"setup_sample_created": "Образец заметки был создан в качестве дочерней записи.",
"disabled_description": "Эти заметки для рендера поступают из внешнего источника. Чтобы защитить вас от вредоносного содержимого, они не включены по умолчанию. Убедитесь, что вы доверяете источнику до его включения.",
"disabled_button_enable": "Включить заметки для рендера"
},
"web_view_setup": {
"title": "Создайте живой просмотр веб-страницы прямо в Trilium",
"url_placeholder": "Введите или вставьте адрес сайта, например https://triliumnotes.org",
"create_button": "Создать веб-просмотр",
"invalid_url_title": "Неверный адрес",
"invalid_url_message": "Введите корректный веб-адрес, например https://triliumnotes.org.",
"disabled_description": "Этот веб-просмотр был импортирован из внешнего источника. Чтобы защитить вас от фишинга или вредоносного контента, он не загружается автоматически. Вы можете включить его, если доверяете источнику.",
"disabled_button_enable": "Включить просмотр веб-страниц"
},
"active_content_badges": {
"type_icon_pack": "Набор иконок",
"type_backend_script": "Бэкенд скрипт",
"type_frontend_script": "Фронтенд скрипт",
"type_widget": "Виджет",
"type_app_css": "Пользовательский CSS",
"type_render_note": "Заметка для рендера",
"type_web_view": "Просмотр веб-страницы",
"type_app_theme": "Пользовательская тема",
"toggle_tooltip_enable_tooltip": "Нажмите, чтобы включить этот {{type}}.",
"toggle_tooltip_disable_tooltip": "Нажмите, чтобы выключить этот {{type}}.",
"menu_docs": "Открытая документация",
"menu_execute_now": "Выполнить скрипт сейчас",
"menu_run": "Выполнять автоматически",
"menu_run_disabled": "Вручную",
"menu_run_backend_startup": "При запуске бэкенда",
"menu_run_hourly": "Ежечасно",
"menu_run_daily": "Ежедневно",
"menu_run_frontend_startup": "Когда запускается интерфейс ПК",
"menu_run_mobile_startup": "При запуске мобильного интерфейса",
"menu_change_to_widget": "Изменить виджет",
"menu_change_to_frontend_script": "Перейти к фронтенд скрипту",
"menu_theme_base": "Базовая тема"
},
"setup_form": {
"more_info": "Узнать больше"
}
}

View File

@@ -3,6 +3,32 @@
"title": "Om Trilium Notes",
"homepage": "Hemsida:",
"app_version": "App version:",
"db_version": "DB version:"
"db_version": "DB version:",
"sync_version": "Sync version:",
"build_date": "Bygg datum:",
"build_revision": "Bygg version:",
"data_directory": "Data sökväg:"
},
"toast": {
"critical-error": {
"title": "Kritiskt fel",
"message": "Ett kritiskt fel har inträffat som förhindrar klientprogrammet från att starta:\n\n{{message}}\n\nDetta beror troligen på att ett skript har misslyckats på ett oväntat sätt. Försök att starta programmet i felsäkert läge och åtgärda problemet."
},
"widget-error": {
"title": "Misslyckades att starta widget",
"message-custom": "Anpassad widget från anteckning med ID \"{{id}}\", med rubrik \"{{title}}\" kunde inte startas på grund av:\n\n{{message}}",
"message-unknown": "Okänd widget kunde inte startas på grund av:\n\n{{message}}"
},
"bundle-error": {
"title": "Misslyckades att starta ett anpassat skript",
"message": "Skript kunde inte startas på grund av:\n\n{{message}}"
},
"widget-list-error": {
"title": "Misslyckades att hämta widget-listan från servern"
},
"widget-render-error": {
"title": "Misslyckades att renderera en anpassad React-widget"
},
"widget-missing-parent": "Anpassad widget saknar '{{property}}', som måste vara definierad.\n\nOm skriptet är avsett att köras utan gränssnitt, använd '#run-frontendStartup' istället."
}
}

View File

@@ -50,8 +50,15 @@
},
"bundle-error": {
"title": "Özel bir betik yüklenemedi",
"message": "ID'si \"{{id}}\" ve başlığı \"{{title}}\" olan nottan alınan komut dosyası şunun nedeniyle yürütülemedi:\n\n{{message}}"
}
"message": "Komut şu nedenle yürütülemedi:\n\n{{message}}"
},
"widget-list-error": {
"title": "Sunucudan widget listesi alınamadı"
},
"widget-render-error": {
"title": "Özel React widget'ı çizilirken sorun yaşandı"
},
"scripting-error": "Kullanıcı tanımlı betik hatası: {{title}}"
},
"add_link": {
"add_link": "Bağlantı ekle",

View File

@@ -1495,7 +1495,9 @@
"beta-feature": "Beta",
"task-list": "任務列表",
"new-feature": "新增",
"collections": "集合"
"collections": "集合",
"ai-chat": "AI 聊天",
"spreadsheet": "試算表"
},
"protect_note": {
"toggle-on": "保護筆記",
@@ -1594,7 +1596,8 @@
},
"search_result": {
"no_notes_found": "沒有找到符合搜尋條件的筆記。",
"search_not_executed": "尚未執行搜尋。請點擊上方的「搜尋」按鈕查看結果。"
"search_not_executed": "尚未執行搜尋。",
"search_now": "立即搜尋"
},
"spacer": {
"configure_launchbar": "設定啟動欄"
@@ -2011,7 +2014,9 @@
"app-restart-required": "(需要重啟程式以套用更改)"
},
"pagination": {
"total_notes": "{{count}} 筆記"
"total_notes": "{{count}} 筆記",
"prev_page": "上一頁",
"next_page": "下一頁"
},
"collections": {
"rendering_error": "發現錯誤,無法顯示內容。"

View File

@@ -55,7 +55,10 @@
"show_help": "Показати Довідку",
"logout": "Вийти",
"show-cheatsheet": "Показати Шпаргалку",
"toggle-zen-mode": "Дзен-режим"
"toggle-zen-mode": "Дзен-режим",
"new-version-available": "Доступне оновлення",
"download-update": "Отримати версію {{latest Version}}",
"search_notes": "Пошук нотаток"
},
"modal": {
"help_title": "Показати більше інформації про це вікно",
@@ -293,7 +296,8 @@
},
"import-status": "Статус Імпорту",
"in-progress": "Триває Імпорт: {{progress}}",
"successful": "Імпорт успішно завершено."
"successful": "Імпорт успішно завершено.",
"importZipRecommendation": "Під час імпорту ZIP-файлу ієрархія нотаток відображатиме структуру підкаталогів в архіві."
},
"prompt": {
"title": "Запит(prompt)",
@@ -355,7 +359,8 @@
"info": {
"modalTitle": "Інформаційне повідомлення",
"closeButton": "Закрити",
"okButton": "ОК"
"okButton": "ОК",
"copy_to_clipboard": "Копіювати в буфер обміну"
},
"jump_to_note": {
"search_placeholder": "Пошук нотатки за її назвою або типом > для команд...",
@@ -805,7 +810,14 @@
"convert_into_attachment_failed": "Не вдалося конвертувати нотатку '{{title}}'.",
"convert_into_attachment_successful": "Нотатку '{{title}}' перетворено на вкладення.",
"convert_into_attachment_prompt": "Ви впевнені, що хочете перетворити нотатку '{{title}}' на вкладення батьківської нотатки?",
"print_pdf": "Експортувати як PDF..."
"print_pdf": "Експортувати як PDF...",
"open_note_on_server": "Відкрити нотатку на сервері",
"view_revisions": "Ревізії нотатки...",
"advanced": "Розширені",
"export_as_image": "Експортувати як зображення",
"export_as_image_png": "PNG (растровий)",
"export_as_image_svg": "SVG (векторний)",
"note_map": "Карта нотатки"
},
"onclick_button": {
"no_click_handler": "Віджет кнопки '{{componentId}}' не має визначеного обробника кліків"
@@ -858,7 +870,10 @@
"insert_child_note": "Вставити дочірню нотатку",
"delete_this_note": "Видалити цю нотатку",
"error_cannot_get_branch_id": "Не вдається отримати branchId для notePath '{{notePath}}'",
"error_unrecognized_command": "Нерозпізнана команда {{command}}"
"error_unrecognized_command": "Нерозпізнана команда {{command}}",
"note_revisions": "Ревізії нотатки",
"backlinks": "Зворотні посилання",
"content_language_switcher": "Мова контенту: {{language}}"
},
"note_icon": {
"change_note_icon": "Змінити значок нотатки",
@@ -866,7 +881,13 @@
"reset-default": "Скинути значок до стандартного значення",
"search_placeholder_one": "Пошук {{number}} значка у {{count}} пакеті",
"search_placeholder_few": "Пошук {{number}} значків у {{count}} пакетах",
"search_placeholder_many": "Пошук {{number}} значків у {{count}} пакетах"
"search_placeholder_many": "Пошук {{number}} значків у {{count}} пакетах",
"search_placeholder_filtered": "Пошук {{number}} іконок у {{name}}",
"filter": "Фільтр",
"filter-none": "Всі іконки",
"filter-default": "Іконки за замовчуванням",
"icon_tooltip": "{{name}}\nПакет іконок: {{iconPack}}",
"no_results": "Іконки не знайдено"
},
"basic_properties": {
"note_type": "Тип нотатки",
@@ -888,7 +909,13 @@
"table": "Таблиця",
"geo-map": "Географічна карта",
"board": "Дошка",
"include_archived_notes": "Показати архівовані нотатки"
"include_archived_notes": "Показати архівовані нотатки",
"expand_tooltip": "Розгортає безпосередні дочірні елементи цієї колекції (на один рівень у глибину). Щоб переглянути більше параметрів, натисніть стрілку праворуч.",
"expand_first_level": "Розгорнути прямі дочірні елементи",
"expand_nth_level": "Розгорнути {{depth}} рівнів",
"expand_all_levels": "Розгорнути всі рівні",
"presentation": "Презентація",
"hide_child_notes": "Приховати дочірні нотатки в дереві"
},
"edited_notes": {
"no_edited_notes_found": "Цього дня ще немає редагованих нотаток...",
@@ -921,7 +948,8 @@
},
"inherited_attribute_list": {
"title": "Успадковані Атрибути",
"no_inherited_attributes": "Немає успадкованих атрибутів."
"no_inherited_attributes": "Немає успадкованих атрибутів.",
"none": "пусто"
},
"note_info_widget": {
"note_id": "ID Нотатки",
@@ -932,7 +960,9 @@
"note_size_info": "Розмір нотатки надає приблизну оцінку вимог до зберігання для цієї нотатки. Він враховує вміст нотатки та вміст її версій.",
"calculate": "обчислити",
"subtree_size": "(розмір піддерева: {{size}} у {{count}} нотатках)",
"title": "Інформація про нотатку"
"title": "Інформація про нотатку",
"mime": "Тип MIME",
"show_similar_notes": "Показати схожі нотатки"
},
"note_map": {
"open_full": "Розгорнути на повний розмір",
@@ -995,7 +1025,9 @@
"search_parameters": "Параметри пошуку",
"unknown_search_option": "Невідомий параметр пошуку {{searchOptionName}}",
"actions_executed": "Дії виконано.",
"search_note_saved": "Нотатка з пошуку збережена у {{- notePathTitle}}"
"search_note_saved": "Нотатка з пошуку збережена у {{- notePathTitle}}",
"option": "опції",
"view_options": "Опції перегляду:"
},
"similar_notes": {
"title": "Схожі нотатки",
@@ -1089,7 +1121,13 @@
},
"editable_text": {
"placeholder": "Введіть тут вміст вашої нотатки...",
"auto-detect-language": "Автовизначено"
"auto-detect-language": "Автовизначено",
"editor_crashed_title": "Збій текстового редактора",
"editor_crashed_content": "Ваш контент успішно відновлено, але деякі з ваших останніх змін могли бути не збережені.",
"editor_crashed_details_button": "Переглянути більше деталей...",
"editor_crashed_details_intro": "Якщо ви стикаєтеся з цією помилкою кілька разів, подумайте про те, щоб повідомити про неї на GitHub, вставивши наведену нижче інформацію.",
"editor_crashed_details_title": "Технічна інформація",
"keeps-crashing": "Компонент редагування постійно аварійно завершує роботу. Спробуйте перезапустити Trilium. Якщо проблема не зникає, спробуйте створити звіт про помилку."
},
"empty": {
"open_note_instruction": "Відкрийте нотатку, ввівши її заголовок в поле нижче, або виберіть нотатку в дереві.",
@@ -1955,5 +1993,11 @@
"pages_one": "{{count}} сторінка",
"pages_few": "{{count}} сторінки",
"pages_many": "{{count}} сторінок"
},
"render": {
"setup_title": "Відображати власний HTML або Preact JSX у цій нотатці",
"setup_create_sample_preact": "Створіть зразок нотатки за допомогою Preact",
"setup_create_sample_html": "Створити зразок нотатки за допомогою HTML",
"setup_sample_created": "Зразок нотатки було створено як дочірню нотатку."
}
}

View File

@@ -4,6 +4,7 @@
overflow: visible;
contain: none !important;
clear: both;
&.full-height {
overflow: auto;

View File

@@ -54,6 +54,8 @@ export default function PopupEditor() {
}
});
// Events triggered at note context level (e.g. the save indicator) would not work since the note context has no parent component. Propagate events to parent component so that they can be handled properly.
noteContext.triggerEvent = (name, data) => parentComponent?.handleEventInChildren(name, data);
setNoteContext(noteContext);
setShown(true);
});

View File

@@ -7,7 +7,7 @@ import { t } from "../../services/i18n";
import { goToLinkExt } from "../../services/link";
import { Badge, BadgeWithDropdown } from "../react/Badge";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useGetContextDataFrom, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useShareState } from "../ribbon/BasicPropertiesTab";
import { useShareInfo } from "../shared_info";
import { ActiveContentBadges } from "./ActiveContentBadges";
@@ -112,7 +112,8 @@ function ExecuteBadge() {
}
export function SaveStatusBadge() {
const saveState = useGetContextData("saveState");
const { noteContext} = useNoteContext();
const saveState = useGetContextDataFrom(noteContext, "saveState");
if (!saveState) return;
const stateConfig = {

View File

@@ -141,5 +141,11 @@ export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
view: () => import("./type_widgets/SqlConsole"),
className: "sql-console-widget-container",
isFullHeight: true
},
spreadsheet: {
view: () => import("./type_widgets/Spreadsheet"),
className: "note-detail-spreadsheet",
printable: true,
isFullHeight: true
}
};

View File

@@ -79,7 +79,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
}
#isFullWidthNote(note: FNote) {
if (["code", "image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
if (["code", "image", "mermaid", "book", "render", "canvas", "webView", "mindMap", "spreadsheet"].includes(note.type)) {
return true;
}
@@ -102,7 +102,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
const COLLECTIONS_WITH_BACKGROUND_EFFECTS = [
"grid",
"list"
]
];
if (note.isOptions()) {
return true;

View File

@@ -85,7 +85,7 @@ export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNote
);
const isElectron = getIsElectron();
const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(noteType);
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "spreadsheet"].includes(noteType);
const isSearchOrBook = ["search", "book"].includes(noteType);
const isHelpPage = note.noteId.startsWith("_help");
const [syncServerHost] = useTriliumOption("syncServerHost");

View File

@@ -1,6 +1,6 @@
import "./TableOfContents.css";
import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import { attributeChangeAffectsHeading, CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import clsx from "clsx";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
@@ -170,11 +170,14 @@ function EditableTextTableOfContents() {
const affectsHeadings = changes.some( change => {
return (
change.type === 'insert' || change.type === 'remove' || (change.type === 'attribute' && change.attributeKey === 'headingLevel')
change.type === 'insert' || change.type === 'remove' ||
(change.type === 'attribute' && attributeChangeAffectsHeading(change, textEditor))
);
});
if (affectsHeadings) {
setHeadings(extractTocFromTextEditor(textEditor));
requestAnimationFrame(() => {
setHeadings(extractTocFromTextEditor(textEditor));
});
}
};

View File

@@ -0,0 +1,3 @@
.note-detail-spreadsheet > .spreadsheet {
height: 100%;
}

View File

@@ -0,0 +1,125 @@
import "@univerjs/preset-sheets-core/lib/index.css";
import "./Spreadsheet.css";
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
import UniverPresetSheetsCoreEnUS from '@univerjs/preset-sheets-core/locales/en-US';
import { CommandType, createUniver, FUniver, IDisposable, IWorkbookData, LocaleType, mergeLocales } from '@univerjs/presets';
import { MutableRef, useEffect, useRef } from "preact/hooks";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import { useColorScheme, useEditorSpacedUpdate } from "../react/hooks";
import { TypeWidgetProps } from "./type_widget";
interface PersistedData {
version: number;
workbook: Parameters<FUniver["createWorkbook"]>[0];
}
export default function Spreadsheet({ note, noteContext }: TypeWidgetProps) {
const containerRef = useRef<HTMLDivElement>(null);
const apiRef = useRef<FUniver>();
useInitializeSpreadsheet(containerRef, apiRef);
useDarkMode(apiRef);
usePersistence(note, noteContext, apiRef);
return <div ref={containerRef} className="spreadsheet" />;
}
function useInitializeSpreadsheet(containerRef: MutableRef<HTMLDivElement | null>, apiRef: MutableRef<FUniver | undefined>) {
useEffect(() => {
if (!containerRef.current) return;
const { univerAPI } = createUniver({
locale: LocaleType.EN_US,
locales: {
[LocaleType.EN_US]: mergeLocales(
UniverPresetSheetsCoreEnUS
),
},
presets: [
UniverSheetsCorePreset({
container: containerRef.current,
})
]
});
apiRef.current = univerAPI;
return () => univerAPI.dispose();
}, [ apiRef, containerRef ]);
}
function useDarkMode(apiRef: MutableRef<FUniver | undefined>) {
const colorScheme = useColorScheme();
// React to dark mode.
useEffect(() => {
const univerAPI = apiRef.current;
if (!univerAPI) return;
univerAPI.toggleDarkMode(colorScheme === 'dark');
}, [ colorScheme, apiRef ]);
}
function usePersistence(note: FNote, noteContext: NoteContext | null | undefined, apiRef: MutableRef<FUniver | undefined>) {
const changeListener = useRef<IDisposable>(null);
const spacedUpdate = useEditorSpacedUpdate({
noteType: "spreadsheet",
note,
noteContext,
getData() {
const univerAPI = apiRef.current;
if (!univerAPI) return undefined;
const workbook = univerAPI.getActiveWorkbook();
if (!workbook) return undefined;
const content = {
version: 1,
workbook: workbook.save()
};
return {
content: JSON.stringify(content)
};
},
onContentChange(newContent) {
const univerAPI = apiRef.current;
if (!univerAPI) return undefined;
// Dispose the existing workbook.
const existingWorkbook = univerAPI.getActiveWorkbook();
if (existingWorkbook) {
univerAPI.disposeUnit(existingWorkbook.getId());
}
let workbookData: Partial<IWorkbookData> = {};
if (newContent) {
try {
const parsedContent = JSON.parse(newContent) as unknown;
if (parsedContent && typeof parsedContent === "object" && "workbook" in parsedContent) {
const persistedData = parsedContent as PersistedData;
workbookData = persistedData.workbook;
}
} catch (e) {
console.error("Failed to parse spreadsheet content", e);
}
}
const workbook = univerAPI.createWorkbook(workbookData);
if (changeListener.current) {
changeListener.current.dispose();
}
changeListener.current = workbook.onCommandExecuted(command => {
if (command.type !== CommandType.MUTATION) return;
spacedUpdate.scheduleUpdate();
});
},
});
useEffect(() => {
return () => {
if (changeListener.current) {
changeListener.current.dispose();
changeListener.current = null;
}
};
}, []);
}

View File

@@ -120,7 +120,11 @@ export default defineConfig(() => ({
environment: "happy-dom",
setupFiles: [
"./src/test/setup.ts"
]
],
reporters: [
"verbose",
["html", { outputFile: "./test-output/vitest/html/index.html" }]
],
},
commonjsOptions: {
transformMixedEsModules: true,

View File

@@ -34,8 +34,8 @@
"@types/electron-squirrel-startup": "1.0.2",
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.1",
"electron": "40.6.1",
"copy-webpack-plugin": "14.0.0",
"electron": "40.8.0",
"@electron-forge/cli": "7.11.1",
"@electron-forge/maker-deb": "7.11.1",
"@electron-forge/maker-dmg": "7.11.1",

View File

@@ -11,9 +11,9 @@
"@triliumnext/client": "workspace:*",
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.1",
"electron": "40.6.1",
"fs-extra": "11.3.3"
"copy-webpack-plugin": "14.0.0",
"electron": "40.8.0",
"fs-extra": "11.3.4"
},
"scripts": {
"build": "tsx scripts/build.ts",

View File

@@ -31,7 +31,7 @@
"dependencies": {
"better-sqlite3": "12.6.2",
"html-to-text": "9.0.5",
"node-html-parser": "7.0.2",
"node-html-parser": "7.1.0",
"sucrase": "3.35.1"
},
"devDependencies": {
@@ -55,9 +55,9 @@
"@types/html": "1.0.4",
"@types/ini": "4.1.1",
"@types/mime-types": "3.0.1",
"@types/multer": "2.0.0",
"@types/multer": "2.1.0",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.16.0",
"@types/sanitize-html": "2.16.1",
"@types/sax": "1.2.7",
"@types/serve-favicon": "2.5.7",
"@types/serve-static": "2.2.0",
@@ -81,24 +81,24 @@
"csrf-csrf": "3.2.2",
"debounce": "3.0.0",
"debug": "4.4.3",
"ejs": "4.0.1",
"electron": "40.6.1",
"ejs": "5.0.1",
"electron": "40.8.0",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"express": "5.2.1",
"express-http-proxy": "2.1.2",
"express-openid-connect": "2.19.4",
"express-rate-limit": "8.2.1",
"express-rate-limit": "8.3.0",
"express-session": "1.19.0",
"file-uri-to-path": "2.0.0",
"fs-extra": "11.3.3",
"fs-extra": "11.3.4",
"helmet": "8.1.0",
"html": "1.0.0",
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "25.8.13",
"i18next": "25.8.14",
"i18next-fs-backend": "2.6.1",
"image-type": "6.0.0",
"ini": "6.0.0",
@@ -106,15 +106,15 @@
"is-svg": "6.1.0",
"jimp": "1.6.0",
"lorem-ipsum": "2.0.8",
"marked": "17.0.3",
"marked": "17.0.4",
"mime-types": "3.0.2",
"multer": "2.1.0",
"multer": "2.1.1",
"normalize-strings": "1.1.1",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.17.1",
"sax": "1.4.4",
"sax": "1.5.0",
"serve-favicon": "2.5.1",
"stream-throttle": "0.1.3",
"strip-bom": "5.0.0",

View File

@@ -86,8 +86,9 @@ export default async function buildApp() {
app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt")));
app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png")));
const sessionParser = (await import("./routes/session_parser.js")).default;
const { default: sessionParser, startSessionCleanup } = await import("./routes/session_parser.js");
app.use(sessionParser);
startSessionCleanup();
app.use(favicon(path.join(assetsDir, isDev ? "icon-dev.ico" : "icon.ico")));
if (openID.isOpenIDEnabled())
@@ -98,16 +99,16 @@ export default async function buildApp() {
custom.register(app);
error_handlers.register(app);
// triggers sync timer
await import("./services/sync.js");
const { startSyncTimer } = await import("./services/sync.js");
startSyncTimer();
// triggers backup timer
await import("./services/backup.js");
// trigger consistency checks timer
await import("./services/consistency_checks.js");
const { startConsistencyChecks } = await import("./services/consistency_checks.js");
startConsistencyChecks();
await import("./services/scheduler.js");
const { startScheduler } = await import("./services/scheduler.js");
startScheduler();
startScheduledCleanup();

View File

@@ -156,7 +156,8 @@
"go-to-next-note-title": "К следующей заметке",
"open-today-journal-note-title": "Открыть сегодняшнюю заметку в журнале",
"zen-mode": "Режим \"Дзен\"",
"command-palette": "Открыть панель команд"
"command-palette": "Открыть панель команд",
"tab-switcher-title": "Переключатель вкладок"
},
"tray": {
"bookmarks": "Закладки",
@@ -313,7 +314,7 @@
"title": "Настройка",
"heading": "Настройка Trilium",
"new-document": "Я новый пользователь и хочу создать новый документ Trilium для своих заметок",
"sync-from-desktop": "У меня уже есть приложение ПК, и я хочу настроить синхронизацию с ним",
"sync-from-desktop": "У меня уже есть настольное приложение, и я хочу настроить синхронизацию с ним",
"sync-from-server": "У меня уже есть сервер, и я хочу настроить синхронизацию с ним",
"init-in-progress": "Идет инициализация документа",
"redirecting": "Вскоре вы будете перенаправлены на страницу приложения."
@@ -397,8 +398,8 @@
"clipped-from": "Эта заметка изначально была вырезана из {{- url}}"
},
"setup_sync-from-desktop": {
"heading": "Синхронизация с приложения ПК",
"description": "Эту настройку необходимо инициировать из приложения для ПК:",
"heading": "Синхронизация с настольной версией",
"description": "Это настройку нужно выполнить с помощью настольной версии:",
"step1": "Откройте приложение Trilium Notes на ПК.",
"step2": "В меню Trilium выберите «Параметры».",
"step3": "Нажмите на категорию «Синхронизация».",

View File

@@ -2,6 +2,21 @@
"keyboard_actions": {
"back-in-note-history": "Gå till föregående anteckning i historiken",
"forward-in-note-history": "Gå till nästa anteckning i historiken",
"open-jump-to-note-dialog": "Öppna \"Hoppa till anteckning\" dialog"
"open-jump-to-note-dialog": "Öppna \"Hoppa till anteckning\" dialog",
"open-command-palette": "Öppna kommandomenyn",
"quick-search": "Öppna snabbsökning",
"search-in-subtree": "Sök anteckningar nedåt i anteckningshierarkin",
"expand-subtree": "Expandera hierarkin under denna anteckning",
"collapse-tree": "Stänger anteckningshierarkin",
"collapse-subtree": "Stänger hierarkin under aktuell anteckning",
"sort-child-notes": "Sortera underordnade anteckningar",
"creating-and-moving-notes": "Skapa och flytta anteckningar",
"create-note-after": "Skapa ny anteckning efter aktiv anteckning",
"create-note-into": "Skapa ny anteckning underordnad aktiv anteckning",
"create-note-into-inbox": "Skapa en anteckning i inboxen (om angiven) eller som daganteckning",
"delete-note": "Radera anteckning",
"move-note-up": "Flytta anteckning uppåt",
"move-note-down": "Flytta anteckning nedåt",
"scroll-to-active-note": "Bläddra i anteckningshierarkin till aktiv anteckning"
}
}

View File

@@ -16,6 +16,9 @@
"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"
"move-note-down": "Notu aşağıya kaydır",
"create-note-into-inbox": "Eğer tanımlandıysa gelen kutusunda bir not veya günlük not oluşturun",
"move-note-up-in-hierarchy": "Notu hiyerarşide yukarı taşı",
"move-note-down-in-hierarchy": "Notu hiyerarşide aşağı taşı"
}
}

View File

@@ -1,14 +1,15 @@
import type { AttachmentRow } from "@triliumnext/commons";
import type { Router } from "express";
import becca from "../becca/becca.js";
import utils from "../services/utils.js";
import eu from "./etapi_utils.js";
import type { ValidatorMap } from "./etapi-interface.js";
import mappers from "./mappers.js";
import v from "./validators.js";
import utils from "../services/utils.js";
import type { Router } from "express";
import type { AttachmentRow } from "@triliumnext/commons";
import type { ValidatorMap } from "./etapi-interface.js";
function register(router: Router) {
eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const attachments = note.getAttachments();
res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment)));
@@ -41,7 +42,7 @@ function register(router: Router) {
}
});
eu.route(router, "get", "/etapi/attachments/:attachmentId", (req, res, next) => {
eu.route<{ attachmentId: string }>(router, "get", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
res.json(mappers.mapAttachmentToPojo(attachment));
@@ -54,7 +55,7 @@ function register(router: Router) {
position: [v.notNull, v.isInteger]
};
eu.route(router, "patch", "/etapi/attachments/:attachmentId", (req, res, next) => {
eu.route<{ attachmentId: string }>(router, "patch", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
@@ -67,7 +68,7 @@ function register(router: Router) {
res.json(mappers.mapAttachmentToPojo(attachment));
});
eu.route(router, "get", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
eu.route<{ attachmentId: string }>(router, "get", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
@@ -84,7 +85,7 @@ function register(router: Router) {
res.send(attachment.getContent());
});
eu.route(router, "put", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
eu.route<{ attachmentId: string }>(router, "put", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
@@ -96,7 +97,7 @@ function register(router: Router) {
return res.sendStatus(204);
});
eu.route(router, "delete", "/etapi/attachments/:attachmentId", (req, res, next) => {
eu.route<{ attachmentId: string }>(router, "delete", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = becca.getAttachment(req.params.attachmentId);
if (!attachment) {

View File

@@ -1,14 +1,15 @@
import becca from "../becca/becca.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import attributeService from "../services/attributes.js";
import v from "./validators.js";
import type { Router } from "express";
import type { AttributeRow } from "@triliumnext/commons";
import type { Router } from "express";
import becca from "../becca/becca.js";
import attributeService from "../services/attributes.js";
import eu from "./etapi_utils.js";
import type { ValidatorMap } from "./etapi-interface.js";
import mappers from "./mappers.js";
import v from "./validators.js";
function register(router: Router) {
eu.route(router, "get", "/etapi/attributes/:attributeId", (req, res, next) => {
eu.route<{ attributeId: string }>(router, "get", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
res.json(mappers.mapAttributeToPojo(attribute));
@@ -51,7 +52,7 @@ function register(router: Router) {
position: [v.notNull, v.isInteger]
};
eu.route(router, "patch", "/etapi/attributes/:attributeId", (req, res, next) => {
eu.route<{ attributeId: string }>(router, "patch", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
if (attribute.type === "label") {
@@ -67,7 +68,7 @@ function register(router: Router) {
res.json(mappers.mapAttributeToPojo(attribute));
});
eu.route(router, "delete", "/etapi/attributes/:attributeId", (req, res, next) => {
eu.route<{ attributeId: string }>(router, "delete", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = becca.getAttribute(req.params.attributeId);
if (!attribute) {

View File

@@ -1,10 +1,10 @@
import type { Router } from "express";
import eu from "./etapi_utils.js";
import backupService from "../services/backup.js";
import eu from "./etapi_utils.js";
function register(router: Router) {
eu.route(router, "put", "/etapi/backup/:backupName", (req, res, next) => {
eu.route<{ backupName: string }>(router, "put", "/etapi/backup/:backupName", (req, res, next) => {
backupService.backupNow(req.params.backupName)
.then(() => res.sendStatus(204))
.catch(() => res.sendStatus(500));

View File

@@ -1,15 +1,15 @@
import type { BranchRow } from "@triliumnext/commons";
import type { Router } from "express";
import becca from "../becca/becca.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import BBranch from "../becca/entities/bbranch.js";
import entityChangesService from "../services/entity_changes.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import v from "./validators.js";
import type { BranchRow } from "@triliumnext/commons";
function register(router: Router) {
eu.route(router, "get", "/etapi/branches/:branchId", (req, res, next) => {
eu.route<{ branchId: string }>(router, "get", "/etapi/branches/:branchId", (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId);
res.json(mappers.mapBranchToPojo(branch));
@@ -37,15 +37,15 @@ function register(router: Router) {
existing.save();
return res.status(200).json(mappers.mapBranchToPojo(existing));
} else {
try {
const branch = new BBranch(params).save();
}
try {
const branch = new BBranch(params).save();
res.status(201).json(mappers.mapBranchToPojo(branch));
} catch (e: any) {
throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
}
res.status(201).json(mappers.mapBranchToPojo(branch));
} catch (e: any) {
throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
}
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
@@ -54,7 +54,7 @@ function register(router: Router) {
isExpanded: [v.notNull, v.isBoolean]
};
eu.route(router, "patch", "/etapi/branches/:branchId", (req, res, next) => {
eu.route<{ branchId: string }>(router, "patch", "/etapi/branches/:branchId", (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId);
eu.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
@@ -63,7 +63,7 @@ function register(router: Router) {
res.json(mappers.mapBranchToPojo(branch));
});
eu.route(router, "delete", "/etapi/branches/:branchId", (req, res, next) => {
eu.route<{ branchId: string }>(router, "delete", "/etapi/branches/:branchId", (req, res, next) => {
const branch = becca.getBranch(req.params.branchId);
if (!branch) {
@@ -75,7 +75,7 @@ function register(router: Router) {
res.sendStatus(204);
});
eu.route(router, "post", "/etapi/refresh-note-ordering/:parentNoteId", (req, res, next) => {
eu.route<{ parentNoteId: string }>(router, "post", "/etapi/refresh-note-ordering/:parentNoteId", (req, res, next) => {
eu.getAndCheckNote(req.params.parentNoteId);
entityChangesService.putNoteReorderingEntityChange(req.params.parentNoteId, "etapi");

View File

@@ -1,12 +1,14 @@
import cls from "../services/cls.js";
import sql from "../services/sql.js";
import log from "../services/log.js";
import becca from "../becca/becca.js";
import etapiTokenService from "../services/etapi_tokens.js";
import config from "../services/config.js";
import type { NextFunction, Request, RequestHandler, Response, Router } from "express";
import type { ValidatorMap } from "./etapi-interface.js";
import type { ParamsDictionary } from "express-serve-static-core";
import becca from "../becca/becca.js";
import type { ApiRequestHandler, SyncRouteRequestHandler } from "../routes/route_api.js";
import cls from "../services/cls.js";
import config from "../services/config.js";
import etapiTokenService from "../services/etapi_tokens.js";
import log from "../services/log.js";
import sql from "../services/sql.js";
import type { ValidatorMap } from "./etapi-interface.js";
const GENERIC_CODE = "GENERIC";
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
@@ -35,8 +37,8 @@ function sendError(res: Response, statusCode: number, code: string, message: str
.send(
JSON.stringify({
status: statusCode,
code: code,
message: message
code,
message
})
);
}
@@ -49,7 +51,7 @@ function checkEtapiAuth(req: Request, res: Response, next: NextFunction) {
}
}
function processRequest(req: Request, res: Response, routeHandler: ApiRequestHandler, next: NextFunction, method: string, path: string) {
function processRequest<P extends ParamsDictionary>(req: Request<P>, res: Response, routeHandler: ApiRequestHandler<P>, next: NextFunction, method: string, path: string) {
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
@@ -73,12 +75,12 @@ function processRequest(req: Request, res: Response, routeHandler: ApiRequestHan
}
}
function route(router: Router, method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler) {
router[method](path, checkEtapiAuth, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
function route<P extends ParamsDictionary>(router: Router, method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler<P>) {
router[method](path, checkEtapiAuth, (req: Request<P>, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
}
function NOT_AUTHENTICATED_ROUTE(router: Router, method: HttpMethod, path: string, middleware: RequestHandler[], routeHandler: SyncRouteRequestHandler) {
router[method](path, ...middleware, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
function NOT_AUTHENTICATED_ROUTE<P extends ParamsDictionary>(router: Router, method: HttpMethod, path: string, middleware: RequestHandler[], routeHandler: SyncRouteRequestHandler<P>) {
router[method](path, ...middleware, (req: Request<P>, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
}
function getAndCheckNote(noteId: string) {
@@ -86,9 +88,8 @@ function getAndCheckNote(noteId: string) {
if (note) {
return note;
} else {
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
}
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
}
function getAndCheckAttachment(attachmentId: string) {
@@ -96,9 +97,8 @@ function getAndCheckAttachment(attachmentId: string) {
if (attachment) {
return attachment;
} else {
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
}
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
}
function getAndCheckBranch(branchId: string) {
@@ -106,9 +106,8 @@ function getAndCheckBranch(branchId: string) {
if (branch) {
return branch;
} else {
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
}
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
}
function getAndCheckAttribute(attributeId: string) {
@@ -116,9 +115,8 @@ function getAndCheckAttribute(attributeId: string) {
if (attribute) {
return attribute;
} else {
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
}
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
}
function getAndCheckRevision(revisionId: string) {
@@ -126,9 +124,8 @@ function getAndCheckRevision(revisionId: string) {
if (revision) {
return revision;
} else {
throw new EtapiError(404, "REVISION_NOT_FOUND", `Revision '${revisionId}' not found.`);
}
throw new EtapiError(404, "REVISION_NOT_FOUND", `Revision '${revisionId}' not found.`);
}
function validateAndPatch(target: any, source: any, allowedProperties: ValidatorMap) {

View File

@@ -1,20 +1,21 @@
import becca from "../becca/becca.js";
import utils from "../services/utils.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 v from "./validators.js";
import searchService from "../services/search/services/search.js";
import SearchContext from "../services/search/search_context.js";
import zipExportService from "../services/export/zip.js";
import zipImportService from "../services/import/zip.js";
import type { Request, Router } from "express";
import type { ParsedQs } from "qs";
import type { NoteParams } from "../services/note-interface.js";
import type { SearchParams } from "../services/search/services/types.js";
import type { ValidatorMap } from "./etapi-interface.js";
import becca from "../becca/becca.js";
import zipExportService from "../services/export/zip.js";
import type { ExportFormat } from "../services/export/zip/abstract_provider.js";
import zipImportService from "../services/import/zip.js";
import type { NoteParams } from "../services/note-interface.js";
import noteService from "../services/notes.js";
import SearchContext from "../services/search/search_context.js";
import searchService from "../services/search/services/search.js";
import type { SearchParams } from "../services/search/services/types.js";
import TaskContext from "../services/task_context.js";
import utils from "../services/utils.js";
import eu from "./etapi_utils.js";
import type { ValidatorMap } from "./etapi-interface.js";
import mappers from "./mappers.js";
import v from "./validators.js";
function register(router: Router) {
eu.route(router, "get", "/etapi/notes", (req, res, next) => {
@@ -41,7 +42,7 @@ function register(router: Router) {
res.json(resp);
});
eu.route(router, "get", "/etapi/notes/:noteId", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
res.json(mappers.mapNoteToPojo(note));
@@ -86,7 +87,7 @@ function register(router: Router) {
utcDateCreated: [v.notNull, v.isString, v.isUtcDateTime]
};
eu.route(router, "patch", "/etapi/notes/:noteId", (req, res, next) => {
eu.route<{ noteId: string }>(router, "patch", "/etapi/notes/:noteId", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
@@ -100,7 +101,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "delete", "/etapi/notes/:noteId", (req, res, next) => {
eu.route<{ noteId: string }>(router, "delete", "/etapi/notes/:noteId", (req, res, next) => {
const { noteId } = req.params;
const note = becca.getNote(noteId);
@@ -114,7 +115,7 @@ function register(router: Router) {
res.sendStatus(204);
});
eu.route(router, "get", "/etapi/notes/:noteId/content", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId/content", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
@@ -131,7 +132,7 @@ function register(router: Router) {
res.send(note.getContent());
});
eu.route(router, "put", "/etapi/notes/:noteId/content", (req, res, next) => {
eu.route<{ noteId: string }>(router, "put", "/etapi/notes/:noteId/content", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
@@ -146,7 +147,7 @@ function register(router: Router) {
return res.sendStatus(204);
});
eu.route(router, "get", "/etapi/notes/:noteId/export", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId/export", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const format = req.query.format || "html";
@@ -163,7 +164,7 @@ function register(router: Router) {
zipExportService.exportToZip(taskContext, branch, format as ExportFormat, res);
});
eu.route(router, "post", "/etapi/notes/:noteId/import", (req, res, next) => {
eu.route<{ noteId: string }>(router, "post", "/etapi/notes/:noteId/import", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const taskContext = new TaskContext("no-progress-reporting", "importNotes", null);
@@ -175,7 +176,7 @@ function register(router: Router) {
}); // we need better error handling here, async errors won't be properly processed.
});
eu.route(router, "post", "/etapi/notes/:noteId/revision", (req, res, next) => {
eu.route<{ noteId: string }>(router, "post", "/etapi/notes/:noteId/revision", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
note.saveRevision();
@@ -183,7 +184,7 @@ function register(router: Router) {
return res.sendStatus(204);
});
eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const attachments = note.getAttachments();

View File

@@ -1,13 +1,14 @@
import type { NoteRow, RecentChangeRow } from "@triliumnext/commons";
import type { Router } from "express";
import becca from "../becca/becca.js";
import noteService from "../services/notes.js";
import protectedSessionService from "../services/protected_session.js";
import sql from "../services/sql.js";
import TaskContext from "../services/task_context.js";
import utils from "../services/utils.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
@@ -130,7 +131,7 @@ function register(router: Router) {
});
// GET /etapi/notes/:noteId/revisions - List all revisions for a note
eu.route(router, "get", "/etapi/notes/:noteId/revisions", (req, res, next) => {
eu.route<{ noteId: string }>(router, "get", "/etapi/notes/:noteId/revisions", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const revisions = becca.getRevisionsFromQuery(
@@ -146,7 +147,7 @@ function register(router: Router) {
});
// POST /etapi/notes/:noteId/undelete - Restore a deleted note
eu.route(router, "post", "/etapi/notes/:noteId/undelete", (req, res, next) => {
eu.route<{ noteId: string }>(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]);
@@ -172,7 +173,7 @@ function register(router: Router) {
});
// GET /etapi/revisions/:revisionId - Get revision metadata
eu.route(router, "get", "/etapi/revisions/:revisionId", (req, res, next) => {
eu.route<{ revisionId: string }>(router, "get", "/etapi/revisions/:revisionId", (req, res, next) => {
const revision = eu.getAndCheckRevision(req.params.revisionId);
if (revision.isProtected) {
@@ -183,7 +184,7 @@ function register(router: Router) {
});
// GET /etapi/revisions/:revisionId/content - Get revision content
eu.route(router, "get", "/etapi/revisions/:revisionId/content", (req, res, next) => {
eu.route<{ revisionId: string }>(router, "get", "/etapi/revisions/:revisionId/content", (req, res, next) => {
const revision = eu.getAndCheckRevision(req.params.revisionId);
if (revision.isProtected) {

View File

@@ -1,8 +1,9 @@
import specialNotesService from "../services/special_notes.js";
import type { Router } from "express";
import dateNotesService from "../services/date_notes.js";
import specialNotesService from "../services/special_notes.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import type { Router } from "express";
const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
const getWeekInvalidError = (week: string) => new eu.EtapiError(400, "WEEK_INVALID", `Week "${week}" is not valid.`);
@@ -15,7 +16,7 @@ function isValidDate(date: string) {
}
function register(router: Router) {
eu.route(router, "get", "/etapi/inbox/:date", (req, res, next) => {
eu.route<{ date: string }>(router, "get", "/etapi/inbox/:date", (req, res, next) => {
const { date } = req.params;
if (!isValidDate(date)) {
@@ -25,7 +26,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "get", "/etapi/calendar/days/:date", (req, res, next) => {
eu.route<{ date: string }>(router, "get", "/etapi/calendar/days/:date", (req, res, next) => {
const { date } = req.params;
if (!isValidDate(date)) {
@@ -36,7 +37,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "get", "/etapi/calendar/week-first-day/:date", (req, res, next) => {
eu.route<{ date: string }>(router, "get", "/etapi/calendar/week-first-day/:date", (req, res, next) => {
const { date } = req.params;
if (!isValidDate(date)) {
@@ -47,7 +48,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "get", "/etapi/calendar/weeks/:week", (req, res, next) => {
eu.route<{ week: string }>(router, "get", "/etapi/calendar/weeks/:week", (req, res, next) => {
const { week } = req.params;
if (!/[0-9]{4}-W[0-9]{2}/.test(week)) {
@@ -63,7 +64,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "get", "/etapi/calendar/months/:month", (req, res, next) => {
eu.route<{ month: string }>(router, "get", "/etapi/calendar/months/:month", (req, res, next) => {
const { month } = req.params;
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
@@ -74,7 +75,7 @@ function register(router: Router) {
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, "get", "/etapi/calendar/years/:year", (req, res, next) => {
eu.route<{ year: string }>(router, "get", "/etapi/calendar/years/:year", (req, res, next) => {
const { year } = req.params;
if (!/[0-9]{4}/.test(year)) {

View File

@@ -1,29 +1,30 @@
import becca from "../../becca/becca.js";
import blobService from "../../services/blob.js";
import ValidationError from "../../errors/validation_error.js";
import imageService from "../../services/image.js";
import type { Request } from "express";
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
import type { Request } from "express";
function getAttachmentBlob(req: Request) {
import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js";
import blobService from "../../services/blob.js";
import imageService from "../../services/image.js";
function getAttachmentBlob(req: Request<{ attachmentId: string }>) {
const preview = req.query.preview === "true";
return blobService.getBlobPojo("attachments", req.params.attachmentId, { preview });
}
function getAttachments(req: Request) {
function getAttachments(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
return note.getAttachments();
}
function getAttachment(req: Request) {
function getAttachment(req: Request<{ attachmentId: string }>) {
const { attachmentId } = req.params;
return becca.getAttachmentOrThrow(attachmentId);
}
function getAllAttachments(req: Request) {
function getAllAttachments(req: Request<{ attachmentId: string }>) {
const { attachmentId } = req.params;
// one particular attachment is requested, but return all note's attachments
@@ -31,10 +32,10 @@ function getAllAttachments(req: Request) {
return attachment.getNote()?.getAttachments() || [];
}
function saveAttachment(req: Request) {
function saveAttachment(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const { attachmentId, role, mime, title, content } = req.body;
const matchByQuery = req.query.matchBy
const matchByQuery = req.query.matchBy;
const isValidMatchBy = (typeof matchByQuery === "string") && (matchByQuery === "attachmentId" || matchByQuery === "title");
const matchBy = isValidMatchBy ? matchByQuery : undefined;
@@ -42,7 +43,7 @@ function saveAttachment(req: Request) {
note.saveAttachment({ attachmentId, role, mime, title, content }, matchBy);
}
function uploadAttachment(req: Request) {
function uploadAttachment(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const { file } = req;
@@ -76,7 +77,7 @@ function uploadAttachment(req: Request) {
};
}
function renameAttachment(req: Request) {
function renameAttachment(req: Request<{ attachmentId: string }>) {
const { title } = req.body;
const { attachmentId } = req.params;
@@ -90,7 +91,7 @@ function renameAttachment(req: Request) {
attachment.save();
}
function deleteAttachment(req: Request) {
function deleteAttachment(req: Request<{ attachmentId: string }>) {
const { attachmentId } = req.params;
const attachment = becca.getAttachment(attachmentId);
@@ -100,7 +101,7 @@ function deleteAttachment(req: Request) {
}
}
function convertAttachmentToNote(req: Request) {
function convertAttachmentToNote(req: Request<{ attachmentId: string }>) {
const { attachmentId } = req.params;
const attachment = becca.getAttachmentOrThrow(attachmentId);

View File

@@ -1,21 +1,22 @@
"use strict";
import sql from "../../services/sql.js";
import log from "../../services/log.js";
import attributeService from "../../services/attributes.js";
import BAttribute from "../../becca/entities/battribute.js";
import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import { UpdateAttributeResponse } from "@triliumnext/commons";
import type { Request } from "express";
function getEffectiveNoteAttributes(req: Request) {
import becca from "../../becca/becca.js";
import BAttribute from "../../becca/entities/battribute.js";
import ValidationError from "../../errors/validation_error.js";
import attributeService from "../../services/attributes.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
function getEffectiveNoteAttributes(req: Request<{ noteId: string }>) {
const note = becca.getNote(req.params.noteId);
return note?.getAttributes();
}
function updateNoteAttribute(req: Request) {
function updateNoteAttribute(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const body = req.body;
@@ -47,7 +48,7 @@ function updateNoteAttribute(req: Request) {
}
attribute = new BAttribute({
noteId: noteId,
noteId,
name: body.name,
type: body.type,
isInheritable: body.isInheritable
@@ -96,7 +97,7 @@ function addNoteAttribute(req: Request) {
new BAttribute({ ...body, noteId }).save();
}
function deleteNoteAttribute(req: Request) {
function deleteNoteAttribute(req: Request<{ noteId: string; attributeId: string }>) {
const noteId = req.params.noteId;
const attributeId = req.params.attributeId;
@@ -111,7 +112,7 @@ function deleteNoteAttribute(req: Request) {
}
}
function updateNoteAttributes(req: Request) {
function updateNoteAttributes(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const incomingAttributes = req.body;
@@ -193,7 +194,7 @@ function getValuesForAttribute(req: Request) {
return sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND type = 'label' AND value != '' ORDER BY value", [attributeName]);
}
function createRelation(req: Request) {
function createRelation(req: Request<{ noteId: string; targetNoteId: string; name: string }>) {
const sourceNoteId = req.params.noteId;
const targetNoteId = req.params.targetNoteId;
const name = req.params.name;
@@ -208,7 +209,7 @@ function createRelation(req: Request) {
if (!attribute) {
attribute = new BAttribute({
noteId: sourceNoteId,
name: name,
name,
type: "relation",
value: targetNoteId
}).save();

View File

@@ -1,24 +1,23 @@
"use strict";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
import entityChangesService from "../../services/entity_changes.js";
import treeService from "../../services/tree.js";
import eraseService from "../../services/erase.js";
import becca from "../../becca/becca.js";
import TaskContext from "../../services/task_context.js";
import branchService from "../../services/branches.js";
import log from "../../services/log.js";
import ValidationError from "../../errors/validation_error.js";
import eventService from "../../services/events.js";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js";
import branchService from "../../services/branches.js";
import entityChangesService from "../../services/entity_changes.js";
import eraseService from "../../services/erase.js";
import eventService from "../../services/events.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
import TaskContext from "../../services/task_context.js";
import treeService from "../../services/tree.js";
import utils from "../../services/utils.js";
/**
* Code in this file deals with moving and cloning branches. The relationship between note and parent note is unique
* for not deleted branches. There may be multiple deleted note-parent note relationships.
*/
function moveBranchToParent(req: Request) {
function moveBranchToParent(req: Request<{ branchId: string, parentBranchId: string }>) {
const { branchId, parentBranchId } = req.params;
const branchToMove = becca.getBranch(branchId);
@@ -31,7 +30,7 @@ function moveBranchToParent(req: Request) {
return branchService.moveBranchToBranch(branchToMove, targetParentBranch, branchId);
}
function moveBranchBeforeNote(req: Request) {
function moveBranchBeforeNote(req: Request<{ branchId: string, beforeBranchId: string }>) {
const { branchId, beforeBranchId } = req.params;
const branchToMove = becca.getBranchOrThrow(branchId);
@@ -79,7 +78,7 @@ function moveBranchBeforeNote(req: Request) {
return { success: true };
}
function moveBranchAfterNote(req: Request) {
function moveBranchAfterNote(req: Request<{ branchId: string, afterBranchId: string }>) {
const { branchId, afterBranchId } = req.params;
const branchToMove = becca.getBranchOrThrow(branchId);
@@ -128,7 +127,7 @@ function moveBranchAfterNote(req: Request) {
return { success: true };
}
function setExpanded(req: Request) {
function setExpanded(req: Request<{ branchId: string, expanded: string }>) {
const { branchId } = req.params;
const expanded = parseInt(req.params.expanded);
@@ -150,7 +149,7 @@ function setExpanded(req: Request) {
}
}
function setExpandedForSubtree(req: Request) {
function setExpandedForSubtree(req: Request<{ branchId: string, expanded: string }>) {
const { branchId } = req.params;
const expanded = parseInt(req.params.expanded);
@@ -232,7 +231,7 @@ function setExpandedForSubtree(req: Request) {
* - session: []
* tags: ["data"]
*/
function deleteBranch(req: Request) {
function deleteBranch(req: Request<{ branchId: string }>) {
const last = req.query.last === "true";
const eraseNotes = req.query.eraseNotes === "true";
const branch = becca.getBranchOrThrow(req.params.branchId);
@@ -256,11 +255,11 @@ function deleteBranch(req: Request) {
}
return {
noteDeleted: noteDeleted
noteDeleted
};
}
function setPrefix(req: Request) {
function setPrefix(req: Request<{ branchId: string }>) {
const branchId = req.params.branchId;
//TriliumNextTODO: req.body arrives as string, so req.body.prefix will be undefined did the code below ever even work?
const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
@@ -272,7 +271,7 @@ function setPrefix(req: Request) {
function setPrefixBatch(req: Request) {
const { branchIds, prefix } = req.body;
if (!Array.isArray(branchIds)) {
throw new ValidationError("branchIds must be an array");
}

View File

@@ -38,7 +38,7 @@ async function addClipping(req: Request) {
if (!clippingNote) {
clippingNote = noteService.createNewNote({
parentNoteId: clipperInbox.noteId,
title: title,
title,
content: "",
type: "text"
}).note;
@@ -188,7 +188,7 @@ export function processContent(images: Image[], note: BNote, content: string) {
return rewrittenContent;
}
function openNote(req: Request) {
function openNote(req: Request<{ noteId: string }>) {
if (utils.isElectron) {
ws.sendMessageToAllClients({
type: "openNote",
@@ -198,11 +198,11 @@ function openNote(req: Request) {
return {
result: "ok"
};
} else {
return {
result: "open-in-browser"
};
}
}
return {
result: "open-in-browser"
};
}
function handshake() {
@@ -212,7 +212,7 @@ function handshake() {
};
}
async function findNotesByUrl(req: Request) {
async function findNotesByUrl(req: Request<{ noteUrl: string }>) {
const pageUrl = req.params.noteUrl;
const clipperInbox = await getClipperInboxNote();
const foundPage = findClippingNote(clipperInbox, pageUrl, null);

View File

@@ -1,29 +1,28 @@
"use strict";
import type { Request } from "express";
import cloningService from "../../services/cloning.js";
function cloneNoteToBranch(req: Request) {
function cloneNoteToBranch(req: Request<{ noteId: string; parentBranchId: string }>) {
const { noteId, parentBranchId } = req.params;
const { prefix } = req.body;
return cloningService.cloneNoteToBranch(noteId, parentBranchId, prefix);
}
function cloneNoteToParentNote(req: Request) {
function cloneNoteToParentNote(req: Request<{ noteId: string; parentNoteId: string }>) {
const { noteId, parentNoteId } = req.params;
const { prefix } = req.body;
return cloningService.cloneNoteToParentNote(noteId, parentNoteId, prefix);
}
function cloneNoteAfter(req: Request) {
function cloneNoteAfter(req: Request<{ noteId: string; afterBranchId: string }>) {
const { noteId, afterBranchId } = req.params;
return cloningService.cloneNoteAfter(noteId, afterBranchId);
}
function toggleNoteInParent(req: Request) {
function toggleNoteInParent(req: Request<{ noteId: string; parentNoteId: string; present: string }>) {
const { noteId, parentNoteId, present } = req.params;
return cloningService.toggleNoteInParent(present === "true", noteId, parentNoteId);

View File

@@ -1,6 +1,7 @@
import type { Request } from "express";
import etapiTokenService from "../../services/etapi_tokens.js";
import { EtapiToken, PostTokensResponse } from "@triliumnext/commons";
import type { Request } from "express";
import etapiTokenService from "../../services/etapi_tokens.js";
function getTokens() {
const tokens = etapiTokenService.getTokens();
@@ -14,11 +15,11 @@ function createToken(req: Request) {
return etapiTokenService.createToken(req.body.tokenName) satisfies PostTokensResponse;
}
function patchToken(req: Request) {
function patchToken(req: Request<{ etapiTokenId: string }>) {
etapiTokenService.renameToken(req.params.etapiTokenId, req.body.name);
}
function deleteToken(req: Request) {
function deleteToken(req: Request<{ etapiTokenId: string }>) {
etapiTokenService.deleteToken(req.params.etapiTokenId);
}

View File

@@ -1,17 +1,16 @@
"use strict";
import zipExportService from "../../services/export/zip.js";
import singleExportService from "../../services/export/single.js";
import opmlExportService from "../../services/export/opml.js";
import becca from "../../becca/becca.js";
import TaskContext from "../../services/task_context.js";
import log from "../../services/log.js";
import NotFoundError from "../../errors/not_found_error.js";
import type { Request, Response } from "express";
import becca from "../../becca/becca.js";
import NotFoundError from "../../errors/not_found_error.js";
import ValidationError from "../../errors/validation_error.js";
import opmlExportService from "../../services/export/opml.js";
import singleExportService from "../../services/export/single.js";
import zipExportService from "../../services/export/zip.js";
import log from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
function exportBranch(req: Request, res: Response) {
function exportBranch(req: Request<{ branchId: string; type: string; format: string; version: string; taskId: string }>, res: Response) {
const { branchId, type, format, version, taskId } = req.params;
const branch = becca.getBranch(branchId);

View File

@@ -1,5 +1,3 @@
import chokidar from "chokidar";
import type { Request, Response } from "express";
import fs from "fs";
@@ -17,7 +15,7 @@ import protectedSessionService from "../../services/protected_session.js";
import utils from "../../services/utils.js";
import ws from "../../services/ws.js";
function updateFile(req: Request) {
function updateFile(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
const file = req.file;
@@ -46,7 +44,7 @@ function updateFile(req: Request) {
};
}
function updateAttachment(req: Request) {
function updateAttachment(req: Request<{ attachmentId: string }>) {
const attachment = becca.getAttachmentOrThrow(req.params.attachmentId);
const file = req.file;
if (!file) {
@@ -103,20 +101,20 @@ function downloadAttachmentInt(attachmentId: string, res: Response, contentDispo
return downloadData(attachment, res, contentDisposition);
}
const downloadFile = (req: Request, res: Response) => downloadNoteInt(req.params.noteId, res, true);
const openFile = (req: Request, res: Response) => downloadNoteInt(req.params.noteId, res, false);
const downloadFile = (req: Request<{ noteId: string }>, res: Response) => downloadNoteInt(req.params.noteId, res, true);
const openFile = (req: Request<{ noteId: string }>, res: Response) => downloadNoteInt(req.params.noteId, res, false);
const downloadAttachment = (req: Request, res: Response) => downloadAttachmentInt(req.params.attachmentId, res, true);
const openAttachment = (req: Request, res: Response) => downloadAttachmentInt(req.params.attachmentId, res, false);
const downloadAttachment = (req: Request<{ attachmentId: string }>, res: Response) => downloadAttachmentInt(req.params.attachmentId, res, true);
const openAttachment = (req: Request<{ attachmentId: string }>, res: Response) => downloadAttachmentInt(req.params.attachmentId, res, false);
function fileContentProvider(req: Request) {
function fileContentProvider(req: Request<{ noteId: string }>) {
// Read the file name from route params.
const note = becca.getNoteOrThrow(req.params.noteId);
return streamContent(note.getContent(), note.getFileName(), note.mime);
}
function attachmentContentProvider(req: Request) {
function attachmentContentProvider(req: Request<{ attachmentId: string }>) {
// Read the file name from route params.
const attachment = becca.getAttachmentOrThrow(req.params.attachmentId);
@@ -149,7 +147,7 @@ async function streamContent(content: string | Buffer, fileName: string, mimeTyp
};
}
function saveNoteToTmpDir(req: Request) {
function saveNoteToTmpDir(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
const fileName = note.getFileName();
const content = note.getContent();
@@ -157,7 +155,7 @@ function saveNoteToTmpDir(req: Request) {
return saveToTmpDir(fileName, content, "notes", note.noteId);
}
function saveAttachmentToTmpDir(req: Request) {
function saveAttachmentToTmpDir(req: Request<{ attachmentId: string }>) {
const attachment = becca.getAttachmentOrThrow(req.params.attachmentId);
const fileName = attachment.getFileName();
const content = attachment.getContent();
@@ -205,7 +203,7 @@ function saveToTmpDir(fileName: string, content: string | Buffer, entityType: st
};
}
function uploadModifiedFileToNote(req: Request) {
function uploadModifiedFileToNote(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const { filePath } = req.body;
@@ -228,7 +226,7 @@ function uploadModifiedFileToNote(req: Request) {
note.setContent(fileContent);
}
function uploadModifiedFileToAttachment(req: Request) {
function uploadModifiedFileToAttachment(req: Request<{ attachmentId: string }>) {
const { attachmentId } = req.params;
const { filePath } = req.body;

View File

@@ -1,20 +1,19 @@
"use strict";
import imageService from "../../services/image.js";
import becca from "../../becca/becca.js";
import fs from "fs";
import type { Request, Response } from "express";
import fs from "fs";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import type BRevision from "../../becca/entities/brevision.js";
import imageService from "../../services/image.js";
import { RESOURCE_DIR } from "../../services/resource_dir.js";
function returnImageFromNote(req: Request, res: Response) {
function returnImageFromNote(req: Request<{ noteId: string }>, res: Response) {
const image = becca.getNote(req.params.noteId);
return returnImageInt(image, res);
}
function returnImageFromRevision(req: Request, res: Response) {
function returnImageFromRevision(req: Request<{ revisionId: string }>, res: Response) {
const image = becca.getRevision(req.params.revisionId);
return returnImageInt(image, res);
@@ -61,7 +60,7 @@ export function renderSvgAttachment(image: BNote | BRevision, res: Response, att
res.send(svg);
}
function returnAttachedImage(req: Request, res: Response) {
function returnAttachedImage(req: Request<{ attachmentId: string }>, res: Response) {
const attachment = becca.getAttachment(req.params.attachmentId);
if (!attachment) {
@@ -78,7 +77,7 @@ function returnAttachedImage(req: Request, res: Response) {
res.send(attachment.getContent());
}
function updateImage(req: Request) {
function updateImage(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const { file } = req;

View File

@@ -1,21 +1,20 @@
"use strict";
import enexImportService from "../../services/import/enex.js";
import opmlImportService from "../../services/import/opml.js";
import zipImportService from "../../services/import/zip.js";
import singleImportService from "../../services/import/single.js";
import cls from "../../services/cls.js";
import type { Request } from "express";
import path from "path";
import becca from "../../becca/becca.js";
import beccaLoader from "../../becca/becca_loader.js";
import type BNote from "../../becca/entities/bnote.js";
import ValidationError from "../../errors/validation_error.js";
import cls from "../../services/cls.js";
import enexImportService from "../../services/import/enex.js";
import opmlImportService from "../../services/import/opml.js";
import singleImportService from "../../services/import/single.js";
import zipImportService from "../../services/import/zip.js";
import log from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import type BNote from "../../becca/entities/bnote.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
async function importNotesToBranch(req: Request) {
async function importNotesToBranch(req: Request<{ parentNoteId: string }>) {
const { parentNoteId } = req.params;
const { taskId, last } = req.body;
@@ -88,7 +87,7 @@ async function importNotesToBranch(req: Request) {
setTimeout(
() =>
taskContext.taskSucceeded({
parentNoteId: parentNoteId,
parentNoteId,
importedNoteId: note?.noteId
}),
1000
@@ -101,7 +100,7 @@ async function importNotesToBranch(req: Request) {
return note.getPojo();
}
function importAttachmentsToNote(req: Request) {
function importAttachmentsToNote(req: Request<{ parentNoteId: string }>) {
const { parentNoteId } = req.params;
const { taskId, last } = req.body;
@@ -138,7 +137,7 @@ function importAttachmentsToNote(req: Request) {
setTimeout(
() =>
taskContext.taskSucceeded({
parentNoteId: parentNoteId
parentNoteId
}),
1000
);

View File

@@ -1,11 +1,12 @@
"use strict";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import type BAttribute from "../../becca/entities/battribute.js";
import { BacklinkCountResponse, BacklinksResponse } from "@triliumnext/commons";
import type { Request } from "express";
import { HTMLElement, parse, TextNode } from "node-html-parser";
import { BacklinkCountResponse, BacklinksResponse } from "@triliumnext/commons";
import becca from "../../becca/becca.js";
import type BAttribute from "../../becca/entities/battribute.js";
import type BNote from "../../becca/entities/bnote.js";
interface TreeLink {
sourceNoteId: string;
@@ -97,7 +98,7 @@ function getNeighbors(note: BNote, depth: number): string[] {
return retNoteIds;
}
function getLinkMap(req: Request) {
function getLinkMap(req: Request<{ noteId: string }>) {
const mapRootNote = becca.getNoteOrThrow(req.params.noteId);
// if the map root itself has "excludeFromNoteMap" attribute (journal typically) then there wouldn't be anything
@@ -156,9 +157,9 @@ function getLinkMap(req: Request) {
return false;
} else if (excludeRelations.has(rel.name)) {
return false;
} else {
return true;
}
}
return true;
})
.map((rel) => ({
id: `${rel.noteId}-${rel.name}-${rel.value}`,
@@ -168,13 +169,13 @@ function getLinkMap(req: Request) {
}));
return {
notes: notes,
notes,
noteIdToDescendantCountMap: buildDescendantCountMap(noteIdsArray),
links: links
links
};
}
function getTreeMap(req: Request) {
function getTreeMap(req: Request<{ noteId: string }>) {
const mapRootNote = becca.getNoteOrThrow(req.params.noteId);
// if the map root itself has "excludeFromNoteMap" (journal typically) then there wouldn't be anything to display,
// so we'll just ignore it
@@ -223,9 +224,9 @@ function getTreeMap(req: Request) {
updateDescendantCountMapForSearch(noteIdToDescendantCountMap, subtree.relationships);
return {
notes: notes,
noteIdToDescendantCountMap: noteIdToDescendantCountMap,
links: links
notes,
noteIdToDescendantCountMap,
links
};
}
@@ -350,7 +351,7 @@ function getFilteredBacklinks(note: BNote): BAttribute[] {
);
}
function getBacklinkCount(req: Request) {
function getBacklinkCount(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const note = becca.getNoteOrThrow(noteId);
@@ -360,7 +361,7 @@ function getBacklinkCount(req: Request) {
} satisfies BacklinkCountResponse;
}
function getBacklinks(req: Request): BacklinksResponse {
function getBacklinks(req: Request<{ noteId: string }>): BacklinksResponse {
const { noteId } = req.params;
const note = becca.getNoteOrThrow(noteId);

View File

@@ -1,18 +1,19 @@
"use strict";
import noteService from "../../services/notes.js";
import eraseService from "../../services/erase.js";
import treeService from "../../services/tree.js";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
import log from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import type BBranch from "../../becca/entities/bbranch.js";
import ValidationError from "../../errors/validation_error.js";
import blobService from "../../services/blob.js";
import type { Request } from "express";
import type BBranch from "../../becca/entities/bbranch.js";
import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
import eraseService from "../../services/erase.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import sql from "../../services/sql.js";
import TaskContext from "../../services/task_context.js";
import treeService from "../../services/tree.js";
import utils from "../../services/utils.js";
/**
* @swagger
@@ -39,7 +40,7 @@ import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, Metadata
* - session: []
* tags: ["data"]
*/
function getNote(req: Request) {
function getNote(req: Request<{ noteId: string }>) {
return becca.getNoteOrThrow(req.params.noteId);
}
@@ -66,7 +67,7 @@ function getNote(req: Request) {
* - session: []
* tags: ["data"]
*/
function getNoteBlob(req: Request) {
function getNoteBlob(req: Request<{ noteId: string }>) {
return blobService.getBlobPojo("notes", req.params.noteId);
}
@@ -93,7 +94,7 @@ function getNoteBlob(req: Request) {
* - session: []
* tags: ["data"]
*/
function getNoteMetadata(req: Request) {
function getNoteMetadata(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
return {
@@ -126,7 +127,7 @@ function createNote(req: Request) {
} satisfies CreateChildrenResponse;
}
function updateNoteData(req: Request) {
function updateNoteData(req: Request<{ noteId: string }>) {
const { content, attachments } = req.body;
const { noteId } = req.params;
@@ -170,7 +171,7 @@ function updateNoteData(req: Request) {
* - session: []
* tags: ["data"]
*/
function deleteNote(req: Request) {
function deleteNote(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const taskId = req.query.taskId;
const eraseNotes = req.query.eraseNotes === "true";
@@ -197,7 +198,7 @@ function deleteNote(req: Request) {
}
}
function undeleteNote(req: Request) {
function undeleteNote(req: Request<{ noteId: string }>) {
const taskContext = TaskContext.getInstance(utils.randomString(10), "undeleteNotes", null);
noteService.undeleteNote(req.params.noteId, taskContext);
@@ -205,7 +206,7 @@ function undeleteNote(req: Request) {
taskContext.taskSucceeded(null);
}
function sortChildNotes(req: Request) {
function sortChildNotes(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const { sortBy, sortDirection, foldersFirst, sortNatural, sortLocale } = req.body;
@@ -216,7 +217,7 @@ function sortChildNotes(req: Request) {
treeService.sortNotes(noteId, sortBy, reverse, foldersFirst, sortNatural, sortLocale);
}
function protectNote(req: Request) {
function protectNote(req: Request<{ noteId: string; isProtected: string }>) {
const noteId = req.params.noteId;
const note = becca.notes[noteId];
const protect = !!parseInt(req.params.isProtected);
@@ -229,7 +230,7 @@ function protectNote(req: Request) {
taskContext.taskSucceeded(null);
}
function setNoteTypeMime(req: Request) {
function setNoteTypeMime(req: Request<{ noteId: string }>) {
// can't use [] destructuring because req.params is not iterable
const { noteId } = req.params;
const { type, mime } = req.body;
@@ -240,7 +241,7 @@ function setNoteTypeMime(req: Request) {
note.save();
}
function changeTitle(req: Request) {
function changeTitle(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const title = req.body.title;
@@ -267,7 +268,7 @@ function changeTitle(req: Request) {
return note;
}
function duplicateSubtree(req: Request) {
function duplicateSubtree(req: Request<{ noteId: string; parentNoteId: string }>) {
const { noteId, parentNoteId } = req.params;
return noteService.duplicateSubtree(noteId, parentNoteId);
@@ -342,7 +343,7 @@ function getDeleteNotesPreview(req: Request) {
} satisfies DeleteNotesPreview;
}
function forceSaveRevision(req: Request) {
function forceSaveRevision(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const note = becca.getNoteOrThrow(noteId);
@@ -353,7 +354,7 @@ function forceSaveRevision(req: Request) {
note.saveRevision();
}
function convertNoteToAttachment(req: Request) {
function convertNoteToAttachment(req: Request<{ noteId: string }>) {
const { noteId } = req.params;
const note = becca.getNoteOrThrow(noteId);

View File

@@ -128,7 +128,7 @@ function getOptions() {
return resultMap;
}
function updateOption(req: Request) {
function updateOption(req: Request<{ name: string; value: string }>) {
const { name, value } = req.params;
if (!update(name, value)) {

View File

@@ -1,13 +1,12 @@
"use strict";
import sql from "../../services/sql.js";
import protectedSessionService from "../../services/protected_session.js";
import noteService from "../../services/notes.js";
import becca from "../../becca/becca.js";
import type { Request } from "express";
import type { RecentChangeRow } from "@triliumnext/commons";
import type { Request } from "express";
function getRecentChanges(req: Request) {
import becca from "../../becca/becca.js";
import noteService from "../../services/notes.js";
import protectedSessionService from "../../services/protected_session.js";
import sql from "../../services/sql.js";
function getRecentChanges(req: Request<{ ancestorNoteId: string }>) {
const { ancestorNoteId } = req.params;
let recentChanges: RecentChangeRow[] = [];

View File

@@ -1,18 +1,19 @@
"use strict";
import beccaService from "../../becca/becca_service.js";
import utils from "../../services/utils.js";
import sql from "../../services/sql.js";
import cls from "../../services/cls.js";
import path from "path";
import becca from "../../becca/becca.js";
import blobService from "../../services/blob.js";
import eraseService from "../../services/erase.js";
import type { Request, Response } from "express";
import type BRevision from "../../becca/entities/brevision.js";
import type BNote from "../../becca/entities/bnote.js";
import type { NotePojo } from "../../becca/becca-interface.js";
import { EditedNotesResponse, RevisionItem, RevisionPojo, RevisionRow } from "@triliumnext/commons";
import type { Request, Response } from "express";
import path from "path";
import becca from "../../becca/becca.js";
import beccaService from "../../becca/becca_service.js";
import type { NotePojo } from "../../becca/becca-interface.js";
import type BNote from "../../becca/entities/bnote.js";
import type BRevision from "../../becca/entities/brevision.js";
import blobService from "../../services/blob.js";
import cls from "../../services/cls.js";
import eraseService from "../../services/erase.js";
import sql from "../../services/sql.js";
import utils from "../../services/utils.js";
interface NotePath {
noteId: string;
@@ -26,13 +27,13 @@ interface NotePojoWithNotePath extends NotePojo {
notePath?: string[] | null;
}
function getRevisionBlob(req: Request) {
function getRevisionBlob(req: Request<{ revisionId: string }>) {
const preview = req.query.preview === "true";
return blobService.getBlobPojo("revisions", req.params.revisionId, { preview });
}
function getRevisions(req: Request) {
function getRevisions(req: Request<{ noteId: string }>) {
return becca.getRevisionsFromQuery(
`
SELECT revisions.*,
@@ -45,7 +46,7 @@ function getRevisions(req: Request) {
) satisfies RevisionItem[];
}
function getRevision(req: Request) {
function getRevision(req: Request<{ revisionId: string }>) {
const revision = becca.getRevisionOrThrow(req.params.revisionId);
if (revision.type === "file") {
@@ -85,7 +86,7 @@ function getRevisionFilename(revision: BRevision) {
return filename;
}
function downloadRevision(req: Request, res: Response) {
function downloadRevision(req: Request<{ revisionId: string }>, res: Response) {
const revision = becca.getRevisionOrThrow(req.params.revisionId);
if (!revision.isContentAvailable()) {
@@ -100,13 +101,13 @@ function downloadRevision(req: Request, res: Response) {
res.send(revision.getContent());
}
function eraseAllRevisions(req: Request) {
function eraseAllRevisions(req: Request<{ noteId: string }>) {
const revisionIdsToErase = sql.getColumn<string>("SELECT revisionId FROM revisions WHERE noteId = ?", [req.params.noteId]);
eraseService.eraseRevisions(revisionIdsToErase);
}
function eraseRevision(req: Request) {
function eraseRevision(req: Request<{ revisionId: string }>) {
eraseService.eraseRevisions([req.params.revisionId]);
}
@@ -117,7 +118,7 @@ function eraseAllExcessRevisions() {
});
}
function restoreRevision(req: Request) {
function restoreRevision(req: Request<{ revisionId: string }>) {
const revision = becca.getRevision(req.params.revisionId);
if (revision) {
@@ -166,7 +167,7 @@ function getEditedNotesOnDate(req: Request) {
)
ORDER BY isDeleted
LIMIT 50`,
{ date: `${req.params.date}%` }
{ date: `${req.params.date}%` }
);
let notes = becca.getNotes(noteIds, true);
@@ -204,7 +205,7 @@ function getNotePathData(note: BNote): NotePath | undefined {
return {
noteId: note.noteId,
branchId: branchId,
branchId,
title: noteTitle,
notePath: retPath,
path: retPath.join("/")

View File

@@ -1,11 +1,12 @@
"use strict";
import scriptService, { type Bundle } from "../../services/script.js";
import attributeService from "../../services/attributes.js";
import becca from "../../becca/becca.js";
import syncService from "../../services/sync.js";
import sql from "../../services/sql.js";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import attributeService from "../../services/attributes.js";
import scriptService, { type Bundle } from "../../services/script.js";
import sql from "../../services/sql.js";
import syncService from "../../services/sync.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
interface ScriptBody {
@@ -43,7 +44,7 @@ async function exec(req: Request) {
}
}
function run(req: Request) {
function run(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
const result = scriptService.executeNote(note, { originEntity: note });
@@ -71,23 +72,23 @@ function getStartupBundles(req: Request) {
if (!process.env.TRILIUM_SAFE_MODE) {
if (req.query.mobile === "true") {
return getBundlesWithLabel("run", "mobileStartup");
} else {
return getBundlesWithLabel("run", "frontendStartup");
}
} else {
return [];
}
}
return getBundlesWithLabel("run", "frontendStartup");
}
return [];
}
function getWidgetBundles() {
if (!process.env.TRILIUM_SAFE_MODE) {
return getBundlesWithLabel("widget");
} else {
return [];
}
}
return [];
}
function getRelationBundles(req: Request) {
function getRelationBundles(req: Request<{ noteId: string, relationName: string }>) {
const noteId = req.params.noteId;
const note = becca.getNoteOrThrow(noteId);
const relationName = req.params.relationName;
@@ -116,7 +117,7 @@ function getRelationBundles(req: Request) {
return bundles;
}
function getBundle(req: Request) {
function getBundle(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
const { script, params } = req.body ?? {};

View File

@@ -1,19 +1,19 @@
"use strict";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import SearchContext from "../../services/search/search_context.js";
import searchService, { EMPTY_RESULT, type SearchNoteResult } from "../../services/search/services/search.js";
import beccaService from "../../becca/becca_service.js";
import ValidationError from "../../errors/validation_error.js";
import attributeFormatter from "../../services/attribute_formatter.js";
import bulkActionService from "../../services/bulk_actions.js";
import cls from "../../services/cls.js";
import attributeFormatter from "../../services/attribute_formatter.js";
import ValidationError from "../../errors/validation_error.js";
import type SearchResult from "../../services/search/search_result.js";
import hoistedNoteService from "../../services/hoisted_note.js";
import beccaService from "../../becca/becca_service.js";
import SearchContext from "../../services/search/search_context.js";
import type SearchResult from "../../services/search/search_result.js";
import searchService, { EMPTY_RESULT, type SearchNoteResult } from "../../services/search/services/search.js";
function searchFromNote(req: Request): SearchNoteResult {
function searchFromNote(req: Request<{ noteId: string }>): SearchNoteResult {
const note = becca.getNoteOrThrow(req.params.noteId);
if (!note) {
@@ -28,7 +28,7 @@ function searchFromNote(req: Request): SearchNoteResult {
return searchService.searchFromNote(note);
}
function searchAndExecute(req: Request) {
function searchAndExecute(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
if (!note) {
@@ -45,7 +45,7 @@ function searchAndExecute(req: Request) {
bulkActionService.executeActionsFromNote(note, searchResultNoteIds);
}
function quickSearch(req: Request) {
function quickSearch(req: Request<{ searchString: string }>) {
const { searchString } = req.params;
const searchContext = new SearchContext({
@@ -82,7 +82,7 @@ function quickSearch(req: Request) {
highlightedContentSnippet: result.highlightedContentSnippet,
attributeSnippet: result.attributeSnippet,
highlightedAttributeSnippet: result.highlightedAttributeSnippet,
icon: icon
icon
};
});
@@ -90,12 +90,12 @@ function quickSearch(req: Request) {
return {
searchResultNoteIds: resultNoteIds,
searchResults: searchResults,
searchResults,
error: searchContext.getError()
};
}
function search(req: Request) {
function search(req: Request<{ searchString: string }>) {
const { searchString } = req.params;
const searchContext = new SearchContext({

View File

@@ -1,12 +1,10 @@
"use strict";
import { SimilarNoteResponse } from "@triliumnext/commons";
import type { Request } from "express";
import similarityService from "../../becca/similarity.js";
import becca from "../../becca/becca.js";
import { SimilarNoteResponse } from "@triliumnext/commons";
import similarityService from "../../becca/similarity.js";
async function getSimilarNotes(req: Request) {
async function getSimilarNotes(req: Request<{ noteId: string }>) {
const noteId = req.params.noteId;
const _note = becca.getNoteOrThrow(noteId);

View File

@@ -6,33 +6,33 @@ import dateNoteService from "../../services/date_notes.js";
import specialNotesService, { type LauncherType } from "../../services/special_notes.js";
import sql from "../../services/sql.js";
function getInboxNote(req: Request) {
function getInboxNote(req: Request<{ date: string }>) {
return specialNotesService.getInboxNote(req.params.date);
}
function getDayNote(req: Request) {
function getDayNote(req: Request<{ date: string }>) {
const calendarRootId = req.query.calendarRootId;
const calendarRoot = typeof calendarRootId === "string" ? becca.getNoteOrThrow(calendarRootId) : null;
return dateNoteService.getDayNote(req.params.date, calendarRoot);
}
function getWeekFirstDayNote(req: Request) {
function getWeekFirstDayNote(req: Request<{ date: string }>) {
return dateNoteService.getWeekFirstDayNote(req.params.date);
}
function getWeekNote(req: Request) {
function getWeekNote(req: Request<{ week: string }>) {
return dateNoteService.getWeekNote(req.params.week);
}
function getMonthNote(req: Request) {
function getMonthNote(req: Request<{ month: string }>) {
return dateNoteService.getMonthNote(req.params.month);
}
function getQuarterNote(req: Request) {
function getQuarterNote(req: Request<{ quarter: string }>) {
return dateNoteService.getQuarterNote(req.params.quarter);
}
function getYearNote(req: Request) {
function getYearNote(req: Request<{ year: string }>) {
return dateNoteService.getYearNote(req.params.year);
}
@@ -90,7 +90,7 @@ function getHoistedNote() {
return becca.getNote(cls.getHoistedNoteId());
}
function createLauncher(req: Request) {
function createLauncher(req: Request<{ parentNoteId: string, launcherType: string }>) {
return specialNotesService.createLauncher({
parentNoteId: req.params.parentNoteId,
// TODO: Validate the parameter
@@ -98,7 +98,7 @@ function createLauncher(req: Request) {
});
}
function resetLauncher(req: Request) {
function resetLauncher(req: Request<{ noteId: string }>) {
return specialNotesService.resetLauncher(req.params.noteId);
}

View File

@@ -1,9 +1,8 @@
"use strict";
import sql from "../../services/sql.js";
import becca from "../../becca/becca.js";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js";
import sql from "../../services/sql.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
interface Table {
@@ -25,7 +24,7 @@ function getSchema() {
return tables;
}
function execute(req: Request) {
function execute(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
const content = note.getContent();

View File

@@ -1,7 +1,8 @@
import sql from "../../services/sql.js";
import becca from "../../becca/becca.js";
import type { Request } from "express";
import { NoteSizeResponse, SubtreeSizeResponse } from "@triliumnext/commons";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import sql from "../../services/sql.js";
function getNoteSize(req: Request) {
const { noteId } = req.params;
@@ -26,7 +27,7 @@ function getNoteSize(req: Request) {
} satisfies NoteSizeResponse;
}
function getSubtreeSize(req: Request) {
function getSubtreeSize(req: Request<{ noteId: string }>) {
const note = becca.getNoteOrThrow(req.params.noteId);
const subTreeNoteIds = note.getSubtreeNoteIds();

View File

@@ -1,21 +1,20 @@
"use strict";
import syncService from "../../services/sync.js";
import syncUpdateService from "../../services/sync_update.js";
import entityChangesService from "../../services/entity_changes.js";
import sql from "../../services/sql.js";
import sqlInit from "../../services/sql_init.js";
import optionService from "../../services/options.js";
import contentHashService from "../../services/content_hash.js";
import log from "../../services/log.js";
import syncOptions from "../../services/sync_options.js";
import utils, { safeExtractMessageAndStackFromError } from "../../services/utils.js";
import ws from "../../services/ws.js";
import { type EntityChange, SyncTestResponse } from "@triliumnext/commons";
import type { Request } from "express";
import { t } from "i18next";
import ValidationError from "../../errors/validation_error.js";
import consistencyChecksService from "../../services/consistency_checks.js";
import { t } from "i18next";
import { SyncTestResponse, type EntityChange } from "@triliumnext/commons";
import contentHashService from "../../services/content_hash.js";
import entityChangesService from "../../services/entity_changes.js";
import log from "../../services/log.js";
import optionService from "../../services/options.js";
import sql from "../../services/sql.js";
import sqlInit from "../../services/sql_init.js";
import syncService from "../../services/sync.js";
import syncOptions from "../../services/sync_options.js";
import syncUpdateService from "../../services/sync_update.js";
import utils, { safeExtractMessageAndStackFromError } from "../../services/utils.js";
import ws from "../../services/ws.js";
async function testSync(): Promise<SyncTestResponse> {
try {
@@ -287,10 +286,10 @@ function update(req: Request) {
if (pageIndex !== pageCount - 1) {
return;
} else {
body = JSON.parse(partialRequests[requestId].payload);
delete partialRequests[requestId];
}
body = JSON.parse(partialRequests[requestId].payload);
delete partialRequests[requestId];
}
const { entities, instanceId } = body;
@@ -314,7 +313,7 @@ function syncFinished() {
sqlInit.setDbAsInitialized();
}
function queueSector(req: Request) {
function queueSector(req: Request<{ entityName: string; sector: string }>) {
const entityName = utils.sanitizeSqlIdentifier(req.params.entityName);
const sector = utils.sanitizeSqlIdentifier(req.params.sector);

View File

@@ -2,7 +2,7 @@ import { dayjs } from "@triliumnext/commons";
import type { Application } from "express";
import { SessionData } from "express-session";
import supertest, { type Response } from "supertest";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import cls from "../services/cls.js";
import { type SQLiteSessionStore } from "./session_parser.js";
@@ -20,6 +20,10 @@ describe("Login Route test", () => {
({ sessionStore, CLEAN_UP_INTERVAL } = (await import("./session_parser.js")));
});
afterAll(() => {
vi.useRealTimers();
});
it("should return the login page, when using a GET request", async () => {
// RegExp for login page specific string in HTML

View File

@@ -1,15 +1,17 @@
import express, { type RequestHandler } from "express";
import type { ParamsDictionary } from "express-serve-static-core";
import multer from "multer";
import log from "../services/log.js";
import cls from "../services/cls.js";
import sql from "../services/sql.js";
import entityChangesService from "../services/entity_changes.js";
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import NotFoundError from "../errors/not_found_error.js";
import ValidationError from "../errors/validation_error.js";
import auth from "../services/auth.js";
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
import cls from "../services/cls.js";
import entityChangesService from "../services/entity_changes.js";
import log from "../services/log.js";
import sql from "../services/sql.js";
import { safeExtractMessageAndStackFromError } from "../services/utils.js";
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
const MAX_ALLOWED_FILE_SIZE_MB = 250;
export const router = express.Router();
@@ -20,8 +22,8 @@ type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options
export type ApiResultHandler = (req: express.Request, res: express.Response, result: unknown) => number;
type NotAPromise<T> = T & { then?: void };
export type ApiRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => unknown;
export type SyncRouteRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => NotAPromise<object> | number | string | void | null;
export type ApiRequestHandler<P extends ParamsDictionary> = (req: express.Request<P>, res: express.Response, next: express.NextFunction) => unknown;
export type SyncRouteRequestHandler<P extends ParamsDictionary> = (req: express.Request<P>, res: express.Response, next: express.NextFunction) => NotAPromise<object> | number | string | void | null;
/** Handling common patterns. If entity is not caught, serialization to JSON will fail */
function convertEntitiesToPojo(result: unknown) {
@@ -67,9 +69,9 @@ export function apiResultHandler(req: express.Request, res: express.Response, re
return send(res, statusCode, response);
} else if (result === undefined) {
return send(res, 204, "");
} else {
return send(res, 200, result);
}
return send(res, 200, result);
}
function send(res: express.Response, statusCode: number, response: unknown) {
@@ -81,34 +83,34 @@ function send(res: express.Response, statusCode: number, response: unknown) {
res.status(statusCode).send(response);
return response.length;
} else {
const json = JSON.stringify(response);
res.setHeader("Content-Type", "application/json");
res.status(statusCode).send(json);
return json.length;
}
const json = JSON.stringify(response);
res.setHeader("Content-Type", "application/json");
res.status(statusCode).send(json);
return json.length;
}
export function apiRoute(method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler) {
export function apiRoute<P extends ParamsDictionary>(method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler<P>) {
route(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
}
export function asyncApiRoute(method: HttpMethod, path: string, routeHandler: ApiRequestHandler) {
export function asyncApiRoute<P extends ParamsDictionary>(method: HttpMethod, path: string, routeHandler: ApiRequestHandler<P>) {
asyncRoute(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
}
export function route(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: SyncRouteRequestHandler, resultHandler: ApiResultHandler | null = null) {
export function route<P extends ParamsDictionary>(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: SyncRouteRequestHandler<P>, resultHandler: ApiResultHandler | null = null) {
internalRoute(method, path, middleware, routeHandler, resultHandler, true);
}
export function asyncRoute(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler, resultHandler: ApiResultHandler | null = null) {
export function asyncRoute<P extends ParamsDictionary>(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler<P>, resultHandler: ApiResultHandler | null = null) {
internalRoute(method, path, middleware, routeHandler, resultHandler, false);
}
function internalRoute(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler, resultHandler: ApiResultHandler | null = null, transactional: boolean) {
router[method](path, ...(middleware as express.Handler[]), (req: express.Request, res: express.Response, next: express.NextFunction) => {
function internalRoute<P extends ParamsDictionary>(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler<P>, resultHandler: ApiResultHandler | null = null, transactional: boolean) {
router[method](path, ...(middleware as express.Handler[]), (req: express.Request<P>, res: express.Response, next: express.NextFunction) => {
const start = Date.now();
try {
@@ -193,7 +195,7 @@ export function createUploadMiddleware(): RequestHandler {
const uploadMiddleware = createUploadMiddleware();
export const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: express.Response, next: express.NextFunction) {
uploadMiddleware(req, res, function (err) {
uploadMiddleware(req, res, (err) => {
if (err?.code === "LIMIT_FILE_SIZE") {
res.setHeader("Content-Type", "text/plain").status(400).send(`Cannot upload file because it excceeded max allowed file size of ${MAX_ALLOWED_FILE_SIZE_MB} MiB`);
} else {

View File

@@ -10,14 +10,13 @@ import etapiBackupRoute from "../etapi/backup.js";
import etapiBranchRoutes from "../etapi/branches.js";
import etapiMetricsRoute from "../etapi/metrics.js";
import etapiNoteRoutes from "../etapi/notes.js";
import etapiRevisionsRoutes from "../etapi/revisions.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";
import shareRoutes from "../share/routes.js";
import appInfoRoute from "./api/app_info.js";
import attachmentsApiRoute from "./api/attachments.js";
import attributesRoute from "./api/attributes.js";
@@ -35,12 +34,10 @@ import fontsRoute from "./api/fonts.js";
import imageRoute from "./api/image.js";
import importRoute from "./api/import.js";
import keysRoute from "./api/keys.js";
import loginApiRoute from "./api/login.js";
import metricsRoute from "./api/metrics.js";
import noteMapRoute from "./api/note_map.js";
import notesApiRoute from "./api/notes.js";
import optionsApiRoute from "./api/options.js";
import otherRoute from "./api/other.js";
import passwordApiRoute from "./api/password.js";

View File

@@ -113,11 +113,13 @@ const sessionParser: express.RequestHandler = session({
store: sessionStore
});
setInterval(() => {
// Clean up expired sesions.
const now = Date.now();
const result = sql.execute(/*sql*/`DELETE FROM sessions WHERE expires < ?`, now);
console.log("Cleaning up expired sessions: ", result.changes);
}, CLEAN_UP_INTERVAL);
export function startSessionCleanup() {
setInterval(() => {
// Clean up expired sessions.
const now = Date.now();
const result = sql.execute(/*sql*/`DELETE FROM sessions WHERE expires < ?`, now);
console.log("Cleaning up expired sessions: ", result.changes);
}, CLEAN_UP_INTERVAL);
}
export default sessionParser;

View File

@@ -1,11 +1,12 @@
import path from "path";
import build from "./build.js";
import packageJson from "../../package.json" with { type: "json" };
import dataDir from "./data_dir.js";
import { AppInfo } from "@triliumnext/commons";
import path from "path";
import packageJson from "../../package.json" with { type: "json" };
import build from "./build.js";
import dataDir from "./data_dir.js";
const APP_DB_VERSION = 234;
const SYNC_VERSION = 36;
const SYNC_VERSION = 37;
const CLIPPER_PROTOCOL_VERSION = "1.0";
export default {

View File

@@ -953,12 +953,14 @@ function runEntityChangesChecks() {
consistencyChecks.findEntityChangeIssues();
}
sqlInit.dbReady.then(() => {
setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000);
export function startConsistencyChecks() {
sqlInit.dbReady.then(() => {
setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000);
// kickoff checks soon after startup (to not block the initial load)
setTimeout(cls.wrap(runPeriodicChecks), 4 * 1000);
});
// kickoff checks soon after startup (to not block the initial load)
setTimeout(cls.wrap(runPeriodicChecks), 4 * 1000);
});
}
export default {
runOnDemandChecks,

View File

@@ -24,6 +24,7 @@ async function testImport(fileName: string, mimetype: string) {
const rootNote = becca.getNote("root");
if (!rootNote) {
reject("Missing root note.");
return;
}
const importedNote = single.importSingleFile(

View File

@@ -57,7 +57,7 @@ function importFile(taskContext: TaskContext<"importNotes">, file: File, parentN
const mime = mimeService.getMime(originalName) || file.mimetype;
const { note } = noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: getNoteTitle(originalName, mime === "application/pdf"),
title: getNoteTitle(originalName, mime === "application/pdf", { mime }),
content: file.buffer,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
type: "file",

View File

@@ -14,7 +14,8 @@ const noteTypes = [
{ type: "launcher", defaultMime: "" },
{ type: "doc", defaultMime: "" },
{ type: "contentWidget", defaultMime: "" },
{ type: "mindMap", defaultMime: "application/json" }
{ type: "mindMap", defaultMime: "application/json" },
{ type: "spreadsheet", defaultMime: "application/json" }
];
function getDefaultMimeForNoteType(typeName: string) {

View File

@@ -35,39 +35,41 @@ function runNotesWithLabel(runAttrValue: string) {
}
}
// If the database is already initialized, we need to check the hidden subtree. Otherwise, hidden subtree
// is also checked before importing the demo.zip, so no need to do it again.
if (sqlInit.isDbInitialized()) {
console.log("Checking hidden subtree.");
sqlInit.dbReady.then(() => cls.init(() => hiddenSubtreeService.checkHiddenSubtree()));
}
// Periodic checks.
sqlInit.dbReady.then(() => {
if (!process.env.TRILIUM_SAFE_MODE) {
setTimeout(
cls.wrap(() => runNotesWithLabel("backendStartup")),
10 * 1000
);
setInterval(
cls.wrap(() => runNotesWithLabel("hourly")),
3600 * 1000
);
setInterval(
cls.wrap(() => runNotesWithLabel("daily")),
24 * 3600 * 1000
);
setInterval(
cls.wrap(() => hiddenSubtreeService.checkHiddenSubtree()),
7 * 3600 * 1000
);
export function startScheduler() {
// If the database is already initialized, we need to check the hidden subtree. Otherwise, hidden subtree
// is also checked before importing the demo.zip, so no need to do it again.
if (sqlInit.isDbInitialized()) {
console.log("Checking hidden subtree.");
sqlInit.dbReady.then(() => cls.init(() => hiddenSubtreeService.checkHiddenSubtree()));
}
setInterval(() => checkProtectedSessionExpiration(), 30000);
});
// Periodic checks.
sqlInit.dbReady.then(() => {
if (!process.env.TRILIUM_SAFE_MODE) {
setTimeout(
cls.wrap(() => runNotesWithLabel("backendStartup")),
10 * 1000
);
setInterval(
cls.wrap(() => runNotesWithLabel("hourly")),
3600 * 1000
);
setInterval(
cls.wrap(() => runNotesWithLabel("daily")),
24 * 3600 * 1000
);
setInterval(
cls.wrap(() => hiddenSubtreeService.checkHiddenSubtree()),
7 * 3600 * 1000
);
}
setInterval(() => checkProtectedSessionExpiration(), 30000);
});
}
function checkProtectedSessionExpiration() {
const protectedSessionTimeout = options.getOptionInt("protectedSessionTimeout");

View File

@@ -50,7 +50,7 @@ async function initDbConnection() {
await migrationService.migrateIfNecessary();
sql.execute('CREATE TEMP TABLE "param_list" (`paramId` TEXT NOT NULL PRIMARY KEY)');
sql.execute('CREATE TEMP TABLE IF NOT EXISTS "param_list" (`paramId` TEXT NOT NULL PRIMARY KEY)');
sql.execute(`
CREATE TABLE IF NOT EXISTS "user_data"

View File

@@ -446,15 +446,17 @@ function getOutstandingPullCount() {
return outstandingPullCount;
}
becca_loader.beccaLoaded.then(() => {
setInterval(cls.wrap(sync), 60000);
export function startSyncTimer() {
becca_loader.beccaLoaded.then(() => {
setInterval(cls.wrap(sync), 60000);
// kickoff initial sync immediately, but should happen after initial consistency checks
setTimeout(cls.wrap(sync), 5000);
// kickoff initial sync immediately, but should happen after initial consistency checks
setTimeout(cls.wrap(sync), 5000);
// called just so ws.setLastSyncedPush() is called
getLastSyncedPush();
});
// called just so ws.setLastSyncedPush() is called
getLastSyncedPush();
});
}
export default {
sync,

View File

@@ -204,9 +204,13 @@ export function formatDownloadTitle(fileName: string, type: string | null, mime:
return `${fileNameBase}${getExtension()}`;
}
export function removeFileExtension(filePath: string) {
export function removeFileExtension(filePath: string, mime?: string) {
const extension = path.extname(filePath).toLowerCase();
if (mime?.startsWith("video/") || mime?.startsWith("audio/")) {
return filePath.substring(0, filePath.length - extension.length);
}
switch (extension) {
case ".md":
case ".mdx":
@@ -227,7 +231,7 @@ export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boo
const trimmedNoteMeta = noteMeta?.title?.trim();
if (trimmedNoteMeta) return trimmedNoteMeta;
const basename = path.basename(removeFileExtension(filePath));
const basename = path.basename(removeFileExtension(filePath, noteMeta?.mime));
return replaceUnderscoresWithSpaces ? basename.replace(/_/g, " ").trim() : basename;
}

View File

@@ -1,6 +1,6 @@
import type { Application, NextFunction,Request, Response } from "express";
import supertest from "supertest";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { safeExtractMessageAndStackFromError } from "../services/utils.js";
@@ -23,6 +23,10 @@ describe("Share API test", () => {
});
});
afterAll(() => {
vi.useRealTimers();
});
beforeEach(() => {
cannotSetHeadersCount = 0;
});

View File

@@ -19,16 +19,18 @@ export default defineConfig(() => ({
exclude: [
"spec/build-checks/**",
],
hookTimeout: 20000,
hookTimeout: 20_000,
testTimeout: 40_000,
reporters: [
"verbose"
"verbose",
["html", { outputFile: "./test-output/vitest/html/index.html" }]
],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
reporter: [ "text", "html" ]
},
pool: "vmForks",
maxWorkers: 3
pool: "forks",
maxWorkers: 6
},
}));

View File

@@ -9,16 +9,16 @@
"preview": "pnpm build && vite preview"
},
"dependencies": {
"i18next": "25.8.13",
"i18next": "25.8.14",
"i18next-http-backend": "3.0.2",
"preact": "10.28.4",
"preact-iso": "2.11.1",
"preact-render-to-string": "6.6.6",
"react-i18next": "16.5.4"
"react-i18next": "16.5.6"
},
"devDependencies": {
"@preact/preset-vite": "2.10.3",
"eslint": "10.0.2",
"eslint": "10.0.3",
"eslint-config-preact": "2.0.0",
"typescript": "5.9.3",
"user-agent-data-types": "0.4.2",

View File

@@ -43,7 +43,7 @@
"code_title": "Code Notizen",
"canvas_title": "Leinwand",
"mermaid_title": "Mermaid Diagramm",
"mindmap_title": "Mind Map",
"mindmap_title": "Mindmap",
"text_description": "Die Notizen werden mit einem visuellen Editor (WYSIWYG) bearbeitet, der Tabellen, Bilder, mathematische Ausdrücke und Code-Blöcke mit Syntaxhervorhebung unterstützt. Formatieren Sie den Text schnell mit einer Markdown-ähnlichen Syntax oder mit Slash-Befehlen.",
"code_description": "Große Quellcode- oder Skriptdateien werden mit einem speziellen Editor bearbeitet, der Syntaxhervorhebung für viele Programmiersprachen und diverse Farbschemata bietet.",
"title": "Verschiedene Darstellungsformen für Ihre Informationen",

View File

@@ -21,15 +21,18 @@
},
"note_types": {
"canvas_title": "कैनवास",
"mindmap_title": "माइंडमैप"
"mindmap_title": "माइंडमैप",
"file_title": "फ़ाइल नोट्स"
},
"extensibility_benefits": {
"share_title": "वेब पर नोट्स शेयर करें",
"share_description": "अगर आपके पास सर्वर है, तो इसका उपयोग अपने नोट्स के एक हिस्से को अन्य लोगों के साथ शेयर करने के लिए किया जा सकता है।"
"share_description": "अगर आपके पास सर्वर है, तो इसका उपयोग अपने नोट्स के एक हिस्से को अन्य लोगों के साथ शेयर करने के लिए किया जा सकता है।",
"import_export_title": "इंपोर्ट/एक्सपोर्ट"
},
"collections": {
"calendar_title": "कैलेंडर",
"table_title": "टेबल"
"table_title": "टेबल",
"board_title": "कानबान बोर्ड"
},
"download_now": {
"linux_small": "लिनक्स के लिए",

View File

@@ -12,7 +12,7 @@
"get_started": "Начало работы",
"github": "GitHub",
"dockerhub": "Docker Hub",
"screenshot_alt": "Скриншот приложения Trilium Notes для ПК"
"screenshot_alt": "Скриншот приложения Trilium Notes для настольного приложения"
},
"organization_benefits": {
"title": "Структура",
@@ -202,6 +202,7 @@
"title": "Ресурсы",
"icon_packs": "Наборы иконок",
"download": "Скачать",
"website": "Сайт"
"website": "Сайт",
"icon_packs_intro": "Расширьте выбор значков для заметок, используя набор иконок. Подробнее о наборах иконок смотрите в <DocumentationLink>официальной документации</DocumentationLink>."
}
}

View File

@@ -2,6 +2,25 @@
"get-started": {
"title": "Kom igång",
"desktop_title": "Ladda ner skrivbordsprogrammet (v{{version}})",
"architecture": "Arkitektur:"
"architecture": "Arkitektur:",
"older_releases": "Se äldre versioner",
"server_title": "Skapa en server för åtkomst på flera enheter"
},
"hero_section": {
"title": "Organisera dina tankar. Skapa din personliga kunskapsbank.",
"subtitle": "Trilium är en lösning med öppen källkod som möjliggör anteckningar och organisering av en personlig kunskapsbank. Använd den lokalt på ditt skrivbord, eller synka till en självhostad server för att komma åt dina anteckningar överallt.",
"screenshot_alt": "Skärmdump av Trilium Notes skrivbordsapplikation",
"get_started": "Kom igång",
"github": "GitHub",
"dockerhub": "Docker Hub"
},
"organization_benefits": {
"title": "Organisation",
"note_structure_title": "Anteckningsstruktur",
"note_structure_description": "Anteckningar kan sorteras hierarkiskt. Det behövs inga mappar eftersom varje anteckning kan innehålla underordnade anteckningar. En enskild anteckning kan placeras på flera ställen samtidigt i hierarkin.",
"attributes_title": "Hantera etiketter och relationer",
"attributes_description": "Använd relationer mellan anteckningar eller lägg till etiketter för enkel kategorisering. Använd framhävda attribut för att ange strukturerad information som sedan kan visas i tabeller och tavlor.",
"hoisting_title": "Arbetsyta och fokusområde",
"hoisting_description": "Separera enkelt privata- och jobbanteckningar genom att gruppera dem på en arbetsyta, vilket fokuserar anteckningshierarkin att enbart visa en viss grupp av anteckningar."
}
}

View File

@@ -19,6 +19,11 @@
"note_structure_title": "Not yapısı",
"note_structure_description": "Notlar hiyerarşik olarak düzenlenebilir. Her not 'alt notlar' içerebildiği için klasörlere ihtiyaç duyulmaz. Tek bir not, hiyerarşinin birden fazla noktasına eklenebilir.",
"attributes_description": "Notlar arasında ilişkiler kurun veya kolay kategorizasyon için etiketler ekleyin. Tablolarda ve panolarda kullanılabilen yapılandırılmış bilgileri eklemek için öne çıkan öznitelikleri kullanın.",
"hoisting_description": "Kişisel ve iş notlarınızı bir çalışma alanı altında gruplandırarak kolayca ayırın; bu sayede not ağacınız yalnızca belirli bir not kümesini gösterecek şekilde odaklanacaktır."
"hoisting_description": "Kişisel ve iş notlarınızı bir çalışma alanı altında gruplandırarak kolayca ayırın; bu sayede not ağacınız yalnızca belirli bir not kümesini gösterecek şekilde odaklanacaktır.",
"attributes_title": "Not etiketleri ve ilişkileri"
},
"productivity_benefits": {
"title": "Üretkenlik ve güvenlik",
"revisions_title": "Note düzenlemeleri"
}
}

326
docs/README-ar.md vendored
View File

@@ -35,13 +35,13 @@ script)](./README-ZH_TW.md) | [English](../README.md) | [French](./README-fr.md)
<img src="./app.png" alt="Trilium Screenshot" width="1000">
## ⬇️ تنزيل
## ⬇️ التنزيل
- [النسخة الأخيرة](https://github.com/TriliumNext/Trilium/releases/latest)
نسخة مستقرة، محبذة لأكثر المستخدمين.
- [الإصدار الليلي](https://github.com/TriliumNext/Trilium/releases/tag/nightly)
إصدار تطوير غير مستقر، يتم تحديثه يوميًا بأحدث الميزات والإصلاحات.
## 📚توثيق
## 📚 الوثائق
**يمكنكم الاطلاع على وثائقنا الشاملة على الرابط التالي:
[docs.triliumnotes.org](https://docs.triliumnotes.org/)**
@@ -61,33 +61,35 @@ script)](./README-ZH_TW.md) | [English](../README.md) | [French](./README-fr.md)
- [ترقية تريليوم
للملاحظات](https://docs.triliumnotes.org/user-guide/setup/upgrading)
- [مفاهيم ومميزات
اساسية](https://docs.triliumnotes.org/user-guide/concepts/notes)
أساسية](https://docs.triliumnotes.org/user-guide/concepts/notes)
- [أنماط قاعدة المعرفة
الشخصية](https://docs.triliumnotes.org/user-guide/misc/patterns-of-personal-knowledge)
## 🎁الميزات
## 🎁 المميزات
* يمكن ترتيب النوتات الموسيقية في شجرة ذات عمق غير محدود. ويمكن وضع نوتة واحدة
في أماكن متعددة في الشجرة (انظر
* يمكن تنظيم الملاحظات في شجرة ذات عمق غير محدود. كما يمكن وضع الملاحظة الواحدة
في أماكن متعددة داخل الشجرة (راجع
[الاستنساخ](https://docs.triliumnotes.org/user-guide/concepts/notes/cloning))
* محرر ملاحظات WYSIWYG غني يتضمن على سبيل المثال الجداول والصور
و[الرياضيات](https://docs.triliumnotes.org/user-guide/note-types/text) مع
تنسيق تلقائي لـ Markdown[2]
* دعم تحرير [الملاحظات التي تحتوي على شفرة
المصدر](https://docs.triliumnotes.org/user-guide/note-types/code)، بما في ذلك
تمييز بناء الجملة
* التنقل السريع والسهل بين الملاحظات
(https://docs.triliumnotes.org/user-guide/concepts/navigation/note-navigation)،
والبحث في النص الكامل، ورفع الملاحظات
(https://docs.triliumnotes.org/user-guide/concepts/navigation/note-hoisting)
* سلس [ملاحظة حول إصدار
النظام](https://docs.triliumnotes.org/user-guide/concepts/notes/note-revisions)
Markdown [تنسيق
تلقائي](https://docs.triliumnotes.org/user-guide/note-types/text/markdown-formatting)
* دعم تحرير [ملاحظات التعليمات
البرمجية](https://docs.triliumnotes.org/user-guide/note-types/code)، بما في
ذلك تمييز بناء الجملة
* [التنقل بين
الملاحظات](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-navigation)
سهل وسريع، مع دعم البحث في النص الكامل، و[رفع
الملاحظات](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-hoisting)
* نظام [إصدارات
الملاحظات](https://docs.triliumnotes.org/user-guide/concepts/notes/note-revisions)
السلس
* يمكن استخدام
[السمات](https://docs.triliumnotes.org/user-guide/advanced-usage/attributes)
لتنظيم الملاحظات والاستعلام عنها و[البرمجة
النصية](https://docs.triliumnotes.org/user-guide/scripts) المتقدمة
* UI available in English, German, Spanish, French, Romanian, and Chinese
(simplified and traditional)
* واجهة المستخدم متوفرة باللغات الإنجليزية والألمانية والإسبانية والفرنسية
والرومانية والصينية (المبسطة والتقليدية)
* تكامل مباشر مع [أنظمة الهوية المفتوحة OpenID وكلمات المرور المؤقتة
TOTP](https://docs.triliumnotes.org/user-guide/setup/server/mfa) لتسجيل دخول
أكثر أماناً
@@ -97,8 +99,8 @@ script)](./README-ZH_TW.md) | [English](../README.md) | [French](./README-fr.md)
المزامنة](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
* [مشاركة](https://docs.triliumnotes.org/user-guide/advanced-usage/sharing)
الملاحظات (نشرها) على شبكة الإنترنت العامة
* [تشفير الملاحظات]
(https://docs.triliumnotes.org/user-guide/concepts/notes/protected-notes)
* [تشفير
الملاحظات](https://docs.triliumnotes.org/user-guide/concepts/notes/protected-notes)
تشفير قوي مع إمكانية التحكم بكل ملاحظة على حدة
* رسم المخططات، بالاعتماد على إكسكاليدرا [Excalidraw](https://excalidraw.com/)
(نوع الملاحظة "لوحة رسم")
@@ -118,131 +120,129 @@ script)](./README-ZH_TW.md) | [English](../README.md) | [French](./README-fr.md)
المتقدمة](https://docs.triliumnotes.org/user-guide/advanced-usage/advanced-showcases)
* [واجهة REST
API](https://docs.triliumnotes.org/user-guide/advanced-usage/etapi) للأتمتة
* Scales well in both usability and performance upwards of 100 000 notes
* Touch optimized [mobile
frontend](https://docs.triliumnotes.org/user-guide/setup/mobile-frontend) for
smartphones and tablets
* Built-in [dark
theme](https://docs.triliumnotes.org/user-guide/concepts/themes), support for
user themes
* [Evernote](https://docs.triliumnotes.org/user-guide/concepts/import-export/evernote)
and [Markdown import &
export](https://docs.triliumnotes.org/user-guide/concepts/import-export/markdown)
* [Web Clipper](https://docs.triliumnotes.org/user-guide/setup/web-clipper) for
easy saving of web content
* Customizable UI (sidebar buttons, user-defined widgets, ...)
* [Metrics](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics),
along with a Grafana Dashboard.
* يتميز بسهولة الاستخدام والأداء العاليين، ويستوعب أكثر من 100,000 ملاحظة
* [واجهة جوال](https://docs.triliumnotes.org/user-guide/setup/mobile-frontend)
محسنة للمس، مخصصة للهواتف الذكية والأجهزة اللوحية
* [الوضع الداكن](https://docs.triliumnotes.org/user-guide/concepts/themes)
المدمج، ودعم سمات المستخدم
* [إيفيرنوت
(Evernote)](https://docs.triliumnotes.org/user-guide/concepts/import-export/evernote)
و[استيراد وتصدير ملفات
Markdown](https://docs.triliumnotes.org/user-guide/concepts/import-export/markdown)
* [أداة قص الويب](https://docs.triliumnotes.org/user-guide/setup/web-clipper)
لحفظ محتوى الويب بسهولة
* واجهة مستخدم قابلة للتخصيص (أزرار الشريط الجانبي، أدوات المستخدم المحددة، ...)
* [مؤشرات
الأداء](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics)، مع
لوحة تحكم Grafana.
Check out the following third-party resources/communities for more TriliumNext
related goodies:
اطلع على الموارد/المجتمعات الخارجية التالية لمزيد من المعلومات المفيدة
المتعلقة بـ TriliumNext:
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party
themes, scripts, plugins and more.
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
- للقوالب والبرامج النصية والإضافات الخارجية والمزيد
[awesome-trillium](https://github.com/Nriver/awesome-trilium).
- للحصول على دروس تعليمية، وأدلة، وغير ذلك الكثير
[TriliumRocks!](https://trilium.rocks/).
## لماذا تريليوم التالي؟
## ؟لماذا TriliumNext؟
The original Trilium developer ([Zadam](https://github.com/zadam)) has
graciously given the Trilium repository to the community project which resides
at https://github.com/TriliumNext
قام مطور Trilium الأصلي ([Zadam](https://github.com/zadam)) مشكورًا بإهداء
مستودع Trilium إلى مشروع المجتمع الموجود على الرابط التالي:
https://github.com/TriliumNext
### ⬆️ الهجرة من Zadam الى تريليوم؟
### ⬆️الانتقال من Zadam/Trilium؟
There are no special migration steps to migrate from a zadam/Trilium instance to
a TriliumNext/Trilium instance. Simply [install
TriliumNext/Trilium](#-installation) as usual and it will use your existing
database.
لا توجد خطوات انتقال خاصة للانتقال من نسخة zadam/Trilium إلى نسخة
TriliumNext/Trilium. ما عليك سوى تثبيت TriliumNext/Trilium كالمعتاد، وسيستخدم
قاعدة بياناتك الحالية.
Versions up to and including
[v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are
compatible with the latest zadam/trilium version of
[v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later
versions of TriliumNext/Trilium have their sync versions incremented which
prevents direct migration.
الإصدارات حتى الإصدار
[v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) متوافقة
مع أحدث إصدار من zadam/trillium وهو [v0.63.7]{2]. أما الإصدارات اللاحقة من
TriliumNext/Trilium، فقد تم تحديث أرقام المزامنة الخاصة بها، مما يمنع الترقية
المباشرة.
## 💬تحدث معنا
## 💬 ناقش معنا
Feel free to join our official conversations. We would love to hear what
features, suggestions, or issues you may have!
لا تترددوا في الانضمام إلى محادثاتنا الرسمية. يسعدنا أن نسمع عن الميزات أو
الاقتراحات أو المشاكل التي قد تواجهونها!
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous
discussions.)
- The `General` Matrix room is also bridged to
[XMPP](xmpp:discuss@trilium.thisgreat.party?join)
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For
asynchronous discussions.)
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug
reports and feature requests.)
- [ماتريكس (Matrix)](https://matrix.to/#/#triliumnext:matrix.org) (للمناقشات
المتزامنة.)
- غرفة ماتريكس `العامة` مرتبطة أيضاً بـ
[XMPP](xmpp:discuss@trilium.thisgreat.party?join) (Extensible Messaging and
Presence Protocol)
- [مناقشات GitHub](https://github.com/TriliumNext/Trilium/discussions)
(للمناقشات غير المتزامنة.)
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (لتقديم تقارير
الأخطاء وطلبات الميزات.)
## 🏗️ تثبيت
## 🏗️ التثبيت
### ويندوز / نظام تشغيل ماك
### ويندوز (Windows) / نظام تشغيل ماك (MacOS)
Download the binary release for your platform from the [latest release
page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package
and run the `trilium` executable.
نزل الإصدار الثنائي (Binary) المناسب لمنصتك من [صفحة أحدث
الإصدارات](https://github.com/TriliumNext/Trilium/releases/latest)، ثم فك ضغط
الحزمة وشغّل ملف `trilium` التنفيذي.
### لينكس
### لينكس (Linux)
If your distribution is listed in the table below, use your distribution's
package.
إذا كانت توزيعتك مدرجة في الجدول أدناه، فاستخدم الحزمة الخاصة بتوزيعتك.
[![Packaging
status](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions)
[![حالة
الحزم](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions)
You may also download the binary release for your platform from the [latest
release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the
package and run the `trilium` executable.
يمكنك أيضًا تنزيل الإصدار الثنائي لمنصتك من [صفحة أحدث
إصدار](https://github.com/TriliumNext/Trilium/releases/latest)، وفك ضغط الحزمة
وتشغيل الملف التنفيذي `trillium`.
TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
يتم توفير TriliumNext أيضًا كـ Flatpak، ولكن لم يتم نشره بعد على FlatHub.
### مستعرض( اي نظام تشغيل)
### متصفح (أي نظام تشغيل)
If you use a server installation (see below), you can directly access the web
interface (which is almost identical to the desktop app).
إذا كنت تستخدم تثبيت الخادم (انظر أدناه)، فيمكنك الوصول مباشرة إلى واجهة الويب
(وهي مطابقة تقريبًا لتطبيق سطح المكتب).
Currently only the latest versions of Chrome & Firefox are supported (and
tested).
حالياً، يتم دعم أحدث إصدارات متصفحي Chrome و Firefox فقط (والتي تم اختبارها).
### هاتف المحمول
### الهاتف المحمول
To use TriliumNext on a mobile device, you can use a mobile web browser to
access the mobile interface of a server installation (see below).
لاستخدام TriliumNext على الأجهزة المحمولة، يمكنك استخدام المتصفح للوصول إلى
واجهة الجوال الخاصة بنسخة الخادم المثبتة (انظر أدناه).
See issue https://github.com/TriliumNext/Trilium/issues/4962 for more
information on mobile app support.
راجع المشكلة رقم https://github.com/TriliumNext/Trilium/issues/4962 لمزيد من
المعلومات حول دعم إصدار الهاتف المحمول.
If you prefer a native Android app, you can use
إذا كنت تفضل تطبيقًا أصليًا لنظام Android، يمكنك استخدام
[TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid).
Report bugs and missing features at [their
repository](https://github.com/FliegendeWurst/TriliumDroid). Note: It is best to
disable automatic updates on your server installation (see below) when using
TriliumDroid since the sync version must match between Trilium and TriliumDroid.
أبلغ عن الأخطاء والميزات المفقودة في
[مستودعهم](https://github.com/FliegendeWurst/TriliumDroid). ملاحظة: يُفضل تعطيل
التحديثات التلقائية على تثبيت الخادم الخاص بك (انظر أدناه) عند استخدام
TriliumDroid، حيث يجب أن تتطابق نسخة المزامنة بين Trilium وTriliumDroid.
### الخادم
### خادم
To install TriliumNext on your own server (including via Docker from
[Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server
installation docs](https://docs.triliumnotes.org/user-guide/setup/server).
لتثبيت TriliumNext على خادمك الخاص (بما في ذلك عبر Docker من
[Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) اتبع [وثائق تثبيت
الخادم]{2].
## 💻 المساهمة
### ترجمات
### الترجمات
If you are a native speaker, help us translate Trilium by heading over to our
[Weblate page](https://hosted.weblate.org/engage/trilium/).
إذا كنت متحدثًا أصليًا للغة، فساعدنا في ترجمة Trilium من خلال التوجه إلى [صفحة
الويب]{1].
Here's the language coverage we have so far:
إليك قائمة نسبة اكتمال اللغات المدعومة حتى الآن:
[![Translation
status](https://hosted.weblate.org/widget/trilium/multi-auto.svg)](https://hosted.weblate.org/engage/trilium/)
[![حالة
الترجمة](https://hosted.weblate.org/widget/trilium/multi-auto.svg)](https://hosted.weblate.org/engage/trilium/)
### كود
### التعليمات البرمجية
Download the repository, install dependencies using `pnpm` and then run the
server (available at http://localhost:8080):
قم بتنزيل المستودع، وقم بتثبيت التبعيات باستخدام `pnpm`، ثم قم بتشغيل الخادم
(المتاح على http://localhost:8080):
```shell
git clone https://github.com/TriliumNext/Trilium.git
cd Trilium
@@ -252,8 +252,8 @@ pnpm run server:start
### التوثيق
Download the repository, install dependencies using `pnpm` and then run the
environment required to edit the documentation:
قم بتنزيل المستودع، وقم بتثبيت التبعيات باستخدام `pnpm`، ثم قم بتشغيل البيئة
المطلوبة لتحرير الوثائق:
```shell
git clone https://github.com/TriliumNext/Trilium.git
cd Trilium
@@ -262,8 +262,8 @@ pnpm edit-docs:edit-docs
```
### بناء الملف التنفيذي
Download the repository, install dependencies using `pnpm` and then build the
desktop app for Windows:
قم بتنزيل المستودع، وقم بتثبيت التبعيات باستخدام `pnpm`، ثم قم ببناء تطبيق سطح
المكتب لنظام التشغيل ويندوز (Windows):
```shell
git clone https://github.com/TriliumNext/Trilium.git
cd Trilium
@@ -271,71 +271,69 @@ pnpm install
pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
```
For more details, see the [development
docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
لمزيد من التفاصيل، راجع [وثائق
التطوير](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
### توثيق المطور
### وثائق المطورين
Please view the [documentation
guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
for details. If you have more questions, feel free to reach out via the links
described in the "Discuss with us" section above.
يرجى الاطلاع على [دليل
التوثيق](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
لمزيد من التفاصيل. إذا كانت لديكم أي استفسارات أخرى، فلا تترددوا في التواصل معنا
عبر الروابط الموضحة في قسم "ناقش معنا" أعلاه.
## 👏 اشادات
## 👏 شكر خاص
* [zadam](https://github.com/zadam) for the original concept and implementation
of the application.
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
application icon.
* [nriver](https://github.com/nriver) for his work on internationalization.
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
widget.
* [Dosu](https://dosu.dev/) for providing us with the automated responses to
GitHub issues and discussions.
* [Tabler Icons](https://tabler.io/icons) for the system tray icons.
* لـ [zadam](https://github.com/zadam): للمفهوم الأصلي وتنفيذ التطبيق.
* لـ [سارة حسين (Sarah Hussein)](https://github.com/Sarah-Hussein): لتصميم
أيقونة التطبيق.
* لـ [nriver](https://github.com/nriver): لعمله على تدويل التطبيق (دعم اللغات).
* لـ [Thomas Frei](https://github.com/thfrei): لعمله الأصلي على "اللوحة"
(Canvas).
* لـ [antoniotejada](https://github.com/nriver): لأداة تمييز الصيغة البرمجية
(Syntax highlight) الأصلية.
* لـ [Dosu](https://dosu.dev/): لتزويدنا بالردود الآلية على مشكلات ونقاشات
GitHub.
* لـ [Tabler Icons](https://tabler.io/icons): لأيقونات شريط النظام.
Trilium would not be possible without the technologies behind it:
لم يكن لـ Trilium أن يرى النور لولا التقنيات التي تقف خلفه:
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - the visual editor behind
text notes. We are grateful for being offered a set of the premium features.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with
support for huge amount of languages.
* [Excalidraw](https://github.com/excalidraw/excalidraw) - the infinite
whiteboard used in Canvas notes.
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - providing the
mind map functionality.
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical
maps.
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive
table used in collections.
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library
without real competition.
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library.
Used in [relation
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
[link
maps](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)
* محرر [CKEditor 5](https://github.com/ckeditor/ckeditor5): - المحرر المرئي خلف
الملاحظات النصية. نحن ممتنون لحصولنا على مجموعة من الميزات المدفوعة (Premium).
* محرر [CodeMirror](https://github.com/codemirror/CodeMirror): - محرر أكواد يدعم
عدداً هائلاً من اللغات.
* برنامج [Excalidraw](https://github.com/excalidraw/excalidraw): - السبورة
البيضاء اللانهائية المستخدمة في ملاحظات Canvas.
* برنامج [Mind Elixir](https://github.com/SSShooter/mind-elixir-core): - يوفر
وظائف الخرائط الذهنية.
* برنامج [Leaflet](https://github.com/Leaflet/Leaflet): - لعرض الخرائط
الجغرافية.
* مكتبة [Tabulator](https://github.com/olifolkerd/tabulator): - للجداول
التفاعلية المستخدمة في المجموعات.
* مكتبة [FancyTree](https://github.com/mar10/fancytree): - مكتبة "شجرية" غنية
بالميزات ولا يوجد لها منافس حقيقي.
* مكتبة [jsPlumb](https://github.com/jsplumb/jsplumb): - مكتبة للربط المرئي،
تُستخدم في [خرائط
العلاقات](https://docs.triliumnotes.org/user-guide/note-types/relation-map)
و[خرائط
الروابط](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)
## 🤝 الدعم
Trilium is built and maintained with [hundreds of hours of
work](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Your
support keeps it open-source, improves features, and covers costs such as
hosting.
تم بناء وصيانة برنامج Trilium بمئات الساعات من العمل. دعمكم يحافظ على كونه مفتوح
المصدر، ويحسن الميزات، ويغطي التكاليف مثل الاستضافة.
Consider supporting the main developer
([eliandoran](https://github.com/eliandoran)) of the application via:
يرجى التفكير في دعم المطور الرئيسي ([eliandoran](https://github.com/eliandoran))
للتطبيق عبر:
- [GitHub Sponsors](https://github.com/sponsors/eliandoran)
- [رعاة GitHub](https://github.com/sponsors/eliandoran)
- [PayPal](https://paypal.me/eliandoran)
- [Buy Me a Coffee](https://buymeacoffee.com/eliandoran)
- [اشترِ لي قهوة](https://buymeacoffee.com/eliandoran)
## 🔑 الترخيص
Copyright 2017-2025 zadam, Elian Doran, and other contributors
جميع الحقوق محفوظة لـ zadam وإليان دوران ومساهمين آخرين، من عام 2017 إلى عام
2025
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
هذا البرنامج هو برنامج مجاني: يمكنك إعادة توزيعه و/أو تعديله بموجب شروط رخصة جنو
أفيرو (GNU Affero) العامة كما نشرتها مؤسسة البرمجيات الحرة، سواء الإصدار 3 من
الرخصة، أو (حسب اختيارك) أي إصدار لاحق.

31
docs/README-sv.md vendored
View File

@@ -37,32 +37,31 @@ antecknings app med fokus på att bygga en stor personlig kunskapsbas.
## Ladda ner
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest)
stabil version, rekommenderas för dom flesta användare.
- [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly)
unstable development version, updated daily with the latest features and
fixes.
- [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly) -
ostabil utvecklings version, uppdaterad dagligen med de senaste funktionerna
och fixarna.
## 📚 Documentation
## 📚 Dokumentation
**Visit our comprehensive documentation at
**Läs mer i vår omfattande dokumentation
[docs.triliumnotes.org](https://docs.triliumnotes.org/)**
Our documentation is available in multiple formats:
- **Online Documentation**: Browse the full documentation at
r dokumentation är tillgänglig i flera format:
- **Webb dokumentation**: Läs hela dokumentationen på
[docs.triliumnotes.org](https://docs.triliumnotes.org/)
- **In-App Help**: Press `F1` within Trilium to access the same documentation
directly in the application
- **GitHub**: Navigate through the [User Guide](./User%20Guide/User%20Guide/) in
this repository
- **I-appen-hjälp**: Tryck `F1` i Trilium för att läsa samma dokumentation inuti
programmet
- **GitHub**: Läs [användarhandboken](./User%20Guide/User%20Guide/) i denna repo
### Quick Links
- [Getting Started Guide](https://docs.triliumnotes.org/)
- [Installation Instructions](https://docs.triliumnotes.org/user-guide/setup)
### Snabblänkar
- [Snabbstartsguide](https://docs.triliumnotes.org/)
- [Installationsanvisning](https://docs.triliumnotes.org/user-guide/setup)
- [Docker
Setup](https://docs.triliumnotes.org/user-guide/setup/server/installation/docker)
- [Upgrading
TriliumNext](https://docs.triliumnotes.org/user-guide/setup/upgrading)
- [Basic Concepts and
Features](https://docs.triliumnotes.org/user-guide/concepts/notes)
- [Grundläggande koncept och
funktioner](https://docs.triliumnotes.org/user-guide/concepts/notes)
- [Patterns of Personal Knowledge
Base](https://docs.triliumnotes.org/user-guide/misc/patterns-of-personal-knowledge)

9
docs/README-tr.md vendored
View File

@@ -50,10 +50,9 @@ edin(https://docs.triliumnotes.org/)**
Dokümantasyonumuz birden fazla formatta mevcuttur:
- **Çevrimiçi Dökümantasyon**: Tüm dökümantasyonu görebilmek için
[docs.triliumnotes.org](https://docs.triliumnotes.org/)'a uğrayın
- **In-App Help**: Press `F1` within Trilium to access the same documentation
directly in the application
- **GitHub**: Navigate through the [User Guide](./User%20Guide/User%20Guide/) in
this repository
- **Uygulama içi Yardım**: Aynı dökümantasyona ulaşmak için Trillium
içerisindeyken `F1` tuşuna basın
- **Github**: Bu repodaki [Kullanıcı Rehberi] sayfasına yönelin
### Hızlı linkler
- [Başlangıç Kılavuzu](https://docs.triliumnotes.org/)
@@ -64,7 +63,7 @@ Dokümantasyonumuz birden fazla formatta mevcuttur:
- [Patterns of Personal Knowledge
Base](https://docs.triliumnotes.org/user-guide/misc/patterns-of-personal-knowledge)
## 🎁 Features
## 🎁 Özellikler
* Notes can be arranged into arbitrarily deep tree. Single note can be placed
into multiple places in the tree (see

View File

@@ -50,7 +50,7 @@
"@triliumnext/server": "workspace:*",
"@types/express": "5.0.6",
"@types/js-yaml": "4.0.9",
"@types/node": "24.11.0",
"@types/node": "24.12.0",
"@vitest/browser-webdriverio": "4.0.18",
"@vitest/coverage-v8": "4.0.18",
"@vitest/ui": "4.0.18",
@@ -58,18 +58,18 @@
"cross-env": "10.1.0",
"dpdm": "4.0.1",
"esbuild": "0.27.3",
"eslint": "10.0.2",
"eslint": "10.0.3",
"eslint-config-preact": "2.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-playwright": "2.8.0",
"eslint-plugin-playwright": "2.9.0",
"eslint-plugin-simple-import-sort": "12.1.1",
"happy-dom": "20.7.0",
"happy-dom": "20.8.3",
"http-server": "14.1.1",
"jiti": "2.6.1",
"js-yaml": "4.1.1",
"jsonc-eslint-parser": "3.1.0",
"react-refresh": "0.18.0",
"rollup-plugin-webpack-stats": "2.1.11",
"rollup-plugin-webpack-stats": "3.0.0",
"tslib": "2.8.1",
"tsx": "4.21.0",
"typescript": "5.9.3",
@@ -103,7 +103,7 @@
"mermaid": "11.12.3",
"preact": "10.28.4",
"roughjs": "4.6.6",
"@types/express-serve-static-core": "5.1.0",
"@types/express-serve-static-core": "5.1.1",
"flat@<5.0.1": ">=5.0.1",
"debug@>=3.2.0 <3.2.7": ">=3.2.7",
"nanoid@<3.3.8": ">=3.3.8",

View File

@@ -29,10 +29,10 @@
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.4.0",
"eslint": "10.0.2",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.3.1",
"lint-staged": "16.3.2",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",

View File

@@ -30,10 +30,10 @@
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.4.0",
"eslint": "10.0.2",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.3.1",
"lint-staged": "16.3.2",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",

View File

@@ -32,10 +32,10 @@
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.4.0",
"eslint": "10.0.2",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.3.1",
"lint-staged": "16.3.2",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",

View File

@@ -32,10 +32,10 @@
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.4.0",
"eslint": "10.0.2",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.3.1",
"lint-staged": "16.3.2",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",

View File

@@ -32,10 +32,10 @@
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.4.0",
"eslint": "10.0.2",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.3.1",
"lint-staged": "16.3.2",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",

View File

@@ -16,7 +16,7 @@
"ckeditor5-premium-features": "47.4.0"
},
"devDependencies": {
"@smithy/middleware-retry": "4.4.37",
"@smithy/middleware-retry": "4.4.40",
"@types/jquery": "4.0.0"
}
}

View File

@@ -11,6 +11,7 @@ export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, Model
export type { TemplateDefinition } from "ckeditor5-premium-features";
export { default as buildExtraCommands } from "./extra_slash_commands.js";
export { default as getCkLocale } from "./i18n.js";
export * from "./utils.js";
// Import with sideffects to ensure that type augmentations are present.
import "@triliumnext/ckeditor5-math";

View File

@@ -0,0 +1,28 @@
import type { DifferItemAttribute, Editor, ModelDocumentFragment, ModelElement, ModelNode } from "ckeditor5";
function hasHeadingAncestor(node: ModelElement | ModelNode | ModelDocumentFragment | null): boolean {
let current: ModelElement | ModelNode | ModelDocumentFragment | null = node;
while (current) {
if (!!current && current.is('element') && (current as ModelElement).name.startsWith("heading")) return true;
current = current.parent;
}
return false;
}
export function attributeChangeAffectsHeading(change: DifferItemAttribute, editor: Editor): boolean {
if (change.type !== "attribute") return false;
// Fast checks on range boundaries
if (hasHeadingAncestor(change.range.start.parent) || hasHeadingAncestor(change.range.end.parent)) {
return true;
}
// Robust check across the whole changed range
const range = editor.model.createRange(change.range.start, change.range.end);
for (const item of range.getItems()) {
const baseNode = item.is("$textProxy") ? item.parent : item;
if (hasHeadingAncestor(baseNode)) return true;
}
return false;
}

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