Compare commits

..

274 Commits

Author SHA1 Message Date
Elian Doran a2b6504d9d feat(ci): build server for Linux ARM
/ Check Docker build (Dockerfile) (push) Failing after 39s
/ Check Docker build (Dockerfile.alpine) (push) Failing after 37s
/ Build Docker images (Dockerfile, linux/arm/v7) (push) Has been skipped
/ Build Docker images (Dockerfile, linux/arm64) (push) Has been skipped
/ Build Docker images (Dockerfile.alpine, linux/amd64) (push) Has been skipped
/ Merge manifest lists (push) Has been skipped
Release / Make Electron (arm64, map[extension:deb image:ubuntu-latest name:linux]) (push) Failing after 35s
Release / Make Electron (x64, map[extension:deb image:ubuntu-latest name:linux]) (push) Failing after 34s
Release / Build Linux Server (x64, ubuntu-latest) (push) Failing after 37s
Release / Make Electron (arm64, map[extension:dmg image:macos-latest name:macos]) (push) Has been cancelled
Release / Make Electron (arm64, map[extension:exe image:windows-latest name:windows]) (push) Has been cancelled
Release / Make Electron (x64, map[extension:dmg image:macos-latest name:macos]) (push) Has been cancelled
Release / Make Electron (x64, map[extension:exe image:windows-latest name:windows]) (push) Has been cancelled
Release / Build Linux Server (arm64, ubuntu-24.04-arm) (push) Has been cancelled
2025-01-28 19:06:14 +02:00
Elian Doran ab9b5a6926 feat(geo_map): move markers when panning across the world
/ Check Docker build (Dockerfile) (push) Failing after 39s
/ Check Docker build (Dockerfile.alpine) (push) Failing after 41s
/ Build Docker images (Dockerfile, linux/arm/v7) (push) Has been skipped
/ Build Docker images (Dockerfile, linux/arm64) (push) Has been skipped
/ Build Docker images (Dockerfile.alpine, linux/amd64) (push) Has been skipped
/ Merge manifest lists (push) Has been skipped
Release / Make Electron (arm64, map[extension:deb image:ubuntu-latest name:linux]) (push) Failing after 43s
Release / Make Electron (x64, map[extension:deb image:ubuntu-latest name:linux]) (push) Failing after 39s
Release / Build Linux Server x86_64 (push) Failing after 36s
Release / Make Electron (arm64, map[extension:dmg image:macos-latest name:macos]) (push) Has been cancelled
Release / Make Electron (arm64, map[extension:exe image:windows-latest name:windows]) (push) Has been cancelled
Release / Make Electron (x64, map[extension:dmg image:macos-latest name:macos]) (push) Has been cancelled
Release / Make Electron (x64, map[extension:exe image:windows-latest name:windows]) (push) Has been cancelled
2025-01-28 17:13:15 +02:00
Elian Doran 687be6d722 fix(client): note type chooser does not have geo map entry 2025-01-28 17:05:51 +02:00
Elian Doran eaf548f056 style(next): fix top position of code badge 2025-01-28 17:05:51 +02:00
Elian Doran f1cb95d95e fix(types): allow falsy values 2025-01-28 16:44:56 +02:00
Elian Doran 189a2296c4 chore(i18n): promote mind map to stable note type 2025-01-28 16:34:10 +02:00
Elian Doran 3f6dc717b6 feat(client/ts): port editability_select 2025-01-28 16:21:26 +02:00
Elian Doran aecba70efc fix(client): broken layouts in editability and note type dropdowns 2025-01-28 16:18:36 +02:00
Elian Doran 546274a79d feat(client/ts): port note_list 2025-01-28 14:13:21 +02:00
Elian Doran 15c63f52dc feat(client/ts): port note_title 2025-01-28 14:07:56 +02:00
Elian Doran 55ce673f1b Merge pull request #1056 from TriliumNext/chore_port-desktop-mobile-entrypoints
chore: port desktop and mobile entrypoints
2025-01-28 13:10:46 +02:00
Elian Doran 10536adecf Merge pull request #1057 from TriliumNext/fix_npm-audit
fix(deps): run npm audit fix
2025-01-28 09:05:30 +02:00
Elian Doran 7eed037ace chore(deps): update package.lock 2025-01-28 08:56:05 +02:00
Elian Doran 7bac28bff4 chore(build): fix running electron on nixos 2025-01-28 08:55:54 +02:00
Panagiotis Papadopoulos 939c2b7ef6 fix(deps): run npm audit fix 2025-01-28 07:40:51 +01:00
Panagiotis Papadopoulos e614e775fa chore(client/ts): port mobile.js entrypoint to TS 2025-01-28 07:25:26 +01:00
Panagiotis Papadopoulos 4c30853258 chore(client/ts): port desktop.js entrypoint to TS 2025-01-28 07:25:26 +01:00
Panagiotis Papadopoulos 144a7ff3a1 chore(types.d.ts): add missing platform prop to CustomGlobals
as used e.g. in dekstop.js – supplied via ejs (and then rendered via `./src/views/partials/windowGlobal`

I left it as optional however, because e.g. it does not seem to exist on *all* pages
2025-01-28 07:25:26 +01:00
Elian Doran 24026ff013 chore(release): prepare for 0.91.4-beta 2025-01-28 00:09:49 +02:00
Elian Doran 7d8e96d6df fix(context-menu): keyboard shortcut not filling width 2025-01-28 00:03:04 +02:00
Elian Doran 9125aeca29 Merge pull request #1048 from TriliumNext/chore_webpack-share
chore: make share.js work with webpack
2025-01-27 22:42:13 +02:00
Elian Doran 4264e37256 Merge pull request #1053 from TriliumNext/renovate/mind-elixir-node-menu-1.x
fix(deps): update dependency @mind-elixir/node-menu to v1.0.4
2025-01-27 22:19:59 +02:00
Panagiotis Papadopoulos 21539857c3 build(npm): remove override again, as @mind-elixir/node-menu got updated :-) 2025-01-27 19:46:36 +01:00
renovate[bot] 1d9f15b9a2 fix(deps): update dependency @mind-elixir/node-menu to v1.0.4 2025-01-27 16:35:43 +00:00
Elian Doran be115e82b6 Merge pull request #1049 from TriliumNext/build_npm-override
build(npm): use specific overrides instead of `legacy-peer-deps`
2025-01-27 14:49:35 +02:00
Elian Doran 3dd5623f73 Merge pull request #1051 from TriliumNext/chore_delete-outdated-issue-template
chore: delete outdated issue_template.md
2025-01-27 14:43:42 +02:00
Panagiotis Papadopoulos 1d9c43f77e chore: delete outdated issue_template.md
chore: delete outdated issue_template.md, the current ones are already existing under `.github`
2025-01-27 10:40:33 +01:00
Panagiotis Papadopoulos a234234373 build(npm): use specific overrides instead of legacy-peer-deps
allows us to handle these peer dependency
issues on a case by case basis, instead of
generally allowing it for all packages,
which could lead to unnoticed issues.

in this case the @mind-elixir/node-menu package
is still showing mind-elixir 2.x.x as peerDep,
which cannot be resolved, since we use v4
2025-01-27 07:54:48 +01:00
Panagiotis Papadopoulos ad02b86719 chore: remove now unnecessary manual copying of share.js
this is now correctly handled by the webpack build
2025-01-26 21:39:34 +01:00
Panagiotis Papadopoulos 74f6ad46ae chore(app/share): add workaround for dead code eliminination by webpack
since fetchNote function is not used anywhere, webpack will treat it as "dead code" and remove it from the final output

I did not have any success with using webpack config flags like "sideEffects", so I've reverted to this workaround for now
2025-01-26 21:23:08 +01:00
Panagiotis Papadopoulos 3ed9528495 chore(app/share): port to TypeScript 2025-01-26 20:54:30 +01:00
Elian Doran c6730c5467 chore(client/ts): port ribbon_container 2025-01-26 21:24:42 +02:00
Elian Doran 47fb4d7b27 chore(client/ts): port note_info_widget 2025-01-26 21:23:08 +02:00
Elian Doran 20584f622d chore(client/ts): port zpetne_odkazy 2025-01-26 21:23:08 +02:00
Elian Doran 32fc5def77 chore(client/ts): port note_map 2025-01-26 21:23:08 +02:00
Elian Doran befdade7ba chore(client/ts): port similar_notes 2025-01-26 21:23:08 +02:00
Elian Doran 4492c60aa9 chore(client/ts): port note_paths 2025-01-26 21:23:08 +02:00
Elian Doran 3f7bda6289 chore(client/ts): port left_pane_toggle 2025-01-26 21:23:08 +02:00
Elian Doran 8e62c942ea Merge pull request #1037 from TriliumNext/renovate/ws-8.x
chore(deps): update dependency @types/ws to v8.5.14
2025-01-26 20:48:57 +02:00
Elian Doran 61d4d3fd2c fix(types): broken types after ws upgrade 2025-01-26 20:39:19 +02:00
Elian Doran e4b186ce8a Merge pull request #1036 from TriliumNext/feat_env-config
feat(services/config): Add ability to set Trilium configuration variables via ENV variables
2025-01-26 14:32:13 +02:00
Elian Doran 7ae7831a27 Merge pull request #1022 from TriliumNext/fix_import-utils-#1016
fix(import/utils.handleH1): fix stripping of all <h1> tags that match title tag
2025-01-26 14:28:52 +02:00
Elian Doran 189dfdb5cd Merge pull request #1046 from TriliumNext/renovate/i18next-24.x
fix(deps): update dependency i18next to v24.2.2
2025-01-26 14:25:17 +02:00
renovate[bot] af204d0d74 fix(deps): update dependency i18next to v24.2.2 2025-01-26 00:39:49 +00:00
Elian Doran faf14e1b14 Merge pull request #1043 from TriliumNext/fix_webpack-share-entry
fix(webpack): add missing share.js entry point
2025-01-25 23:42:41 +02:00
Elian Doran e1a095d111 Merge pull request #1045 from TriliumNext/chore_webpack-config-types
chore(webpack): add Configuration type
2025-01-25 23:41:16 +02:00
Elian Doran 0d82b64ed1 Merge pull request #1033 from TriliumNext/renovate/node-22.x
chore(deps): update node.js to v22.13.1
2025-01-25 23:40:13 +02:00
Elian Doran 395aee5657 Merge pull request #1038 from TriliumNext/renovate/electron-forge-monorepo
chore(deps): update electron-forge monorepo to v7.6.1
2025-01-25 23:39:28 +02:00
Elian Doran 20f2fa3135 Merge pull request #1039 from TriliumNext/renovate/vitest-monorepo
chore(deps): update vitest monorepo to v3.0.4
2025-01-25 23:38:15 +02:00
Elian Doran 24d4375552 Merge pull request #1040 from TriliumNext/renovate/i18next-http-backend-3.x
fix(deps): update dependency i18next-http-backend to v3.0.2
2025-01-25 23:37:40 +02:00
Elian Doran bc901a1cf6 Merge pull request #1041 from TriliumNext/renovate/playwright-monorepo
chore(deps): update dependency @playwright/test to v1.50.0
2025-01-25 23:37:10 +02:00
Elian Doran f3219da749 Merge pull request #1032 from TriliumNext/renovate/electron-34.x
chore(deps): update dependency electron to v34.0.1
2025-01-25 23:35:38 +02:00
Panagiotis Papadopoulos 2031029076 chore(webpack): add Configuration type
-> allows for intellisense in e.g. VSCode
2025-01-25 11:31:46 +01:00
Elian Doran 577c61b0d1 Merge pull request #1042 from j9t/refactor/images
refactor: compress images
2025-01-24 22:52:28 +02:00
Panagiotis Papadopoulos b7f8f00a14 fix(webpack): add missing share.js entry point 2025-01-24 19:30:07 +01:00
Jens Oliver Meiert 2d86d9a7c0 refactor: compress images (losslessly, with Imagemin Guard)
Signed-off-by: Jens Oliver Meiert <jens@meiert.com>
2025-01-24 15:16:05 +01:00
Panagiotis Papadopoulos 8472e44fe6 chore(docker_healthcheck): remove now unused imports 2025-01-24 07:46:39 +01:00
Panagiotis Papadopoulos 2c2a404056 feat(services/config): rename syncProxy to syncServerProxy for naming consistency
backwards compatibility ensured, by still checking the iniConfig for the old value first
2025-01-24 07:45:47 +01:00
renovate[bot] 37cdb2fe5e chore(deps): update dependency @playwright/test to v1.50.0 2025-01-24 01:20:16 +00:00
renovate[bot] ec6b5cda80 fix(deps): update dependency i18next-http-backend to v3.0.2 2025-01-24 01:20:05 +00:00
renovate[bot] 3cfdc6dac7 chore(deps): update vitest monorepo to v3.0.4 2025-01-24 01:19:57 +00:00
renovate[bot] d35bdfa402 chore(deps): update electron-forge monorepo to v7.6.1 2025-01-24 01:19:45 +00:00
renovate[bot] c56eb9a8a1 chore(deps): update dependency @types/ws to v8.5.14 2025-01-24 01:19:25 +00:00
Panagiotis Papadopoulos 9cca9fe074 feat(config-sample): add Sync config block to config-sample
commented out
2025-01-24 00:12:50 +01:00
perf3ct 5c7c995504 feat: have docker_healthcheck use correct config 2025-01-24 00:06:50 +01:00
Panagiotis Papadopoulos 3fc9a10ab6 refactor(services/sync_options): get rid of process.env query
this is now handled in config.ts already
2025-01-24 00:03:50 +01:00
Panagiotis Papadopoulos 8778ca5f6c fix(services): fix type errors 2025-01-24 00:02:45 +01:00
Panagiotis Papadopoulos 85c6764864 feat(services/config): add Sync config block and fallback values
original idea of using config-sample.ini for default values
is not backwards compatible unfortunately

fallback values taken from config-sample.ini and directly from other code in this codebase, where these values were set
2025-01-23 23:58:46 +01:00
Panagiotis Papadopoulos c7bd5e2c67 feat(services/config): add TriliumConfig Type 2025-01-23 23:50:16 +01:00
renovate[bot] a313b96ff8 chore(deps): update node.js to v22.13.1 2025-01-23 20:17:47 +00:00
Panagiotis Papadopoulos edeaca6d1f feat(services/config): add missing config values
these are used in the codebase, but are not defined in the sample.ini
2025-01-23 20:32:45 +01:00
Panagiotis Papadopoulos 279aa156cf feat(services/config): add possibility to read config from ENV
envToBoolean used to turn "string booleans" into JS booleans
2025-01-23 20:23:31 +01:00
Panagiotis Papadopoulos c0327bf8e2 feat(server/utils): add envToBoolean helper function
turns "true" / "false" strings from a process.env property into actual boolean values or undefined
2025-01-23 20:18:05 +01:00
Elian Doran f672747cfc chore(build): bump sync version 2025-01-23 16:52:22 +02:00
renovate[bot] 8150e394a4 chore(deps): update dependency electron to v34.0.1 2025-01-23 01:33:28 +00:00
Elian Doran 25dcc35b3b Merge pull request #1030 from TriliumNext/renovate/vitest-monorepo
chore(deps): update vitest monorepo to v3.0.3
2025-01-22 23:43:20 +02:00
Elian Doran 0b0fcc0685 Merge pull request #1021 from TriliumNext/chore_move-generate-documents
chore: move generate_documents  to bin folder
2025-01-22 23:42:45 +02:00
Elian Doran 4203a1160e Merge pull request #1018 from TriliumNext/fix_server-build
fix: fix server build not running due to missing file
2025-01-22 23:39:09 +02:00
Elian Doran 16b5eef650 Merge pull request #1017 from TriliumNext/feature/map_note_type
Map note type
2025-01-22 23:34:44 +02:00
Elian Doran 9e2c592383 feat(geomap): set default position 2025-01-22 23:18:22 +02:00
Elian Doran a8e2c2901b fix(geomap): error in creating empty map 2025-01-22 23:09:56 +02:00
Elian Doran 7a3a5141af fix(geomap): not working on electron 2025-01-22 23:08:25 +02:00
Elian Doran 474ae481b6 Merge branch 'develop' into feature/map_note_type 2025-01-22 22:50:38 +02:00
Elian Doran 0288ebcad9 feat(context_menu): dismiss note tooltip when a context menu is shown 2025-01-22 22:24:42 +02:00
Elian Doran d814a4d49f chore(i18n): translate geo map messages 2025-01-22 22:12:25 +02:00
Elian Doran 5a40d3f020 fix(build): build errors 2025-01-22 21:55:42 +02:00
Elian Doran e06db0038f fix(geomap): display again note tooltip 2025-01-22 21:49:23 +02:00
Elian Doran 9b1279ce14 feat(geo_map): add option to open location 2025-01-22 21:07:40 +02:00
Elian Doran 47b02da021 feat(geo_map): add back open context menu 2025-01-22 20:02:20 +02:00
Elian Doran 2e1ad24584 feat(geo_map): add option to remove from map 2025-01-22 19:33:53 +02:00
Elian Doran dbd38ecedf fix(geo_map): markers no longer rendering after clicking on a link 2025-01-22 18:57:09 +02:00
renovate[bot] cd22102dab chore(deps): update vitest monorepo to v3.0.3 2025-01-22 00:25:19 +00:00
Elian Doran ac262228f0 feat(geomap): note preview on tooltip 2025-01-21 22:10:20 +02:00
Elian Doran 65553250b5 fix(geomap): dismissing add with any key 2025-01-21 21:50:32 +02:00
Elian Doran 31491b957b feat(geomap): use persistent notification 2025-01-21 21:29:02 +02:00
Elian Doran b813b107a8 chore(geomap): change attribute to geolocation 2025-01-21 21:18:41 +02:00
Elian Doran dc7dd51913 refactor(geomap): simplify changing state 2025-01-21 21:06:36 +02:00
Elian Doran be4ee4c173 feat(geomap): dismiss adding with escape 2025-01-21 21:03:27 +02:00
Elian Doran c2cb07ed08 feat(geomap): dismiss creation if dialog is dismissed 2025-01-21 20:40:19 +02:00
Elian Doran d1aa0e5f50 feat(geomap): invert note creation workflow 2025-01-21 20:38:31 +02:00
Elian Doran 259dcdb568 feat(geomap): set custom icon for notes created from within the map 2025-01-21 19:53:23 +02:00
Elian Doran 6b906a91d7 feat(geomap): add labels to markers 2025-01-21 19:33:12 +02:00
Elian Doran 08722d5935 feat(geomap): add shadow to marker 2025-01-21 19:10:35 +02:00
Elian Doran 1be3492f67 style(geomap): improve alignment for marker icon 2025-01-21 19:02:36 +02:00
Elian Doran 016a9e4a99 fix(geomap): pixel perfect marker positioning 2025-01-21 19:01:22 +02:00
Elian Doran 087d4790f4 feat(geomap): setup marker based on note icon 2025-01-21 18:53:36 +02:00
Elian Doran b2a5f06646 feat(geomap): enable autopan for dragging markers 2025-01-21 15:39:20 +02:00
Elian Doran 04367de112 fix(geomap): duplicate markers 2025-01-21 14:23:59 +02:00
Elian Doran 3281bb8e9f feat(geomap): allow dragging 2025-01-21 14:17:04 +02:00
Elian Doran fed0598b47 feat(geomap): adjust cursor when adding new note 2025-01-21 13:46:18 +02:00
Panagiotis Papadopoulos 05b433d44e test(import/utils): remove leftover unused type 2025-01-21 00:33:45 +01:00
Panagiotis Papadopoulos 2296d1a6ba refactor(import/utils.handleH1): simplify handleH1 2025-01-21 00:25:46 +01:00
Panagiotis Papadopoulos 1de9bc7c6f fix(import/utils.handleH1): fix stripping of all <h1> tags that match title
now it will only strip the very first tag that if it matches the title,
otherwise it gets turned into a h2 tag

fixes #1016
2025-01-21 00:04:05 +01:00
Panagiotis Papadopoulos e1c949aa10 test(import/utils): add test for #handleH1/rework previous tests 2025-01-20 23:57:51 +01:00
Panagiotis Papadopoulos ba91082115 test(import/utils): add test for extractHtmlTitle 2025-01-20 23:04:16 +01:00
Elian Doran ef5b2d60f3 fix(geomap): multiple clicks creating multiple markers 2025-01-20 23:54:22 +02:00
Panagiotis Papadopoulos 1d03818337 chore(generate_documents): add note about script being broken currently 2025-01-20 22:54:12 +01:00
Elian Doran 986a1c25be feat(geomap): reload markers after adding a new note 2025-01-20 23:53:52 +02:00
Elian Doran f76b454d5a feat(geomap): load markers at startup 2025-01-20 23:27:32 +02:00
Elian Doran a3f257f3c5 feat(geomap): set location after creating a note 2025-01-20 23:14:31 +02:00
Elian Doran 2582924046 feat(geomap): add prompt for creating child note 2025-01-20 22:50:36 +02:00
Panagiotis Papadopoulos 3206a7fe73 chore: move generate_documents to bin folder
this file is never called in production code and wouldn't even run, even if it was:
the `lorem-ipsum` dependency is in devDependencies, so never gets installed in
any dist builds

move the file to a place where it makes more sense and avoid it getting packaged
without any reason.
2025-01-20 21:46:25 +01:00
Elian Doran f803b9f822 feat(geomap): add floating button section 2025-01-20 22:39:48 +02:00
Elian Doran 4d5e04fc5a feat(geomap): save & restore zoom 2025-01-20 22:21:59 +02:00
Elian Doran f66f437c8e feat(geomap): restore view coordinates 2025-01-20 22:19:07 +02:00
Elian Doran 5cefd4f50a feat(mindmap): save view center coordinates 2025-01-20 21:28:04 +02:00
Panagiotis Papadopoulos 8f09b6a192 fix: fix server build not running due to missing file
as discussed in #1010 – quick & dirty fix by copying over the file into the server side of things
Comment added to both files, about deduplication in the future

fixes #1010
2025-01-20 20:15:39 +01:00
Elian Doran 278870e06a Merge pull request #1014 from TriliumNext/refactor_import-mime
refactor: refactor and add tests for `services/import/mime`
2025-01-20 20:53:39 +02:00
Elian Doran d2c2d945aa Merge pull request #1015 from TriliumNext/test_fix-flaky-getPlatformAppDataDir
test(data_dir): fix flaky getPlatformAppDataDir test on Windows
2025-01-20 20:50:26 +02:00
Elian Doran 2b8ee31be3 refactor(geomap): skip module loader for JS 2025-01-20 20:36:58 +02:00
Panagiotis Papadopoulos ca8146413a test(data_dir): fix flaky getPlatformAppDataDir test on Windows
Delete the provided process.env.APPDATA on Windows, so that we can use our own values (one of which is "undefined", which was causing the getPlatformAppDataDir to fallback to the "real" process.env.APPDATA
-> causing failing test, when run on Windows
2025-01-20 18:57:43 +01:00
Elian Doran eca3955dc2 feat(geomap): add basic layer 2025-01-20 19:20:59 +02:00
Elian Doran 94a0403981 feat(geomap): load leaflet 2025-01-20 19:18:29 +02:00
Elian Doran e1952fe6b8 feat(geomap): create geomap note type 2025-01-20 18:45:56 +02:00
Panagiotis Papadopoulos 4be675c4e1 test(import/mime): add additional test case for getMime 2025-01-20 08:36:11 +01:00
Panagiotis Papadopoulos 4e59f58ce6 refactor(import/mime): simplify getMime 2025-01-20 08:36:11 +01:00
Panagiotis Papadopoulos 6a0edb68de refactor(import/mime): simplify getType 2025-01-20 08:36:11 +01:00
Panagiotis Papadopoulos 91ae4b629e refactor(import/mime): simplify normalizeMimeType 2025-01-20 08:36:11 +01:00
Panagiotis Papadopoulos 815929c376 refactor(import/mime): split CODE_MIME_TYPES Record into two separate objects
CODE_MIME_TYPES -> as a Set -> as we only care about the existance of those types
CODE_MIME_TYPES_OVERRIDE -> as a Map with those keys and the "overwrite" values as associated value

-> this way  we don't have to unnecessarily store additional boolean values for everything *but* those hand ful of mime types

-> also I've sorted the items alphabetically, while I was at it
2025-01-20 08:36:11 +01:00
Panagiotis Papadopoulos 1405e22f89 test(import/mime): add tests 2025-01-20 08:36:11 +01:00
Elian Doran 7f15f8a7de fix(mindmap): save direction as soon as it is changed (closes #986) 2025-01-19 23:48:30 +02:00
Elian Doran b47dc13ff1 fix(mindmap): regression on new mindmap 2025-01-19 23:38:20 +02:00
Elian Doran 9820e8aa12 fix(mindmap): use dynamic import instead of static one 2025-01-19 23:34:57 +02:00
Elian Doran fb0dda982f fix(mindmap): restore direction 2025-01-19 23:30:07 +02:00
Elian Doran 0292489001 Merge pull request #1004 from TriliumNext/test_simplify-data-dir
test(services/data_dir): simplify getPlatformAppDataDir
2025-01-19 23:14:19 +02:00
Elian Doran 55ce7aac10 Merge remote-tracking branch 'origin/develop' into test_simplify-data-dir 2025-01-19 23:13:56 +02:00
Elian Doran f36ec9b018 Merge pull request #1008 from TriliumNext/renovate/better-sqlite3-11.x-lockfile
fix(deps): update dependency better-sqlite3 to v11.8.1
2025-01-19 23:13:28 +02:00
Elian Doran 632cfb3b7f Merge pull request #954 from TriliumNext/renovate/better-sqlite3-11.x
fix(deps): update dependency better-sqlite3 to v11.8.1
2025-01-19 23:13:11 +02:00
Elian Doran 24475255d9 Merge pull request #1009 from TriliumNext/renovate/react-monorepo
fix(deps): update react monorepo
2025-01-19 23:11:34 +02:00
Elian Doran b913e4e090 Merge pull request #1012 from TriliumNext/fix_webpack-warning
fix(utils/dynamicRequire): get rid of Webpack Critical dependency warning
2025-01-19 15:59:15 +02:00
Panagiotis Papadopoulos d56752e5a9 fix(utils/dynamicRequire): pass moduleName explicitly as string
fixes the Webpack warning:
"WARNING in ./src/public/app/services/utils.ts 249:15-34
Critical dependency: the request of a dependency is an expression"

as we now explicitly pass the moduleName as string and not "expression"
source:
https://stackoverflow.com/questions/42908116/
2025-01-19 12:57:27 +01:00
renovate[bot] 5ddd88ee55 fix(deps): update dependency better-sqlite3 to v11.8.1 2025-01-19 04:47:01 +00:00
renovate[bot] 66ad3779df fix(deps): update react monorepo 2025-01-19 00:47:03 +00:00
renovate[bot] 6443b2ea66 fix(deps): update dependency better-sqlite3 to v11.8.1 2025-01-19 00:46:36 +00:00
Elian Doran 9025516806 Merge pull request #1006 from TriliumNext/chore_move-test-files
chore: clean up and move test files next to actual files
2025-01-18 23:49:02 +02:00
Panagiotis Papadopoulos c9619e1a1b chore(tests): fix type error in data_dir.spec 2025-01-18 20:45:01 +01:00
Panagiotis Papadopoulos 121e7b33ec chore(tests): comment out "todo" code in attribute_parser.spec
we now have more stricter TS rules being applied to the spec files, so `tsc` fails with these now.
and since this is set to TODO, because of outdated code,
let's comment out, instead of using //@ts-expect-error
2025-01-18 20:32:40 +01:00
Panagiotis Papadopoulos 67ee3c30fb chore(tests): remove useless test stub 2025-01-18 20:24:43 +01:00
Panagiotis Papadopoulos 09e0805d16 chore(tests): remove playwright demo spec
as discussed here:
https://github.com/orgs/TriliumNext/discussions/925#discussioncomment-11870786
2025-01-18 20:22:33 +01:00
Panagiotis Papadopoulos 627b2faf0b chore(tests): move remaining valid tests next to actual files 2025-01-18 20:20:30 +01:00
Panagiotis Papadopoulos f803bd6140 chore(tests): move becca_mocking to spec/support 2025-01-18 20:16:02 +01:00
Panagiotis Papadopoulos 49246ee456 chore(tests): move tests next to actual file 2025-01-18 20:15:29 +01:00
Panagiotis Papadopoulos 0ccf91721d test(services/data_dir): simplify getPlatformAppDataDir
use the new available mocks to make tests a tiny bit more simpler :-)
2025-01-18 19:17:00 +01:00
Elian Doran 658317799d feat(options_init): enable fixed editing toolbar by default for new users 2025-01-18 19:38:41 +02:00
Elian Doran 5899ba9a57 Revert "feat(options_init): enable horizontal layout by default for new users"
This reverts commit 637d1df0f5.
2025-01-18 19:37:09 +02:00
Elian Doran ef1ab56f51 chore(i18n): translate two more messages in Romanian 2025-01-18 19:20:49 +02:00
Elian Doran 637d1df0f5 feat(options_init): enable horizontal layout by default for new users 2025-01-18 19:16:26 +02:00
Elian Doran 4e9aafc919 fix(demo): broken TODOs (closes #984) 2025-01-18 19:02:02 +02:00
Elian Doran 0a0707434e feat(mobile): allow resetting launch bar config 2025-01-18 18:49:36 +02:00
Elian Doran 1369cd8b16 feat(client): implement move to available/visible launchers on mobile as well 2025-01-18 18:45:13 +02:00
Elian Doran b367862fe8 fix(client): launch bar move commands not working (fixes #989) 2025-01-18 18:02:26 +02:00
Elian Doran 3090233078 Merge pull request #998 from TriliumNext/test_data-dir-getTriliumDataDir
test(data_dir): add missing getTriliumDataDir tests
2025-01-18 17:04:28 +02:00
Elian Doran 981e105de8 Merge pull request #999 from TriliumNext/test_fix-search-spec-msg
test(spec/search): fix expect error message printing
2025-01-18 17:03:25 +02:00
Panagiotis Papadopoulos c5d4df11a5 test(spec/search): fix expect error message printing
adjusted message to be printed correctly and adjusted wording ->
previously it was printing "[object object]",
since we are dealing with an  array of objects
2025-01-18 13:55:27 +01:00
Panagiotis Papadopoulos cbc10e1f15 chore(prettier): fix data_dir.spec.ts 2025-01-18 13:21:45 +01:00
Panagiotis Papadopoulos dba0ef4945 test(services/data_dir): make test descriptions a bit more readable 2025-01-18 13:21:45 +01:00
Panagiotis Papadopoulos 31fb8eb3d3 test(services/data_dir): add getTriliumDataDir tests
since we *need* mocks to tests getTriliumDataDir, I had to change a bit the setup structure of the tests as well -> functions needs to be
imported dynamically *after* the mocks are registered
2025-01-18 13:21:45 +01:00
Elian Doran c7e688b6e6 chore(client/ts): port left_pane_container 2025-01-18 12:56:00 +02:00
Elian Doran 8e3faffd35 fix(left-pane): preferred width is not restored (closes #990) 2025-01-18 12:54:59 +02:00
Elian Doran 148e166f5e fix(electron): mime type not always checkable in settings 2025-01-18 12:44:51 +02:00
Elian Doran 0ae5453717 feat(settings): disable unchecking of plain text mime 2025-01-18 12:36:23 +02:00
Elian Doran 0a611ac4bb fix(settings): list of mime types disappearing one by one (fixes #988) 2025-01-18 12:33:18 +02:00
Elian Doran 54654322e2 chore(github): treat libraries as vendored 2025-01-18 11:22:17 +02:00
Elian Doran eaacdb57d1 fix(build): add back copying canvas dist files 2025-01-18 11:19:35 +02:00
Elian Doran f2382e2ef5 refactor(canvas): solve API deprecation 2025-01-18 11:15:54 +02:00
Elian Doran 7d3f506efb refactor(deps): use webpack import for canvas 2025-01-18 11:09:57 +02:00
Elian Doran ab65913e52 Merge pull request #980 from TriliumNext/renovate/npm-katex-vulnerability
fix(deps): update dependency katex to v0.16.21 [security]
2025-01-18 09:44:05 +02:00
Elian Doran 134c056667 chore(e2e): disable firefox tests for now 2025-01-18 09:42:13 +02:00
renovate[bot] 02250a842f fix(deps): update dependency katex to v0.16.21 [security] 2025-01-18 00:36:50 +00:00
Elian Doran ac30e00c02 refactor(client): remove redundant variable 2025-01-18 01:24:21 +02:00
Elian Doran 0776f17ebb chore(types): remove redundant interface 2025-01-18 01:24:04 +02:00
Elian Doran 3db93cdf24 chore(client/ts): port note_icon 2025-01-18 01:14:47 +02:00
Elian Doran 043d92a1ab feat(revisions): add spacing between help and delete (closes #974) 2025-01-18 01:02:03 +02:00
Elian Doran 5985519570 fix(mobile): tab bar context menu (closes #969) 2025-01-18 00:51:59 +02:00
Elian Doran 7c7fd044c6 chore(client/ts): port canvas 2025-01-18 00:42:19 +02:00
Elian Doran 2167948509 fix(canvas): significant lag when dragging (fixes #967) 2025-01-18 00:14:31 +02:00
Elian Doran 7cf1fbf088 fix(mobile): position of note detail menu (closes #971) 2025-01-18 00:04:06 +02:00
Elian Doran e3bcd75a5a chore(client/ts): port mobile_detail_menu 2025-01-17 23:55:46 +02:00
Elian Doran 229f473f8b style(next): alignment of dropdown keyboard shortcuts 2025-01-17 23:47:29 +02:00
Elian Doran 11009a5af8 fix(client): positioning of keyboard shortcuts on legacy theme (closes #685) 2025-01-17 23:46:19 +02:00
Elian Doran fdbc33aa23 fix(client): alignment of "Advanced" option in global menu 2025-01-17 23:43:06 +02:00
Elian Doran d9335bec37 fix(client): shortcuts wrapped on two lines on global menu (closes #976) 2025-01-17 23:37:34 +02:00
Elian Doran 47335062a4 feat(e2e): test going back to share root 2025-01-17 23:21:12 +02:00
Elian Doran fd2779383d Merge branch 'share-fix' into develop 2025-01-17 23:20:09 +02:00
Elian Doran 59d8def2c5 Merge remote-tracking branch 'origin/develop' into share-fix 2025-01-17 23:19:53 +02:00
Elian Doran 6ea8a31a22 feat(e2e): test going to share root 2025-01-17 23:13:55 +02:00
Elian Doran f96ab6da57 Merge pull request #932 from TriliumNext/renovate/katex-0.x
fix(deps): update dependency katex to v0.16.20
2025-01-17 23:01:05 +02:00
Elian Doran b376f090d8 fix(e2e): bring back help link tests 2025-01-17 22:58:00 +02:00
Elian Doran b4ff765c5e chore(e2e): use ControlOrMeta instead of Control 2025-01-17 22:58:00 +02:00
Elian Doran 423bea923c Merge pull request #979 from perfectra1n/develop
Update build-server.sh and GitHub Actions to build ARM server
2025-01-17 22:54:01 +02:00
Jon Fuller 7e30ab2e06 Update build-server.sh and GitHub Actions to build ARM server 2025-01-17 20:45:00 +00:00
Elian Doran 6452fa123c Merge branch 'develop' into renovate/katex-0.x 2025-01-17 22:29:58 +02:00
Elian Doran d19e9b4681 fix(test): wrong jasmine-based context 2025-01-17 22:29:44 +02:00
Elian Doran 3f92966976 Merge pull request #938 from TriliumNext/renovate/force-graph-1.x
fix(deps): update dependency force-graph to v1.49.0
2025-01-17 22:23:13 +02:00
Elian Doran fec0ae62b7 Merge branch 'develop' into renovate/katex-0.x 2025-01-17 22:22:53 +02:00
Elian Doran 7fc5f9ab83 Merge pull request #977 from pano9000/test_vitest
test: add vitest as test framework and port current tests
2025-01-17 22:22:00 +02:00
Elian Doran 33baf6c917 fix(e2e): bring back katex test 2025-01-17 22:16:08 +02:00
Panagiotis Papadopoulos 6401f35783 chore(deps): update vitest to v3 2025-01-17 20:33:34 +01:00
Elian Doran 4dfe2b4935 Merge branch 'develop' into renovate/force-graph-1.x 2025-01-17 21:25:56 +02:00
Elian Doran dd3397bcbb chore(client/ts): port note_map 2025-01-17 21:25:36 +02:00
Panagiotis Papadopoulos 8b34c149cc chore: remove unused jasmine.json file 2025-01-17 20:24:20 +01:00
Panagiotis Papadopoulos fb47ecd747 test(data_dir): fix failing test due to vitest being "stricter" than jasmine 2025-01-17 20:24:20 +01:00
Panagiotis Papadopoulos 9f148750d4 chore(vitest): add additional folders to exclude 2025-01-17 20:24:20 +01:00
Panagiotis Papadopoulos 853a7abc4f test: port data_dir.spec.ts to vitest 2025-01-17 20:24:20 +01:00
Panagiotis Papadopoulos 8faff118b1 chore: update npm test scripts with vitest 2025-01-17 20:24:20 +01:00
Panagiotis Papadopoulos 33274ada65 test: port spec tests to vitest 2025-01-17 20:24:20 +01:00
Panagiotis Papadopoulos 6be7109ce3 test: add initial, basic vitest.config.ts 2025-01-17 20:24:20 +01:00
Panagiotis Papadopoulos 0ed06e76c3 test: delete now unused mini_test.ts 2025-01-17 20:24:20 +01:00
Panagiotis Papadopoulos 186f17ec3e chore: add "coverage" folder to gitignore 2025-01-17 20:24:20 +01:00
Panagiotis Papadopoulos 871ee7989e test: set attribute_parser test to "TODO"
the tests are not correctly calling the `parse` method currently
2025-01-17 20:24:20 +01:00
Panagiotis Papadopoulos 3f584af171 test: port spec-es6 tests to vitest 2025-01-17 20:24:20 +01:00
Panagiotis Papadopoulos 325a3b6c83 deps: add vitest and @vitest/coverage-v8 2025-01-17 20:24:16 +01:00
Elian Doran e7eb385b8f refactor(deps): integrate force-graph into webpack 2025-01-17 20:21:52 +02:00
Elian Doran 8d026c8134 feat(e2e): basic test for note map rendering 2025-01-17 20:15:48 +02:00
Elian Doran 7e40200436 chore(deps): remove unnecessary iconsur 2025-01-17 19:53:10 +02:00
Elian Doran 0cee8f9f20 refactor(mermaid): use webpack import for mermaid-elk 2025-01-17 19:53:09 +02:00
renovate[bot] 32c4d7350c fix(deps): update dependency force-graph to v1.49.0 2025-01-17 17:52:23 +00:00
Elian Doran af1d6540bc Merge pull request #955 from TriliumNext/renovate/fs-extra-11.x
fix(deps): update dependency fs-extra to v11.3.0
2025-01-17 19:51:42 +02:00
Elian Doran 54bf2ad9ff feat(e2e): test standard flowchart rendering 2025-01-17 19:19:24 +02:00
Elian Doran 751adf474f feat(e2e): check ELK flowchart 2025-01-17 19:09:19 +02:00
Elian Doran a1b660cfcd fix(e2e): mindmap test not being run 2025-01-17 18:49:22 +02:00
Elian Doran 66c367fe21 Merge pull request #947 from TriliumNext/bugfix/dump-db-ivlength
Fix default ivLength in dump-db tool
2025-01-17 18:39:30 +02:00
Elian Doran 3abe5a348f Merge pull request #965 from pano9000/chore_replace-ts-node
chore: get rid of ts-node
2025-01-17 18:38:35 +02:00
Elian Doran 467852191d Merge pull request #966 from pano9000/fix_csrf-electron-httpOnly
fix(csrf): add exception for electron for httpOnly cookie
2025-01-17 18:36:02 +02:00
Panagiotis Papadopoulos 9382c278b3 fix(csrf): add exception for electron for httpOnly cookie
it does not seem to like having httpOnly set in electron
2025-01-17 17:26:52 +01:00
Panagiotis Papadopoulos 0e33395c47 chore(deps): remove now unused ts-node 2025-01-17 08:09:42 +01:00
Panagiotis Papadopoulos 00bdcfa803 chore(mermaid-elk): replace loader-register with tsx
unnecessary "cross-env" also removed, as it wasn't even
used to set any env variables
2025-01-17 08:05:42 +01:00
Panagiotis Papadopoulos 331dae4eb0 chore: delete unused loader-register file 2025-01-17 08:02:26 +01:00
Panagiotis Papadopoulos 748e30b63c chore(scripts): use tsx for webpack 2025-01-16 23:52:51 +01:00
Panagiotis Papadopoulos 76a0bffcc3 chore(scripts): remove unnecessary cross-env 2025-01-16 23:08:57 +01:00
Caleb Norton b8f15d2fe3 Fix parent share link 2025-01-16 15:28:30 -06:00
Elian Doran b2e1a3e97a Merge pull request #961 from pano9000/fix-csrf-settings
fix(csrf): set more secure csrf related settings
2025-01-16 23:03:43 +02:00
Panagiotis Papadopoulos 5f605b3a91 fix(csrf): set more secure cookieOptions settings
- `sameSite` - previous setting inherited from csurf was to simply not set it at all, which makes all browser nag in their dev console output.
They will default to "Lax" for these type of cookies in the future.
We can even use "strict" here though for our use case:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value

- `httpOnly`: should be enabled for the csrf cookie as well
for the session cookie it already is enabled.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#httponly
2025-01-16 21:40:12 +01:00
Panagiotis Papadopoulos ec19ccd7a7 fix(csrf): stop leaking the CSRF token in the server logs
As per OWASP:
"A CSRF token must not be leaked in the server logs or in the URL.", see:
https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#transmissing-csrf-tokens-in-synchronized-patterns
2025-01-16 21:16:33 +01:00
Elian Doran 283a12b0d5 Merge pull request #960 from pano9000/fix_csrf-csrf_existing_cookie
fix(csrf): fix handling of existing _csrf cookies
2025-01-16 21:53:09 +02:00
Panagiotis Papadopoulos 139bf3dcdf fix(csrf): use generateCsrfToken with more "user friendly" settings
fixes the case, where existing TriliumNext users, will get
a "Invalid CSRF Token" Message, when they have an older
_csrf token in their cookies from a previous installation/visit.
the settings now will handle these cases in the background automatically.

also fixes #950
2025-01-16 20:14:23 +01:00
Panagiotis Papadopoulos 6dd8ab31d5 refactor(csrf): export generateToken utility 2025-01-16 20:14:23 +01:00
Panagiotis Papadopoulos e3d89ce2a5 refactor(csrf): move csrf to own file 2025-01-16 20:14:23 +01:00
Elian Doran edc6b983ac Merge remote-tracking branch 'origin/master' into develop 2025-01-16 18:36:35 +02:00
Elian Doran d684440c1f fix(client): undefined entity in some cases 2025-01-16 18:36:29 +02:00
Elian Doran 1e182f5820 chore(client/ts): port mermaid 2025-01-16 18:20:23 +02:00
Elian Doran c8b745bc6a Merge pull request #952 from TriliumNext/renovate/node-22.x
chore(deps): update dependency @types/node to v22.10.7
2025-01-16 10:49:07 +02:00
Elian Doran d1be673763 Merge pull request #953 from TriliumNext/renovate/better-sqlite3-11.x-lockfile
fix(deps): update dependency better-sqlite3 to v11.8.0
2025-01-16 10:45:59 +02:00
Elian Doran 2fbfc9d668 Merge pull request #946 from TriliumNext/renovate/electron-34.x
chore(deps): update dependency electron to v34
2025-01-16 10:43:33 +02:00
Nriver 5ea3e67dc3 remove unused param 2025-01-16 14:18:01 +08:00
Nriver 4b7445be8e fix compatibility for old encrypted data 2025-01-16 14:01:59 +08:00
renovate[bot] a9570965f1 fix(deps): update dependency fs-extra to v11.3.0 2025-01-16 00:57:03 +00:00
renovate[bot] b77f8aeb43 fix(deps): update dependency better-sqlite3 to v11.8.0 2025-01-16 00:56:43 +00:00
renovate[bot] 01b88c52ef chore(deps): update dependency @types/node to v22.10.7 2025-01-16 00:56:32 +00:00
Elian Doran 4c451753bc chore(build): remove redundant npx 2025-01-15 19:11:26 +02:00
Elian Doran 7076c4cbd6 chore(deps): update better-sqlite3 to 11.8.0 2025-01-15 19:11:05 +02:00
Nriver 9d32cd36ee Fix default ivLength in dump-db tool 2025-01-15 10:16:04 +08:00
renovate[bot] 0eab68e8d1 chore(deps): update dependency electron to v34 2025-01-15 02:07:18 +00:00
renovate[bot] 9b765b458c fix(deps): update dependency katex to v0.16.20 2025-01-13 00:42:04 +00:00
160 changed files with 4747 additions and 3006 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
package-lock.json linguist-generated=true
**/package-lock.json linguist-generated=true
**/package-lock.json linguist-generated=true
libraries/** linguist-vendored
+18 -8
View File
@@ -72,9 +72,18 @@ jobs:
with:
name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }}.${{matrix.os.extension}}
path: upload/*.${{ matrix.os.extension }}
build_linux_server-x64:
name: Build Linux Server x86_64
runs-on: ubuntu-latest
build_linux_server:
name: Build Linux Server
strategy:
fail-fast: false
matrix:
arch: [x64, arm64]
include:
- arch: x64
runs-on: ubuntu-latest
- arch: arm64
runs-on: ubuntu-24.04-arm
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v4
- name: Set up node & dependencies
@@ -84,17 +93,18 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run Linux server build (x86_64)
- name: Run Linux server build
env:
MATRIX_ARCH: ${{ matrix.arch }}
run: |
npm run update-build-info
./bin/build-server.sh
- name: Prepare artifacts
if: runner.os != 'windows'
run: |
mkdir -p upload
file=$(find dist -name '*.tar.xz' -print -quit)
cp "$file" "upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz"
cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
- uses: actions/upload-artifact@v4
with:
name: TriliumNextNotes linux server x64
path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz
name: TriliumNextNotes linux server ${{ matrix.arch }}
path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz
+16 -8
View File
@@ -92,7 +92,16 @@ jobs:
asset_content_type: application/zip # required by GitHub API
nightly-server:
name: Deploy server nightly
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch: [x64, arm64]
include:
- arch: x64
runs-on: ubuntu-latest
- arch: arm64
runs-on: ubuntu-24.04-arm
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v4
- name: Set up node & dependencies
@@ -102,22 +111,21 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run Linux server build (x86_64)
- name: Run Linux server build
env:
MATRIX_ARCH: ${{ matrix.arch }}
run: |
npm run update-build-info
npm run ci-update-nightly-version
./bin/build-server.sh
- name: Prepare artifacts
if: runner.os != 'windows'
run: |
mkdir -p upload
file=$(find dist -name '*.tar.xz' -print -quit)
cp "$file" "upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz"
cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
- uses: actions/upload-artifact@v4
with:
name: TriliumNextNotes linux server x64
path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz
overwrite: true
name: TriliumNextNotes linux server ${{ matrix.arch }}
path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz
- name: Deploy release
uses: WebFreak001/deploy-nightly@v3.2.0
+15 -5
View File
@@ -66,8 +66,17 @@ jobs:
fail_on_unmatched_files: true
files: upload/*.*
build_linux_server-x64:
name: Build Linux Server x86_64
runs-on: ubuntu-latest
name: Build Linux Server
strategy:
fail-fast: false
matrix:
arch: [x64, arm64]
include:
- arch: x64
runs-on: ubuntu-latest
- arch: arm64
runs-on: ubuntu-24.04-arm
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v4
- name: Set up node & dependencies
@@ -77,16 +86,17 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run Linux server build (x86_64)
- name: Run Linux server build
env:
MATRIX_ARCH: ${{ matrix.arch }}
run: |
npm run update-build-info
./bin/build-server.sh
- name: Prepare artifacts
if: runner.os != 'windows'
run: |
mkdir -p upload
file=$(find dist -name '*.tar.xz' -print -quit)
cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-server-linux-x64.tar.xz"
cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
- name: Publish release
uses: softprops/action-gh-release@v2
with:
+1
View File
@@ -2,6 +2,7 @@
node_modules/
dist/
build/
coverage/
src/public/app-dist/
npm-debug.log
yarn-error.log
-1
View File
@@ -1,2 +1 @@
save-prefix = ''
legacy-peer-deps = true
+2 -3
View File
@@ -1,5 +1,5 @@
# Build stage
FROM node:22.13.0-bullseye-slim AS builder
FROM node:22.13.1-bullseye-slim AS builder
# Configure build dependencies in a single layer
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -28,7 +28,6 @@ RUN cp -R build/src/* src/. && \
npm run webpack && \
npm prune --omit=dev && \
npm cache clean --force && \
cp src/public/app/share.js src/public/app-dist/. && \
cp -r src/public/app/doc_notes src/public/app-dist/. && \
rm -rf src/public/app/* && \
mkdir -p src/public/app/services && \
@@ -37,7 +36,7 @@ RUN cp -R build/src/* src/. && \
rm -r build
# Runtime stage
FROM node:22.13.0-bullseye-slim
FROM node:22.13.1-bullseye-slim
# Install only runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
+2 -3
View File
@@ -1,5 +1,5 @@
# Build stage
FROM node:22.13.0-alpine AS builder
FROM node:22.13.1-alpine AS builder
# Configure build dependencies
RUN apk add --no-cache --virtual .build-dependencies \
@@ -27,7 +27,6 @@ RUN cp -R build/src/* src/. && \
npm run webpack && \
npm prune --omit=dev && \
npm cache clean --force && \
cp src/public/app/share.js src/public/app-dist/. && \
cp -r src/public/app/doc_notes src/public/app-dist/. && \
rm -rf src/public/app && \
mkdir -p src/public/app/services && \
@@ -36,7 +35,7 @@ RUN cp -R build/src/* src/. && \
rm -r build
# Runtime stage
FROM node:22.13.0-alpine
FROM node:22.13.1-alpine
# Install runtime dependencies
RUN apk add --no-cache su-exec shadow
+43 -7
View File
@@ -2,21 +2,57 @@
set -e # Fail on any command error
PKG_DIR=dist/trilium-linux-x64-server
# Debug output
echo "Matrix Arch: $MATRIX_ARCH"
# Detect architecture from matrix input, fallback to system architecture
if [ -n "$MATRIX_ARCH" ]; then
ARCH=$MATRIX_ARCH
else
ARCH=$(uname -m)
# Convert system architecture to our naming convention
case $ARCH in
x86_64) ARCH="x64" ;;
aarch64) ARCH="arm64" ;;
esac
fi
# Debug output
echo "Selected Arch: $ARCH"
# Set Node.js version and architecture-specific filename
NODE_VERSION=20.15.1
NODE_ARCH=$ARCH
# Debug output
echo "Node arch: $NODE_ARCH"
# Special case for x64 in Node.js downloads
if [ "$NODE_ARCH" = "x64" ]; then
NODE_FILENAME="x64"
elif [ "$NODE_ARCH" = "arm64" ]; then
NODE_FILENAME="arm64"
fi
# Debug output
echo "Node filename: $NODE_FILENAME"
PKG_DIR=dist/trilium-linux-${ARCH}-server
echo "Package directory: $PKG_DIR"
if [ "$1" != "DONTCOPY" ]
then
./bin/copy-trilium.sh $PKG_DIR
# Need to modify copy-trilium.sh to accept the target directory
./bin/copy-trilium.sh "$PKG_DIR"
fi
cd dist
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz
tar xfJ node-v${NODE_VERSION}-linux-x64.tar.xz
rm node-v${NODE_VERSION}-linux-x64.tar.xz
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
tar xfJ node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
rm node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
cd ..
mv dist/node-v${NODE_VERSION}-linux-x64 $PKG_DIR/node
mv dist/node-v${NODE_VERSION}-linux-${NODE_FILENAME} $PKG_DIR/node
rm -r $PKG_DIR/node/lib/node_modules/npm
rm -r $PKG_DIR/node/include/node
@@ -37,4 +73,4 @@ VERSION=`jq -r ".version" package.json`
cd dist
tar cJf trilium-linux-x64-server-${VERSION}.tar.xz trilium-linux-x64-server
tar cJf trilium-linux-${ARCH}-server-${VERSION}.tar.xz trilium-linux-${ARCH}-server
+2 -2
View File
@@ -76,7 +76,6 @@ const copy = async () => {
"node_modules/@excalidraw/excalidraw/dist/",
"node_modules/katex/dist/",
"node_modules/dayjs/",
"node_modules/force-graph/dist/",
"node_modules/boxicons/css/",
"node_modules/boxicons/fonts/",
"node_modules/mermaid/dist/",
@@ -101,7 +100,8 @@ const copy = async () => {
"node_modules/codemirror/keymap/",
"node_modules/mind-elixir/dist/",
"node_modules/@highlightjs/cdn-assets/languages",
"node_modules/@highlightjs/cdn-assets/styles"
"node_modules/@highlightjs/cdn-assets/styles",
"node_modules/leaflet/dist"
];
for (const folder of nodeModulesFolder) {
-1
View File
@@ -68,7 +68,6 @@ find $DIR -name "*.ts" -type f -delete
d="$DIR"/src/public
[[ -d "$d"/app-dist ]] || mkdir -pv "$d"/app-dist
cp "$d"/app/share.js "$d"/app-dist/
cp -r "$d"/app/doc_notes "$d"/app-dist/
rm -rf "$d"/app
@@ -1,15 +1,15 @@
/**
* Usage: node src/tools/generate_document.js 1000
* Usage: tsx ./generate_document.ts 1000
* will create 1000 new notes and some clones into the current document.db
*/
import sqlInit from "../services/sql_init.js";
import noteService from "../services/notes.js";
import attributeService from "../services/attributes.js";
import cls from "../services/cls.js";
import cloningService from "../services/cloning.js";
import sqlInit from "../src/services/sql_init.js";
import noteService from "../src/services/notes.js";
import attributeService from "../src/services/attributes.js";
import cls from "../src/services/cls.js";
import cloningService from "../src/services/cloning.js";
import loremIpsum from "lorem-ipsum";
import "../becca/entity_constructor.js";
import "../src/becca/entity_constructor.js";
const noteCount = parseInt(process.argv[2]);
@@ -90,4 +90,6 @@ async function start() {
process.exit(0);
}
sqlInit.dbReady.then(cls.wrap(start));
// @TriliumNextTODO sqlInit.dbReady never seems to resolve so program hangs
// see https://github.com/TriliumNext/Notes/issues/1020
sqlInit.dbReady.then(cls.wrap(start)).catch((err) => console.error(err));
+5
View File
@@ -27,3 +27,8 @@ keyPath=
# once set, expressjs will use the X-Forwarded-For header set by the rev. proxy to determinate the real IPs of clients.
# expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
trustedReverseProxy=false
[Sync]
#syncServerHost=
#syncServerTimeout=
#syncServerProxy=
BIN
View File
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

+1 -4
View File
@@ -1,8 +1,5 @@
import http from "http";
import ini from "ini";
import fs from "fs";
import dataDir from "./src/services/data_dir.js";
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
import config from "./src/services/config.js";
if (config.Network.https) {
// built-in TLS (terminated by trilium) is not supported yet, PRs are welcome
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

+1 -1
View File
@@ -12,7 +12,7 @@ function getDataKey(password: any) {
const encryptedDataKey = getOption("encryptedDataKey");
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey, 16);
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey);
return decryptedDataKey;
} catch (e: any) {
+3 -1
View File
@@ -16,7 +16,7 @@ function decryptString(dataKey: any, cipherText: any) {
return str;
}
function decrypt(key: any, cipherText: any, ivLength = 13) {
function decrypt(key: any, cipherText: any) {
if (cipherText === null) {
return null;
}
@@ -27,6 +27,8 @@ function decrypt(key: any, cipherText: any, ivLength = 13) {
try {
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017
const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13;
const iv = cipherTextBufferWithIv.slice(0, ivLength);
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
+6 -6
View File
@@ -464,9 +464,9 @@
"license": "MIT"
},
"node_modules/better-sqlite3": {
"version": "11.7.2",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.7.2.tgz",
"integrity": "sha512-10a57cHVDmfNQS4jrZ9AH2t+2ekzYh5Rhbcnb4ytpmYweoLdogDmyTt5D+hLiY9b44Mx9foowb/4iXBTO2yP3Q==",
"version": "11.8.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz",
"integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==",
"hasInstallScript": true,
"dependencies": {
"bindings": "^1.5.0",
@@ -1516,9 +1516,9 @@
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"better-sqlite3": {
"version": "11.7.2",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.7.2.tgz",
"integrity": "sha512-10a57cHVDmfNQS4jrZ9AH2t+2ekzYh5Rhbcnb4ytpmYweoLdogDmyTt5D+hLiY9b44Mx9foowb/4iXBTO2yP3Q==",
"version": "11.8.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz",
"integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==",
"requires": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
+27
View File
@@ -0,0 +1,27 @@
import { test, expect, Page } from "@playwright/test";
import App from "./support/app";
test("Help popup", async ({ page, context }) => {
page.setDefaultTimeout(15_000);
const app = new App(page, context);
await app.goto();
const popupPromise = page.waitForEvent("popup");
await app.currentNoteSplit.press("F1");
await page.getByRole("link", { name: "online↗" }).click();
const popup = await popupPromise;
expect(popup.url()).toBe("https://triliumnext.github.io/Docs/");
});
test("Complete help in search", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.launcherBar.locator(".bx-search").first().click();
await app.currentNoteSplit.locator(".search-settings .bx-help-circle").click();
const popupPromise = page.waitForEvent("popup");
await page.getByRole("link", { name: "complete help on search syntax" }).click();
const popup = await popupPromise;
expect(popup.url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
});
+67
View File
@@ -0,0 +1,67 @@
import { test, expect, Page, BrowserContext } from "@playwright/test";
import App from "../support/app";
test("renders ELK flowchart", async ({ page, context }) => {
await testAriaSnapshot({
page, context,
noteTitle: "Flowchart ELK on",
snapshot: `
- document:
- paragraph: A
- paragraph: B
- paragraph: C
- paragraph: Guarantee
- paragraph: User attributes
- paragraph: Master data
- paragraph: Exchange Rate
- paragraph: Profit Centers
- paragraph: Vendor Partners
- paragraph: Work Situation
- paragraph: Customer
- paragraph: Profit Centers
- paragraph: Guarantee
- text: Interfaces for B
`
})
});
test("renders standard flowchart", async ({ page, context }) => {
await testAriaSnapshot({
page, context,
noteTitle: "Flowchart ELK off",
snapshot: `
- document:
- paragraph: Guarantee
- paragraph: User attributes
- paragraph: Master data
- paragraph: Exchange Rate
- paragraph: Profit Centers
- paragraph: Vendor Partners
- paragraph: Work Situation
- paragraph: Customer
- paragraph: Profit Centers
- paragraph: Guarantee
- paragraph: A
- paragraph: B
- paragraph: C
- text: Interfaces for B
`
})
});
interface AriaTestOpts {
page: Page;
context: BrowserContext;
noteTitle: string;
snapshot: string;
}
async function testAriaSnapshot({ page, context, noteTitle, snapshot }: AriaTestOpts) {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab(noteTitle);
const svgData = app.currentNoteSplit.locator(".mermaid-render svg");
await expect(svgData).toBeVisible();
await expect(svgData).toMatchAriaSnapshot(snapshot);
}
@@ -1,4 +1,4 @@
import { test, expect, Page } from "@playwright/test";
import { test, expect } from "@playwright/test";
import App from "../support/app";
test("displays simple map", async ({ page, context }) => {
+9
View File
@@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
import App from "../support/app";
test("renders global map", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.launcherBar.locator(".launcher-button.bx-map-alt").click();
await expect(app.currentNoteSplit.locator(".force-graph-container canvas")).toBeVisible();
});
+17
View File
@@ -49,3 +49,20 @@ test("Highlights list is displayed", async ({ page, context }) => {
await expect(rootList.locator("li").nth(index++)).toContainText(highlightedEl);
}
});
test("Displays math popup", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Empty text");
const noteContent = app.currentNoteSplit.locator(".note-detail-editable-text-editor")
await noteContent.fill("Hello world");
await noteContent.press("ControlOrMeta+M");
const mathForm = page.locator(".ck-math-form");
await expect(mathForm).toBeVisible();
await mathForm.locator(".ck-input").first().fill("e=mc^2");
const preview = page.locator('[id^="math-preview"]');
await expect(preview).toMatchAriaSnapshot("- math: e = m c 2");
});
+18
View File
@@ -0,0 +1,18 @@
import { test, expect, Page } from "@playwright/test";
import App from "./support/app";
test("Goes to share root", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ url: "/share" });
const noteTitle = "Shared notes";
await expect(page).toHaveTitle(noteTitle);
await expect(page.locator("h1")).toHaveText(noteTitle);
});
test("Goes to parent share root page", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ url: "/share/bKMn5EFv9KS2" });
await expect(page.locator("h1")).toHaveText("Child note");
await page.locator("#parentLink a").click();
await page.waitForURL("/share/");
});
+15 -6
View File
@@ -2,6 +2,7 @@ import { expect, Locator, Page } from "@playwright/test";
import type { BrowserContext } from "@playwright/test";
interface GotoOpts {
url?: string;
isMobile?: boolean;
}
@@ -13,6 +14,7 @@ export default class App {
readonly tabBar: Locator;
readonly noteTree: Locator;
readonly launcherBar: Locator;
readonly currentNoteSplit: Locator;
readonly sidebar: Locator;
@@ -22,25 +24,32 @@ export default class App {
this.tabBar = page.locator(".tab-row-widget-container");
this.noteTree = page.locator(".tree-wrapper");
this.launcherBar = page.locator("#launcher-container");
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)")
this.sidebar = page.locator("#right-pane");
}
async goto(opts: GotoOpts = {}) {
async goto({ url, isMobile }: GotoOpts = {}) {
await this.context.addCookies([
{
url: BASE_URL,
name: "trilium-device",
value: opts.isMobile ? "mobile" : "desktop"
value: isMobile ? "mobile" : "desktop"
}
]);
await this.page.goto("/", { waitUntil: "networkidle" });
if (!url) {
url = "/";
}
await this.page.goto(url, { waitUntil: "networkidle" });
// Wait for the page to load.
await expect(this.page.locator(".tree"))
.toContainText("Trilium Integration Test");
await this.closeAllTabs();
if (url === "/") {
await expect(this.page.locator(".tree"))
.toContainText("Trilium Integration Test");
await this.closeAllTabs();
}
}
async goToNoteInNewTab(noteTitle: string) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.
-23
View File
@@ -1,23 +0,0 @@
import test, { expect } from "@playwright/test";
test("Help popup", async ({ page }) => {
await page.goto("http://localhost:8082");
await page.getByText("Trilium Integration Test DB").click();
await page.locator("body").press("F1");
await page.getByRole("link", { name: "online↗" }).click();
expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/");
});
test("Complete help in search", async ({ page }) => {
await page.goto("http://localhost:8082");
// Clear all tabs
await page.locator(".note-tab:first-of-type").locator("div").nth(1).click({ button: "right" });
await page.getByText("Close all tabs").click();
await page.locator("#launcher-container").getByRole("button", { name: "" }).first().click();
await page.getByRole("cell", { name: " " }).locator("span").first().click();
await page.getByRole("button", { name: "complete help on search syntax" }).click();
expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
});
-17
View File
@@ -1,17 +0,0 @@
import { test, expect } from "@playwright/test";
const ROOT_URL = "http://localhost:8080";
const LOGIN_PASSWORD = "eliandoran";
test("Can insert equations", async ({ page }) => {
await page.setDefaultTimeout(60_000);
await page.setDefaultNavigationTimeout(60_000);
// Create a new note
// await page.locator("button.button-widget.bx-file-blank")
// .click();
const activeNote = page.locator(".component.note-split:visible");
const noteContent = activeNote.locator(".note-detail-editable-text-editor");
await noteContent.press("Ctrl+M");
});
-5
View File
@@ -1,5 +0,0 @@
For bug reports, **PLEASE mention version of Trilium you're using** and also include **log files** from following location:
* `/home/[user]/.local/share/trilium-data/log` for Linux
* `C:\Users\[user]\AppData\Roaming\trilium-data\log` for Windows Vista and up
* `/Users/[user]/Library/Application Support/trilium-data/log` for Mac OS
File diff suppressed because one or more lines are too long
-13
View File
@@ -1,13 +0,0 @@
{
"name": "mermaid-elk",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mermaid-elk",
"version": "1.0.0",
"license": "ISC"
}
}
}
-13
View File
@@ -1,13 +0,0 @@
{
"name": "mermaid-elk",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "cross-env node --import ../../loader-register.js ../../node_modules/webpack/bin/webpack.js -c webpack.config.cjs"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {}
}
-19
View File
@@ -1,19 +0,0 @@
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "production",
entry: "../../node_modules/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs",
output: {
library: "MERMAID_ELK",
filename: "elk.min.js",
path: path.resolve(__dirname),
libraryTarget: "umd",
libraryExport: "default"
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
})
]
}
-10
View File
@@ -1,10 +0,0 @@
// Used to register the loader with Node.js
// This is used to avoid the warning message when using the loader
// Can be removed if this PR is merged:
// https://github.com/TypeStrong/ts-node/pull/2073
// Then probably can change webpack comand to
// "webpack": "cross-env NODE_OPTIONS=--import=ts-node/esm webpack -c webpack.config.ts",
import { register } from "node:module";
import { pathToFileURL } from "node:url";
register("ts-node/esm", pathToFileURL("./"));
+1846 -1487
View File
File diff suppressed because it is too large Load Diff
+30 -27
View File
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "TriliumNext Notes",
"description": "Build your personal knowledge base with TriliumNext Notes",
"version": "0.91.3-beta",
"version": "0.91.4-beta",
"license": "AGPL-3.0-only",
"main": "./dist/electron-main.js",
"author": {
@@ -28,18 +28,17 @@
"start-electron": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./dist/electron-main.js --inspect=5858 .",
"start-electron-nix": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
"start-electron-no-dir": "npm run prepare-dist && cross-env TRILIUM_ENV=dev electron --inspect=5858 .",
"start-electron-no-dir-nix": "npm run prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
"start-electron-no-dir-nix": "electron-rebuild --version 33.3.1 && npm run prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
"qstart-electron": "npm run switch-electron && npm run start-electron",
"switch-server": "rimraf ./node_modules/better-sqlite3 && npm install",
"switch-electron": "npx electron-rebuild",
"switch-electron": "electron-rebuild",
"build-backend-docs": "rimraf ./docs/backend_api && typedoc ./docs/backend_api src/becca/entities/*.ts src/services/backend_script_api.ts src/services/sql.ts",
"build-frontend-docs": "rimraf ./docs/frontend_api && jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/basic_widget.js src/public/app/widgets/note_context_aware_widget.js src/public/app/widgets/right_panel_widget.js",
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
"webpack": "cross-env node --import ./loader-register.js node_modules/webpack/bin/webpack.js -c webpack.config.ts",
"webpack": "tsx node_modules/webpack/bin/webpack.js -c webpack.config.ts",
"test-playwright": "playwright test",
"test-jasmine": "cross-env TRILIUM_DATA_DIR=./data-test tsx ./node_modules/jasmine/bin/jasmine.js",
"test-es6": "tsx -r esm spec-es6/attribute_parser.spec.ts",
"test": "npm run test-jasmine && npm run test-es6",
"test": "cross-env TRILIUM_DATA_DIR=./data-test vitest",
"test-coverage": "cross-env TRILIUM_DATA_DIR=./data-test vitest --coverage",
"start-electron-forge": "npm run prepare-dist && electron-forge start",
"make-electron": "npm run webpack && npm run prepare-dist && electron-forge make",
"package-electron": "electron-forge package",
@@ -49,7 +48,7 @@
"integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"integration-mem-db": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"generate-document": "cross-env nodemon src/tools/generate_document.ts 1000",
"generate-document": "cross-env nodemon ./bin/generate_document.ts 1000",
"ci-update-nightly-version": "tsx ./bin/update-nightly-version.ts",
"prettier-check": "prettier . --check",
"prettier-fix": "prettier . --write"
@@ -60,13 +59,15 @@
"@excalidraw/excalidraw": "0.17.6",
"@highlightjs/cdn-assets": "11.11.1",
"@mermaid-js/layout-elk": "0.1.7",
"@mind-elixir/node-menu": "1.0.3",
"@mind-elixir/node-menu": "1.0.4",
"@triliumnext/express-partial-content": "1.0.1",
"@types/leaflet": "1.9.16",
"@types/react-dom": "18.3.5",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"autocomplete.js": "0.38.1",
"axios": "1.7.9",
"better-sqlite3": "11.7.2",
"better-sqlite3": "11.8.1",
"bootstrap": "5.3.3",
"boxicons": "2.1.4",
"cheerio": "1.0.0",
@@ -89,16 +90,16 @@
"express": "4.21.2",
"express-rate-limit": "7.5.0",
"express-session": "1.18.1",
"force-graph": "1.47.2",
"fs-extra": "11.2.0",
"force-graph": "1.49.0",
"fs-extra": "11.3.0",
"helmet": "8.0.0",
"html": "1.0.0",
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "24.2.1",
"i18next": "24.2.2",
"i18next-fs-backend": "2.6.0",
"i18next-http-backend": "3.0.1",
"i18next-http-backend": "3.0.2",
"image-type": "5.2.0",
"ini": "5.0.0",
"is-animated": "2.0.2",
@@ -110,8 +111,9 @@
"jquery.fancytree": "2.38.4",
"jsdom": "26.0.0",
"jsplumb": "2.15.6",
"katex": "0.16.19",
"katex": "0.16.21",
"knockout": "3.5.1",
"leaflet": "1.9.4",
"mark.js": "8.11.1",
"marked": "15.0.6",
"mermaid": "11.4.1",
@@ -146,14 +148,14 @@
"yauzl": "3.2.0"
},
"devDependencies": {
"@electron-forge/cli": "7.6.0",
"@electron-forge/maker-deb": "7.6.0",
"@electron-forge/maker-dmg": "7.6.0",
"@electron-forge/maker-squirrel": "7.6.0",
"@electron-forge/maker-zip": "7.6.0",
"@electron-forge/plugin-auto-unpack-natives": "7.6.0",
"@electron-forge/cli": "7.6.1",
"@electron-forge/maker-deb": "7.6.1",
"@electron-forge/maker-dmg": "7.6.1",
"@electron-forge/maker-squirrel": "7.6.1",
"@electron-forge/maker-zip": "7.6.1",
"@electron-forge/plugin-auto-unpack-natives": "7.6.1",
"@electron/rebuild": "3.7.1",
"@playwright/test": "1.49.1",
"@playwright/test": "1.50.0",
"@types/archiver": "6.0.3",
"@types/better-sqlite3": "7.6.12",
"@types/bootstrap": "5.2.10",
@@ -175,7 +177,8 @@
"@types/jsdom": "21.1.7",
"@types/mime-types": "2.1.4",
"@types/multer": "1.4.12",
"@types/node": "22.10.6",
"@types/node": "22.10.10",
"@types/react": "18.3.18",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.13.0",
"@types/sax": "1.2.7",
@@ -186,13 +189,13 @@
"@types/stream-throttle": "0.1.4",
"@types/tmp": "0.2.6",
"@types/turndown": "5.0.5",
"@types/ws": "8.5.13",
"@types/ws": "8.5.14",
"@types/xml2js": "0.4.14",
"@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.0.4",
"cross-env": "7.0.3",
"electron": "33.3.1",
"electron": "34.0.1",
"esm": "3.2.25",
"iconsur": "1.7.0",
"jasmine": "5.5.0",
"jsdoc": "4.0.4",
"lorem-ipsum": "2.0.8",
@@ -200,11 +203,11 @@
"prettier": "3.4.2",
"rcedit": "4.0.1",
"rimraf": "6.0.1",
"ts-node": "10.9.2",
"tslib": "2.8.1",
"tsx": "4.19.2",
"typedoc": "0.27.6",
"typescript": "5.7.3",
"vitest": "3.0.4",
"webpack": "5.97.1",
"webpack-cli": "6.0.1",
"webpack-dev-middleware": "7.4.2"
+4 -4
View File
@@ -41,10 +41,10 @@ export default defineConfig({
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
-138
View File
@@ -1,138 +0,0 @@
import { describe, it, execute, expect } from "./mini_test.ts";
import { getPlatformAppDataDir, getDataDirs} from "../src/services/data_dir.ts"
describe("data_dir.ts unit tests", () => {
describe("#getPlatformAppDataDir()", () => {
type TestCaseGetPlatformAppDataDir = [
description: string,
fnValue: Parameters<typeof getPlatformAppDataDir>,
expectedValueFn: (val: ReturnType<typeof getPlatformAppDataDir>) => boolean
]
const testCases: TestCaseGetPlatformAppDataDir[] = [
[
"w/ unsupported OS it should return 'null'",
["aix", undefined],
(val) => val === null
],
[
"w/ win32 and no APPDATA set it should return 'null'",
["win32", undefined],
(val) => val === null
],
[
"w/ win32 and set APPDATA it should return set 'APPDATA'",
["win32", "AppData"],
(val) => val === "AppData"
],
[
"w/ linux it should return '/.local/share'",
["linux", undefined],
(val) => val !== null && val.endsWith("/.local/share")
],
[
"w/ linux and wrongly set APPDATA it should ignore APPDATA and return /.local/share",
["linux", "FakeAppData"],
(val) => val !== null && val.endsWith("/.local/share")
],
[
"w/ darwin it should return /Library/Application Support",
["darwin", undefined],
(val) => val !== null && val.endsWith("/Library/Application Support")
],
];
testCases.forEach(testCase => {
const [testDescription, value, isExpected] = testCase;
return it(testDescription, () => {
const actual = getPlatformAppDataDir(...value);
const result = isExpected(actual);
expect(result).toBeTruthy()
})
})
})
describe("#getTriliumDataDir", () => {
// TODO
})
describe("#getDataDirs()", () => {
const envKeys: Omit<keyof ReturnType<typeof getDataDirs>, "TRILIUM_DATA_DIR">[] = [
"DOCUMENT_PATH",
"BACKUP_DIR",
"LOG_DIR",
"ANONYMIZED_DB_DIR",
"CONFIG_INI_PATH",
];
const setMockedEnv = (prefix: string | null) => {
envKeys.forEach(key => {
if (prefix) {
process.env[`TRILIUM_${key}`] = `${prefix}_${key}`
} else {
delete process.env[`TRILIUM_${key}`]
}
})
};
it("w/ process.env values present, it should return an object using values from process.env", () => {
// set mocked values
const mockValuePrefix = "MOCK";
setMockedEnv(mockValuePrefix);
// get result
const result = getDataDirs(`${mockValuePrefix}_TRILIUM_DATA_DIR`);
for (const key in result) {
expect(result[key]).toEqual(`${mockValuePrefix}_${key}`)
}
})
it("w/ NO process.env values present, it should return an object using supplied TRILIUM_DATA_DIR as base", () => {
// make sure values are undefined
setMockedEnv(null);
const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR"
const result = getDataDirs(mockDataDir);
for (const key in result) {
expect(result[key].startsWith(mockDataDir)).toBeTruthy()
}
})
it("should ignore attempts to change a property on the returned object", () => {
// make sure values are undefined
setMockedEnv(null);
const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR"
const result = getDataDirs(mockDataDir);
//@ts-expect-error - attempt to change value of readonly property
result.BACKUP_DIR = "attempt to change";
for (const key in result) {
expect(result[key].startsWith(mockDataDir)).toBeTruthy()
}
})
})
});
execute()
-79
View File
@@ -1,79 +0,0 @@
export function describe(name: string, cb: () => any) {
console.log(`Running ${name}`);
cb();
}
export async function it(name: string, cb: () => any) {
console.log(` Running ${name}`);
cb();
}
let errorCount = 0;
export function expect(val: any) {
return {
toEqual: (comparedVal: any) => {
const jsonVal = JSON.stringify(val);
const comparedJsonVal = JSON.stringify(comparedVal);
if (jsonVal !== comparedJsonVal) {
console.trace("toEqual check failed.");
console.error(`expected: ${comparedJsonVal}`);
console.error(`got: ${jsonVal}`);
errorCount++;
}
},
toBeTruthy: () => {
if (!val) {
console.trace("toBeTruthy failed.");
console.error(`expected: truthy value`);
console.error(`got: ${val}`);
errorCount++;
}
},
toBeFalsy: () => {
if (!!val) {
console.trace("toBeFalsy failed.");
console.error(`expected: null, false, undefined, 0 or empty string`);
console.error(`got: ${val}`);
errorCount++;
}
},
toThrow: (errorMessage: any) => {
try {
val();
} catch (e: any) {
if (e.message !== errorMessage) {
console.trace("toThrow caught exception, but messages differ");
console.error(`expected: ${errorMessage}`);
console.error(`got: ${e.message}`);
console.error(`${e.stack}`);
errorCount++;
}
return;
}
console.trace("toThrow did not catch any exception.");
console.error(`expected: ${errorMessage}`);
console.error(`got: [none]`);
errorCount++;
}
};
}
export function execute() {
console.log("");
if (errorCount) {
console.log(`!!!${errorCount} tests failed!!!`);
} else {
console.log("All tests passed!");
}
}
-3
View File
@@ -1,3 +0,0 @@
describe("Notes", () => {
it("zzz", () => {});
});
-10
View File
@@ -1,10 +0,0 @@
{
"spec_dir": "",
"spec_files": [
"spec/**/*.spec.ts",
"src/**/*.spec.ts"
],
"helpers": ["helpers/**/*.js"],
"stopSpecOnExpectationFailure": false,
"random": true
}
+1
View File
@@ -1,3 +1,4 @@
import { describe, it, expect } from "vitest";
import { trimIndentation } from "./utils.js";
describe("Utils", () => {
+3 -1
View File
@@ -1528,7 +1528,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
}
isLaunchBarConfig() {
return this.type === "launcher" || ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(this.noteId);
return this.type === "launcher"
|| ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(this.noteId)
|| ["_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(this.noteId);
}
isOptions() {
+2 -1
View File
@@ -116,7 +116,8 @@ export const ALLOWED_NOTE_TYPES = [
"book",
"webView",
"code",
"mindMap"
"mindMap",
"geoMap"
] as const;
export type NoteType = (typeof ALLOWED_NOTE_TYPES)[number];
+8 -1
View File
@@ -36,6 +36,12 @@ interface DateLimits {
maxDate: string;
}
interface SimilarNote {
score: number;
notePath: string[];
noteId: string;
}
function filterUrlValue(value: string) {
return value
.replace(/https?:\/\//gi, "")
@@ -247,7 +253,7 @@ function hasConnectingRelation(sourceNote: BNote, targetNote: BNote) {
return sourceNote.getAttributes().find((attr) => attr.type === "relation" && ["includenotelink", "imagelink"].includes(attr.name) && attr.value === targetNote.noteId);
}
async function findSimilarNotes(noteId: string) {
async function findSimilarNotes(noteId: string): Promise<SimilarNote[] | undefined> {
const results = [];
let i = 0;
@@ -417,6 +423,7 @@ async function findSimilarNotes(noteId: string) {
// this takes care of note hoisting
if (!notePath) {
// TODO: This return is suspicious, it should probably be continue
return;
}
+31 -3
View File
@@ -23,6 +23,7 @@ import type LoadResults from "../services/load_results.js";
import type { Attribute } from "../services/attribute_parser.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
import type { ContextMenuEvent } from "../menus/context_menu.js";
interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget;
@@ -69,7 +70,8 @@ export interface ExecuteCommandData extends CommandData {
*/
export type CommandMappings = {
"api-log-messages": CommandData;
focusOnDetail: Required<CommandData>;
focusTree: CommandData,
focusOnDetail: CommandData;
focusOnSearchDefinition: Required<CommandData>;
searchNotes: CommandData & {
searchString?: string;
@@ -102,12 +104,15 @@ export type CommandMappings = {
openNoteInNewTab: CommandData;
openNoteInNewSplit: CommandData;
openNoteInNewWindow: CommandData;
hideLeftPane: CommandData;
showLeftPane: CommandData;
openInTab: ContextMenuCommandData;
openNoteInSplit: ContextMenuCommandData;
toggleNoteHoisting: ContextMenuCommandData;
insertNoteAfter: ContextMenuCommandData;
insertChildNote: ContextMenuCommandData;
delete: ContextMenuCommandData;
protectSubtree: ContextMenuCommandData;
unprotectSubtree: ContextMenuCommandData;
openBulkActionsDialog: ContextMenuCommandData;
@@ -192,6 +197,10 @@ export type CommandMappings = {
setZoomFactorAndSave: {
zoomFactor: string;
}
// Geomap
deleteFromMap: { noteId: string },
openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent }
};
type EventMappings = {
@@ -226,9 +235,15 @@ type EventMappings = {
activeContextChanged: {
noteContext: NoteContext;
};
beforeNoteSwitch: {
noteContext: NoteContext;
};
beforeNoteContextRemove: {
ntxIds: string[];
};
noteSwitched: {
noteContext: NoteContext;
notePath: string;
notePath: string | null;
};
noteSwitchedAndActivatedEvent: {
noteContext: NoteContext;
@@ -247,12 +262,16 @@ type EventMappings = {
noteId: string;
};
hoistedNoteChanged: {
ntxId: string;
noteId: string;
ntxId: string | null;
};
contextsReopenedEvent: {
mainNtxId: string;
tabPosition: number;
};
noteDetailRefreshed: {
ntxId?: string | null;
};
noteContextReorderEvent: {
oldMainNtxId: string;
newMainNtxId: string;
@@ -265,6 +284,15 @@ type EventMappings = {
};
exportSvg: {
ntxId: string;
};
geoMapCreateChildNote: {
ntxId: string | null | undefined; // TODO: deduplicate ntxId
};
tabReorder: {
ntxIdsInOrder: string[]
};
refreshNoteList: {
noteId: string;
}
};
+2 -2
View File
@@ -46,7 +46,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
return this;
}
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined {
try {
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
@@ -61,7 +61,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
}
}
triggerEvent(name: string, data = {}): Promise<unknown> | undefined | null {
triggerEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown> | undefined | null {
return this.parent?.triggerEvent(name, data);
}
+1 -1
View File
@@ -24,7 +24,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
notePath?: string | null;
noteId?: string | null;
private parentNoteId?: string | null;
parentNoteId?: string | null;
viewScope?: ViewScope;
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
@@ -9,6 +9,8 @@ import electronContextMenu from "./menus/electron_context_menu.js";
import glob from "./services/glob.js";
import { t } from "./services/i18n.js";
import options from "./services/options.js";
import type ElectronRemote from "@electron/remote";
import type Electron from "electron";
await appContext.earlyInit();
@@ -44,10 +46,9 @@ if (utils.isElectron()) {
}
function initOnElectron() {
const electron = utils.dynamicRequire("electron");
const electron: typeof Electron = utils.dynamicRequire("electron");
electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
const electronRemote = utils.dynamicRequire("@electron/remote");
const electronRemote: typeof ElectronRemote = utils.dynamicRequire("@electron/remote");
const currentWindow = electronRemote.getCurrentWindow();
const style = window.getComputedStyle(document.body);
@@ -58,7 +59,7 @@ function initOnElectron() {
}
}
function initTitleBarButtons(style, currentWindow) {
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
if (window.glob.platform === "win32") {
const applyWindowsOverlay = () => {
const color = style.getPropertyValue("--native-titlebar-background");
@@ -81,9 +82,14 @@ function initTitleBarButtons(style, currentWindow) {
}
}
function initTransparencyEffects(style, currentWindow) {
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
if (window.glob.platform === "win32") {
const material = style.getPropertyValue("--background-material");
currentWindow.setBackgroundMaterial(material);
// TriliumNextTODO: find a nicer way to make TypeScript happy unfortunately TS did not like Array.includes here
const bgMaterialOptions = ["auto", "none", "mica", "acrylic", "tabbed"] as const;
const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption);
if (foundBgMaterialOption) {
currentWindow.setBackgroundMaterial(foundBgMaterialOption);
}
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export default class FBlob {
/**
* @throws Error in case of invalid JSON
*/
getJsonContent(): unknown {
getJsonContent<T>(): T | null {
if (!this.content || !this.content.trim()) {
return null;
}
+10 -9
View File
@@ -27,7 +27,8 @@ const NOTE_TYPE_ICONS = {
launcher: "bx bx-link",
doc: "bx bxs-file-doc",
contentWidget: "bx bxs-widget",
mindMap: "bx bx-sitemap"
mindMap: "bx bx-sitemap",
geoMap: "bx bx-map-alt"
};
/**
@@ -35,12 +36,12 @@ const NOTE_TYPE_ICONS = {
* end user. Those types should be used only for checking against, they are
* not for direct use.
*/
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" | "geoMap";
interface NotePathRecord {
export interface NotePathRecord {
isArchived: boolean;
isInHoistedSubTree: boolean;
isSearch: boolean;
isSearch?: boolean;
notePath: string[];
isHidden: boolean;
}
@@ -401,14 +402,14 @@ class FNote {
return notePaths;
}
getSortedNotePathRecords(hoistedNoteId = "root") {
getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
const isHoistedRoot = hoistedNoteId === "root";
const notePaths = this.getAllNotePaths().map((path) => ({
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
notePath: path,
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
isArchived: path.some((noteId) => froca.notes[noteId].isArchived),
isSearch: path.find((noteId) => froca.notes[noteId].type === "search"),
isSearch: path.some((noteId) => froca.notes[noteId].type === "search"),
isHidden: path.includes("_hidden")
}));
@@ -515,10 +516,10 @@ class FNote {
}
/**
* @param [name] - label name to filter
* @param name - label name to filter
* @returns all note's labels (attributes with type label), including inherited ones
*/
getOwnedLabels(name: string) {
getOwnedLabels(name?: string) {
return this.getOwnedAttributes(LABEL, name);
}
+2
View File
@@ -85,6 +85,7 @@ import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
export default class DesktopLayout {
constructor(customWidgets) {
@@ -200,6 +201,7 @@ export default class DesktopLayout {
.child(new ShowHighlightsListWidgetButton())
.child(new CodeButtonsWidget())
.child(new RelationMapButtons())
.child(new GeoMapButtons())
.child(new CopyImageReferenceButton())
.child(new SvgExportButton())
.child(new BacklinksWidget())
+7
View File
@@ -1,5 +1,6 @@
import type { CommandNames } from "../components/app_context.js";
import keyboardActionService from "../services/keyboard_actions.js";
import note_tooltip from "../services/note_tooltip.js";
import utils from "../services/utils.js";
interface ContextMenuOptions<T extends CommandNames> {
@@ -8,6 +9,8 @@ interface ContextMenuOptions<T extends CommandNames> {
orientation?: "left";
selectMenuItemHandler: MenuHandler<T>;
items: MenuItem<T>[];
/** On mobile, if set to `true` then the context menu is shown near the element. If `false` (default), then the context menu is shown at the bottom of the screen. */
forcePositionOnMobile?: boolean;
}
interface MenuSeparatorItem {
@@ -29,6 +32,7 @@ export interface MenuCommandItem<T extends CommandNames> {
export type MenuItem<T extends CommandNames> = MenuCommandItem<T> | MenuSeparatorItem;
export type MenuHandler<T extends CommandNames> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
class ContextMenu {
private $widget: JQuery<HTMLElement>;
@@ -54,12 +58,15 @@ class ContextMenu {
async show<T extends CommandNames>(options: ContextMenuOptions<T>) {
this.options = options;
note_tooltip.dismissAllTooltips();
if (this.$widget.hasClass("show")) {
// The menu is already visible. Hide the menu then open it again
// at the new location to re-trigger the opening animation.
await this.hide();
}
this.$widget.toggleClass("mobile-bottom-menu", !this.options.forcePositionOnMobile);
this.$cover.addClass("show");
$("body").addClass("context-menu-shown");
@@ -34,8 +34,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers";
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers";
const isVisibleItem = (parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers");
const isAvailableItem = (parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers");
const isItem = isVisibleItem || isAvailableItem;
const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted
const canBeReset = !canBeDeleted && note?.isLaunchBarConfig();
+32 -24
View File
@@ -1,36 +1,44 @@
import { t } from "../services/i18n.js";
import contextMenu from "./context_menu.js";
import appContext from "../components/app_context.js";
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
import appContext, { type CommandNames } from "../components/app_context.js";
import type { ViewScope } from "../services/link.js";
function openContextMenu(notePath: string, e: PointerEvent | JQuery.ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
contextMenu.show({
x: e.pageX,
y: e.pageY,
items: [
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" }
],
selectMenuItemHandler: ({ command }) => {
if (!hoistedNoteId) {
hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId;
}
if (command === "openNoteInNewTab") {
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
} else if (command === "openNoteInNewSplit") {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
} else if (command === "openNoteInNewWindow") {
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
}
}
items: getItems(),
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, notePath, viewScope, hoistedNoteId)
});
}
function getItems(): MenuItem<CommandNames>[] {
return [
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" }
];
}
function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
if (!hoistedNoteId) {
hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId;
}
if (command === "openNoteInNewTab") {
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
} else if (command === "openNoteInNewSplit") {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
} else if (command === "openNoteInNewWindow") {
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
}
}
export default {
getItems,
handleLinkContextMenuItem,
openContextMenu
};
+5 -1
View File
@@ -1,12 +1,16 @@
import type { EntityRowNames } from "./services/load_results.js";
interface Entity {
isDeleted?: boolean;
}
// TODO: Deduplicate with src/services/entity_changes_interface.ts
export interface EntityChange {
id?: number | null;
noteId?: string;
entityName: EntityRowNames;
entityId: string;
entity?: any;
entity?: Entity;
positions?: Record<string, number>;
hash: string;
utcDateChanged?: string;
@@ -1,6 +1,6 @@
import * as attributeParser from "../src/public/app/services/attribute_parser.js";
import { describe, it, expect } from "vitest";
import attributeParser from "./attribute_parser.js";
import { describe, it, expect, execute } from "./mini_test.js";
describe("Lexing", () => {
it("simple label", () => {
@@ -40,8 +40,10 @@ describe("Lexing", () => {
});
});
describe("Parser", () => {
describe.todo("Parser", () => {
/* #TODO
it("simple label", () => {
const attrs = attributeParser.parse(["#token"].map((t: any) => ({ text: t })));
expect(attrs.length).toEqual(1);
@@ -85,6 +87,7 @@ describe("Parser", () => {
expect(attrs[0].name).toEqual("token");
expect(attrs[0].value).toEqual("NFi2gL4xtPxM");
});
*/
});
describe("error cases", () => {
@@ -96,5 +99,3 @@ describe("error cases", () => {
expect(() => attributeParser.lexAndParse("#")).toThrow(`Attribute name is empty, please fill the name.`);
});
});
execute();
+9 -7
View File
@@ -290,14 +290,16 @@ function processAttachment(loadResults: LoadResults, ec: EntityChange) {
return;
}
if (attachment) {
attachment.update(ec.entity as FAttachmentRow);
} else {
const attachmentRow = ec.entity as FAttachmentRow;
const note = froca.notes[attachmentRow.ownerId];
if (ec.entity) {
if (attachment) {
attachment.update(ec.entity as FAttachmentRow);
} else {
const attachmentRow = ec.entity as FAttachmentRow;
const note = froca.notes[attachmentRow.ownerId];
if (note?.attachments) {
note.attachments.push(new FAttachment(froca, attachmentRow));
if (note?.attachments) {
note.attachments.push(new FAttachment(froca, attachmentRow));
}
}
}
+6 -20
View File
@@ -68,26 +68,10 @@ const WHEEL_ZOOM: Library = {
js: ["node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"]
};
const FORCE_GRAPH: Library = {
js: ["node_modules/force-graph/dist/force-graph.min.js"]
};
const MERMAID: Library = {
js: ["node_modules/mermaid/dist/mermaid.min.js"]
};
/**
* The ELK extension of Mermaid.js, which supports more advanced layouts.
* See https://www.npmjs.com/package/@mermaid-js/layout-elk for more information.
*/
const MERMAID_ELK: Library = {
js: ["libraries/mermaid-elk/elk.min.js"]
};
const EXCALIDRAW: Library = {
js: ["node_modules/react/umd/react.production.min.js", "node_modules/react-dom/umd/react-dom.production.min.js", "node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"]
};
const MARKJS: Library = {
js: ["node_modules/mark.js/dist/jquery.mark.es6.min.js"]
};
@@ -122,6 +106,10 @@ const HIGHLIGHT_JS: Library = {
}
};
const LEAFLET: Library = {
css: [ "node_modules/leaflet/dist/leaflet.css" ],
}
async function requireLibrary(library: Library) {
if (library.css) {
library.css.map((cssUrl) => requireCss(cssUrl));
@@ -209,11 +197,9 @@ export default {
CALENDAR_WIDGET,
KATEX,
WHEEL_ZOOM,
FORCE_GRAPH,
MERMAID,
MERMAID_ELK,
EXCALIDRAW,
MARKJS,
I18NEXT,
HIGHLIGHT_JS
HIGHLIGHT_JS,
LEAFLET
};
+5 -8
View File
@@ -234,7 +234,7 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent) {
return goToLinkExt(evt, hrefLink, $link);
}
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | undefined, $link: JQuery<HTMLElement>) {
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement>, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
if (hrefLink?.startsWith("data:")) {
return true;
}
@@ -242,20 +242,17 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
evt.preventDefault();
evt.stopPropagation();
if (hrefLink?.startsWith("#fn")) {
if (hrefLink?.startsWith("#fn") && $link) {
return handleFootnote(hrefLink, $link);
}
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
const ctrlKey = utils.isCtrlKey(evt);
const isLeftClick = evt.which === 1;
const isMiddleClick = evt.which === 2;
const isLeftClick = ("which" in evt && evt.which === 1);
const isMiddleClick = ("which" in evt && evt.which === 2);
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick;
const leftClick = evt.which === 1;
const middleClick = evt.which === 2;
if (notePath) {
if (openInNewTab) {
appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope });
@@ -276,7 +273,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
if (openInNewTab || (withinEditLink && (leftClick || middleClick)) || (outsideOfCKEditor && (leftClick || middleClick))) {
if (openInNewTab || (withinEditLink && (isLeftClick || isMiddleClick)) || (outsideOfCKEditor && (isLeftClick || isMiddleClick))) {
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
window.open(hrefLink, "_blank");
} else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) {
+2 -1
View File
@@ -8,6 +8,7 @@ interface NoteRow {
}
interface BranchRow {
noteId?: string;
branchId: string;
componentId: string;
parentNoteId?: string;
@@ -157,7 +158,7 @@ export default class LoadResults {
return Object.keys(this.noteIdToComponentId);
}
isNoteReloaded(noteId: string, componentId = null) {
isNoteReloaded(noteId: string | undefined, componentId: string | null = null) {
if (!noteId) {
return false;
}
+1 -4
View File
@@ -1,5 +1,3 @@
import library_loader from "./library_loader.js";
let elkLoaded = false;
/**
@@ -22,7 +20,6 @@ export async function loadElkIfNeeded(mermaidContent: string) {
});
if (parsedContent?.config?.layout === "elk") {
elkLoaded = true;
await library_loader.requireLibrary(library_loader.MERMAID_ELK);
mermaid.registerLayoutLoaders(MERMAID_ELK);
mermaid.registerLayoutLoaders((await import("@mermaid-js/layout-elk")).default);
}
}
@@ -1,3 +1,5 @@
// TODO: deduplicate with /src/services/import/mime_type_definitions.ts
/**
* A pseudo-MIME type which is used in the editor to automatically determine the language used in code blocks via heuristics.
*/
+7 -6
View File
@@ -18,11 +18,11 @@ function setupGlobalTooltip() {
return;
}
cleanUpTooltips();
dismissAllTooltips();
});
}
function cleanUpTooltips() {
function dismissAllTooltips() {
$(".note-tooltip").remove();
}
@@ -102,12 +102,12 @@ async function mouseEnterHandler(this: HTMLElement) {
customClass: linkId
});
cleanUpTooltips();
dismissAllTooltips();
$(this).tooltip("show");
// Dismiss the tooltip immediately if a link was clicked inside the tooltip.
$(`.${tooltipClass} a`).on("click", (e) => {
cleanUpTooltips();
dismissAllTooltips();
});
// the purpose of the code below is to:
@@ -117,7 +117,7 @@ async function mouseEnterHandler(this: HTMLElement) {
const checkTooltip = () => {
if (!$(this).filter(":hover").length && !$(`.${linkId}:hover`).length) {
// cursor is neither over the link nor over the tooltip, user likely is not interested
cleanUpTooltips();
dismissAllTooltips();
} else {
setTimeout(checkTooltip, 1000);
}
@@ -172,5 +172,6 @@ function renderFootnote($link: JQuery<HTMLElement>, url: string) {
export default {
setupGlobalTooltip,
setupElementTooltip
setupElementTooltip,
dismissAllTooltips
};
+2 -1
View File
@@ -18,7 +18,8 @@ async function getNoteTypeItems(command?: NoteTypeCommandNames) {
{ title: t("note_types.mermaid-diagram"), command, type: "mermaid", uiIcon: "bx bx-selection" },
{ title: t("note_types.canvas"), command, type: "canvas", uiIcon: "bx bx-pen" },
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" }
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
{ title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" },
];
const templateNoteIds = await server.get<string[]>("search-templates");
+7 -3
View File
@@ -36,10 +36,14 @@ class Options {
getInt(key: string) {
const value = this.arr?.[key];
if (typeof value !== "string") {
return null;
if (typeof value === "number") {
return value;
}
return parseInt(value);
if (typeof value == "string") {
return parseInt(value);
}
console.warn("Attempting to read int for unsupported value: ", value);
return null;
}
getFloat(key: string) {
+8
View File
@@ -43,6 +43,14 @@ export default class SpacedUpdate {
return allSaved;
}
/**
* Normally {@link scheduleUpdate()} would actually trigger the update only once per {@link updateInterval}. If the method is called 200 times within 20s, it will execute only 20 times.
* Sometimes, if the updates are continuous this would cause a performance impact. Resetting the time ensures that the calls to {@link triggerUpdate} have stopped before actually triggering an update.
*/
resetUpdateTimer() {
this.lastUpdated = Date.now();
}
triggerUpdate() {
if (!this.changed) {
return;
+4 -2
View File
@@ -97,7 +97,7 @@ function isMac() {
return navigator.platform.indexOf("Mac") > -1;
}
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent) {
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement>) {
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
}
@@ -308,7 +308,9 @@ function dynamicRequire(moduleName: string) {
if (typeof __non_webpack_require__ !== "undefined") {
return __non_webpack_require__(moduleName);
} else {
return require(moduleName);
// explicitly pass as string and not as expression to suppress webpack warning
// 'Critical dependency: the request of a dependency is an expression'
return require(`${moduleName}`);
}
}
@@ -3,7 +3,7 @@
*
* @param noteId of the given note to be fetched. If false, fetches current note.
*/
async function fetchNote(noteId = null) {
async function fetchNote(noteId: string | null = null) {
if (!noteId) {
noteId = document.body.getAttribute("data-note-id");
}
@@ -25,3 +25,9 @@ document.addEventListener(
},
false
);
// workaround to prevent webpack from removing "fetchNote" as dead code:
// add fetchNote as property to the window object
Object.defineProperty(window, "fetchNote", {
value: fetchNote
});
+3 -3
View File
@@ -43,6 +43,7 @@ interface CustomGlobals {
appCssNoteIds: string[];
triliumVersion: string;
TRILIUM_SAFE_MODE: boolean;
platform?: typeof process.platform;
}
type RequireMethod = (moduleName: string) => any;
@@ -155,13 +156,12 @@ declare global {
registerLayoutLoaders(loader: MermaidLoader);
parse(content: string, opts: {
suppressErrors: true
}): {
}): Promise<{
config: {
layout: string;
}
}
}>
};
var MERMAID_ELK: MermaidLoader;
var CKEditor: {
BalloonEditor: {
+2 -2
View File
@@ -193,7 +193,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
* Indicates if the widget is enabled. Widgets are enabled by default. Generally setting this to `false` will cause the widget not to be displayed, however it will still be available on the DOM but hidden.
* @returns whether the widget is enabled.
*/
isEnabled() {
isEnabled(): boolean | null | undefined {
return true;
}
@@ -205,7 +205,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
*/
doRender() {}
toggleInt(show: boolean) {
toggleInt(show: boolean | null | undefined) {
this.$widget.toggleClass("hidden-int", !show);
}
@@ -86,7 +86,7 @@ const TPL = `
position: relative;
top: 3px;
font-size: 120%;
margin-right: 5px;
margin-right: 6px;
}
body.mobile .global-menu .dropdown-submenu .dropdown-menu {
@@ -162,8 +162,7 @@ const TPL = `
<li class="dropdown-item dropdown-submenu">
<span class="dropdown-toggle">
<span class="bx bx-chip"></span>
${t("global_menu.advanced")}
<span class="bx bx-chip"></span>${t("global_menu.advanced")}
</span>
<ul class="dropdown-menu">
@@ -2,9 +2,11 @@ import options from "../../services/options.js";
import splitService from "../../services/resizer.js";
import CommandButtonWidget from "./command_button.js";
import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js";
export default class LeftPaneToggleWidget extends CommandButtonWidget {
constructor(isHorizontalLayout) {
constructor(isHorizontalLayout: boolean) {
super();
this.class(isHorizontalLayout ? "toggle-button" : "launcher-button");
@@ -32,7 +34,7 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
splitService.setupLeftPaneResizer(options.is("leftPaneVisible"));
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("leftPaneVisible")) {
this.refreshIcon();
}
@@ -42,7 +42,7 @@ const TPL = `
<li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">
<span class="bx bx-paperclip"></span> ${t("note_actions.convert_into_attachment")}
</li>
<li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button">
<span class="bx bx-extension"></span> ${t("note_actions.re_render_note")}<kbd data-command="renderActiveNote"></kbd>
</li>
@@ -54,15 +54,15 @@ const TPL = `
<li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
<span class="bx bx-printer"></span> ${t("note_actions.print_note")}<kbd data-command="printActiveNote"></kbd></li>
<div class="dropdown-divider"></div>
<li class="dropdown-item import-files-button"><span class="bx bx-import"></span> ${t("note_actions.import_files")}</li>
<li class="dropdown-item export-note-button"><span class="bx bx-export"></span> ${t("note_actions.export_note")}</li>
<div class="dropdown-divider"></div>
@@ -79,7 +79,7 @@ const TPL = `
<span class="bx bx-code"></span> ${t("note_actions.note_source")}<kbd data-command="showNoteSource"></kbd>
</li>
<div class="dropdown-divider"></div>
@@ -89,10 +89,10 @@ const TPL = `
<li class="dropdown-item delete-note-button"><span class="bx bx-trash destructive-action-icon"></span> ${t("note_actions.delete_note")}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="showAttachments" class="dropdown-item show-attachments-button">
<span class="bx bx-paperclip"></span> ${t("note_actions.note_attachments")}<kbd data-command="showAttachments"></kbd>
</li>
@@ -154,7 +154,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book"].includes(note.type));
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type));
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type));
this.toggleDisabled(this.$printActiveNoteButton, ["text", "code"].includes(note.type));
@@ -1,8 +1,9 @@
import options from "../../services/options.js";
import FlexContainer from "./flex_container.js";
import appContext from "../../components/app_context.js";
import appContext, { type EventData } from "../../components/app_context.js";
import type Component from "../../components/component.js";
export default class LeftPaneContainer extends FlexContainer {
export default class LeftPaneContainer extends FlexContainer<Component> {
constructor() {
super("column");
@@ -15,13 +16,13 @@ export default class LeftPaneContainer extends FlexContainer {
return super.isEnabled() && options.is("leftPaneVisible");
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("leftPaneVisible")) {
const visible = this.isEnabled();
this.toggleInt(visible);
if (visible) {
this.triggerEvent("focusTree");
this.triggerEvent("focusTree", {});
} else {
const activeNoteContext = appContext.tabManager.getActiveContext();
this.triggerEvent("focusOnDetail", { ntxId: activeNoteContext.ntxId });
@@ -1,6 +1,10 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import keyboardActionsService from "../../services/keyboard_actions.js";
import attributeService from "../../services/attributes.js";
import type CommandButtonWidget from "../buttons/command_button.js";
import type FNote from "../../entities/fnote.js";
import type { NoteType } from "../../entities/fnote.js";
import type { EventData, EventNames } from "../../components/app_context.js";
const TPL = `
<div class="ribbon-container">
@@ -8,11 +12,11 @@ const TPL = `
.ribbon-container {
margin-bottom: 5px;
}
.ribbon-top-row {
display: flex;
}
.ribbon-tab-container {
display: flex;
flex-direction: row;
@@ -21,10 +25,10 @@ const TPL = `
flex-grow: 1;
flex-flow: row wrap;
}
.ribbon-tab-title {
color: var(--muted-text-color);
border-bottom: 1px solid var(--main-border-color);
border-bottom: 1px solid var(--main-border-color);
min-width: 24px;
flex-basis: 24px;
max-width: max-content;
@@ -36,7 +40,7 @@ const TPL = `
position: relative;
top: 3px;
}
.ribbon-tab-title.active {
color: var(--main-text-color);
border-bottom: 3px solid var(--main-text-color);
@@ -44,7 +48,7 @@ const TPL = `
overflow: hidden;
text-overflow: ellipsis;
}
.ribbon-tab-title:hover {
cursor: pointer;
}
@@ -52,11 +56,11 @@ const TPL = `
.ribbon-tab-title:hover {
color: var(--main-text-color);
}
.ribbon-tab-title:first-of-type {
padding-left: 10px;
}
.ribbon-tab-spacer {
flex-basis: 0;
min-width: 0;
@@ -64,41 +68,41 @@ const TPL = `
flex-grow: 1;
border-bottom: 1px solid var(--main-border-color);
}
.ribbon-tab-spacer:last-of-type {
flex-grow: 1;
flex-basis: 0;
min-width: 0;
max-width: 10000px;
}
.ribbon-button-container {
display: flex;
border-bottom: 1px solid var(--main-border-color);
border-bottom: 1px solid var(--main-border-color);
margin-right: 5px;
}
.ribbon-button-container > * {
position: relative;
top: -3px;
margin-left: 10px;
}
.ribbon-body {
display: none;
border-bottom: 1px solid var(--main-border-color);
margin-left: 10px;
margin-right: 5px; /* needs to have this value so that the bottom border is the same width as the top one */
}
.ribbon-body.active {
display: block;
}
.ribbon-tab-title-label {
display: none;
}
.ribbon-tab-title.active .ribbon-tab-title-label {
display: inline;
}
@@ -108,11 +112,21 @@ const TPL = `
<div class="ribbon-tab-container"></div>
<div class="ribbon-button-container"></div>
</div>
<div class="ribbon-body-container"></div>
</div>`;
export default class RibbonContainer extends NoteContextAwareWidget {
private lastActiveComponentId?: string | null;
private lastNoteType?: NoteType;
private ribbonWidgets: NoteContextAwareWidget[];
private buttonWidgets: CommandButtonWidget[];
private $tabContainer!: JQuery<HTMLElement>;
private $buttonContainer!: JQuery<HTMLElement>;
private $bodyContainer!: JQuery<HTMLElement>;
constructor() {
super();
@@ -122,10 +136,10 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
isEnabled() {
return super.isEnabled() && this.noteContext.viewScope.viewMode === "default";
return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default";
}
ribbon(widget) {
ribbon(widget: NoteContextAwareWidget) { // TODO: Base class
super.child(widget);
this.ribbonWidgets.push(widget);
@@ -133,7 +147,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
return this;
}
button(widget) {
button(widget: CommandButtonWidget) {
super.child(widget);
this.buttonWidgets.push(widget);
@@ -163,7 +177,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
});
}
toggleRibbonTab($ribbonTitle, refreshActiveTab = true) {
toggleRibbonTab($ribbonTitle: JQuery<HTMLElement>, refreshActiveTab = true) {
const activate = !$ribbonTitle.hasClass("active");
this.$tabContainer.find(".ribbon-tab-title").removeClass("active");
@@ -181,14 +195,15 @@ export default class RibbonContainer extends NoteContextAwareWidget {
const activeChild = this.getActiveRibbonWidget();
if (activeChild && (refreshActiveTab || !wasAlreadyActive)) {
if (activeChild && (refreshActiveTab || !wasAlreadyActive) && this.noteContext && this.notePath) {
const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath });
if (refreshActiveTab) {
if (handleEventPromise) {
handleEventPromise.then(() => activeChild.focus?.());
handleEventPromise.then(() => (activeChild as any).focus()); // TODO: Base class
} else {
activeChild.focus?.();
// TODO: Base class
(activeChild as any)?.focus();
}
}
}
@@ -203,7 +218,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
await super.noteSwitched();
}
async refreshWithNote(note, noExplicitActivation = false) {
async refreshWithNote(note: FNote, noExplicitActivation = false) {
this.lastNoteType = note.type;
let $ribbonTabToActivate, $lastActiveRibbon;
@@ -211,7 +226,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
this.$tabContainer.empty();
for (const ribbonWidget of this.ribbonWidgets) {
const ret = await ribbonWidget.getTitle(note);
// TODO: Base class for ribbon widget
const ret = await (ribbonWidget as any).getTitle(note);
if (!ret.show) {
continue;
@@ -219,8 +235,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
const $ribbonTitle = $('<div class="ribbon-tab-title">')
.attr("data-ribbon-component-id", ribbonWidget.componentId)
.attr("data-ribbon-component-name", ribbonWidget.name)
.append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", ribbonWidget.toggleCommand))
.attr("data-ribbon-component-name", (ribbonWidget as any).name as string) // TODO: base class for ribbon widgets
.append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", (ribbonWidget as any).toggleCommand)) // TODO: base class
.append(" ")
.append($('<span class="ribbon-tab-title-label">').text(ret.title));
@@ -238,7 +254,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
keyboardActionsService.getActions().then((actions) => {
this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({
title: function () {
title: () => {
const toggleCommandName = $(this).attr("data-toggle-command");
const action = actions.find((act) => act.actionName === toggleCommandName);
const title = $(this).attr("data-title");
@@ -246,7 +262,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
if (action && action.effectiveShortcuts.length > 0) {
return `${title} (${action.effectiveShortcuts.join(", ")})`;
} else {
return title;
return title ?? "";
}
}
});
@@ -263,27 +279,27 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
isRibbonTabActive(name) {
isRibbonTabActive(name: string) {
const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
return $ribbonComponent.hasClass("active");
}
ensureOwnedAttributesAreOpen(ntxId) {
if (this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
ensureOwnedAttributesAreOpen(ntxId: string | null | undefined) {
if (ntxId && this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
this.toggleRibbonTabWithName("ownedAttributes", ntxId);
}
}
addNewLabelEvent({ ntxId }) {
addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
this.ensureOwnedAttributesAreOpen(ntxId);
}
addNewRelationEvent({ ntxId }) {
addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
this.ensureOwnedAttributesAreOpen(ntxId);
}
toggleRibbonTabWithName(name, ntxId) {
toggleRibbonTabWithName(name: string, ntxId?: string) {
if (!this.isNoteContext(ntxId)) {
return false;
}
@@ -295,23 +311,23 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
handleEvent(name, data) {
handleEvent<T extends EventNames>(name: T, data: EventData<T>) {
const PREFIX = "toggleRibbonTab";
if (name.startsWith(PREFIX)) {
let componentName = name.substr(PREFIX.length);
componentName = componentName[0].toLowerCase() + componentName.substr(1);
this.toggleRibbonTabWithName(componentName, data.ntxId);
this.toggleRibbonTabWithName(componentName, (data as any).ntxId);
} else {
return super.handleEvent(name, data);
}
}
async handleEventInChildren(name, data) {
async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
if (["activeContextChanged", "setNoteContext"].includes(name)) {
// won't trigger .refresh();
await super.handleEventInChildren("setNoteContext", data);
await super.handleEventInChildren("setNoteContext", data as EventData<"activeContextChanged" | "setNoteContext">);
} else if (this.isEnabled() || name === "initialRenderComplete") {
const activeRibbonWidget = this.getActiveRibbonWidget();
@@ -326,8 +342,12 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (!this.note) {
return;
}
if (this.noteId && loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
// note type influences the list of available ribbon tabs the most
// check for the type is so that we don't update on each title rename
this.lastNoteType = this.note.type;
@@ -338,7 +358,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
noteTypeMimeChangedEvent() {
async noteTypeMimeChangedEvent() {
// We are ignoring the event which triggers a refresh since it is usually already done by a different
// event and causing a race condition in which the items appear twice.
}
@@ -1,6 +1,10 @@
import attributeService from "../services/attributes.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import { t } from "../services/i18n.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
type Editability = "auto" | "readOnly" | "autoReadOnlyDisabled";
const TPL = `
<div class="dropdown editability-select-widget">
@@ -8,7 +12,11 @@ const TPL = `
.editability-dropdown {
width: 300px;
}
.editability-dropdown .dropdown-item {
display: block !important;
}
.editability-dropdown .dropdown-item div {
font-size: small;
color: var(--muted-text-color);
@@ -23,7 +31,7 @@ const TPL = `
<a class="dropdown-item" href="#" data-editability="auto">
<span class="check">&check;</span>
${t("editability_select.auto")}
<div>${t("editability_select.note_is_editable")}</div>
<div>${t("editability_select.note_is_editable")}</div>
</a>
<a class="dropdown-item" href="#" data-editability="readOnly">
<span class="check">&check;</span>
@@ -40,9 +48,15 @@ const TPL = `
`;
export default class EditabilitySelectWidget extends NoteContextAwareWidget {
private dropdown!: bootstrap.Dropdown;
private $editabilityActiveDesc!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
// TODO: Remove once bootstrap is added to webpack.
//@ts-ignore
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc");
@@ -52,24 +66,28 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
const editability = $(e.target).closest("[data-editability]").attr("data-editability");
if (!this.note || !this.noteId) {
return;
}
for (const ownedAttr of this.note.getOwnedLabels()) {
if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) {
await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
}
}
if (editability !== "auto") {
if (editability && editability !== "auto") {
await attributeService.addLabel(this.noteId, editability);
}
});
}
async refreshWithNote(note) {
let editability = "auto";
async refreshWithNote(note: FNote) {
let editability: Editability = "auto";
if (this.note.isLabelTruthy("readOnly")) {
if (this.note?.isLabelTruthy("readOnly")) {
editability = "readOnly";
} else if (this.note.isLabelTruthy("autoReadOnlyDisabled")) {
} else if (this.note?.isLabelTruthy("autoReadOnlyDisabled")) {
editability = "autoReadOnlyDisabled";
}
@@ -85,7 +103,7 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
this.$editabilityActiveDesc.text(labels[editability]);
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) {
this.refresh();
}
@@ -0,0 +1,42 @@
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js"
const TPL = `\
<div class="geo-map-buttons">
<style>
.geo-map-buttons {
display: flex;
gap: 10px;
}
.leaflet-pane {
z-index: 50;
}
.geo-map-buttons {
contain: none;
background: var(--main-background-color);
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
border-radius: 4px;
}
</style>
<button type="button"
class="geo-map-create-child-note floating-button btn bx bx-folder-plus"
title="${t("geo-map.create-child-note-title")}" />
</div>`;
export default class GeoMapButtons extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && this.note?.type === "geoMap";
}
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$widget.find(".geo-map-create-child-note").on("click", () => this.triggerEvent("geoMapCreateChildNote", { ntxId: this.ntxId }));
}
}
@@ -13,16 +13,16 @@ const TPL = `
<button type="button"
class="relation-map-create-child-note floating-button btn bx bx-folder-plus"
title="${t("relation_map_buttons.create_child_note_title")}"></button>
<button type="button"
class="relation-map-reset-pan-zoom floating-button btn bx bx-crop"
title="${t("relation_map_buttons.reset_pan_zoom_title")}"></button>
<div class="btn-group">
<button type="button"
class="relation-map-zoom-in floating-button btn bx bx-zoom-in"
title="${t("relation_map_buttons.zoom_in_title")}"></button>
<button type="button"
class="relation-map-zoom-out floating-button btn bx bx-zoom-out"
title="${t("relation_map_buttons.zoom_out_title")}"></button>
@@ -43,6 +43,7 @@ export default class RelationMapButtons extends NoteContextAwareWidget {
this.$zoomOutButton = this.$widget.find(".relation-map-zoom-out");
this.$resetPanZoomButton = this.$widget.find(".relation-map-reset-pan-zoom");
// TODO: Deduplicate object creation here.
this.$createChildNote.on("click", () => this.triggerEvent("relationMapCreateChildNote", { ntxId: this.ntxId }));
this.$resetPanZoomButton.on("click", () => this.triggerEvent("relationMapResetPanZoom", { ntxId: this.ntxId }));
@@ -7,6 +7,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js";
import linkService from "../../services/link.js";
import server from "../../services/server.js";
import froca from "../../services/froca.js";
import type FNote from "../../entities/fnote.js";
const TPL = `
<div class="backlinks-widget">
@@ -14,7 +15,7 @@ const TPL = `
.backlinks-widget {
position: relative;
}
.backlinks-ticker {
border-radius: 10px;
border-color: var(--main-border-color);
@@ -25,11 +26,11 @@ const TPL = `
justify-content: space-between;
align-items: center;
}
.backlinks-count {
cursor: pointer;
}
.backlinks-items {
z-index: 10;
position: absolute;
@@ -42,29 +43,41 @@ const TPL = `
padding: 20px;
overflow-y: auto;
}
.backlink-excerpt {
border-left: 2px solid var(--main-border-color);
padding-left: 10px;
opacity: 80%;
font-size: 90%;
}
.backlink-excerpt .backlink-link { /* the actual backlink */
font-weight: bold;
background-color: yellow;
}
</style>
<div class="backlinks-ticker">
<span class="backlinks-count"></span>
</div>
</div>
<div class="backlinks-items" style="display: none;"></div>
</div>
`;
// TODO: Deduplicate with server
interface Backlink {
noteId: string;
relationName?: string;
excerpts?: string[];
}
export default class BacklinksWidget extends NoteContextAwareWidget {
private $count!: JQuery<HTMLElement>;
private $items!: JQuery<HTMLElement>;
private $ticker!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$count = this.$widget.find(".backlinks-count");
@@ -73,7 +86,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
this.$count.on("click", () => {
this.$items.toggle();
this.$items.css("max-height", $(window).height() - this.$items.offset().top - 10);
this.$items.css("max-height", ($(window).height() ?? 0) - (this.$items.offset()?.top ?? 0) - 10);
if (this.$items.is(":visible")) {
this.renderBacklinks();
@@ -83,7 +96,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
this.contentSized();
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
this.clearItems();
if (this.noteContext?.viewScope?.viewMode !== "default") {
@@ -92,7 +105,8 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
}
// can't use froca since that would count only relations from loaded notes
const resp = await server.get(`note-map/${this.noteId}/backlink-count`);
// TODO: Deduplicate response type
const resp = await server.get<{ count: number }>(`note-map/${this.noteId}/backlink-count`);
if (!resp || !resp.count) {
this.toggle(false);
@@ -106,7 +120,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
);
}
toggle(show) {
toggle(show: boolean) {
this.$widget.toggleClass("hidden-no-content", !show);
}
@@ -121,7 +135,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
this.$items.empty();
const backlinks = await server.get(`note-map/${this.noteId}/backlinks`);
const backlinks = await server.get<Backlink[]>(`note-map/${this.noteId}/backlinks`);
if (!backlinks.length) {
return;
@@ -143,7 +157,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
if (backlink.relationName) {
$item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`));
} else {
$item.append(...backlink.excerpts);
$item.append(...backlink.excerpts ?? []);
}
this.$items.append($item);
+57
View File
@@ -0,0 +1,57 @@
import type { Map } from "leaflet";
import library_loader from "../services/library_loader.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
const TPL = `\
<div class="geo-map-widget">
<style>
.note-detail-geo-map,
.geo-map-widget,
.geo-map-container {
height: 100%;
overflow: hidden;
}
</style>
<div class="geo-map-container"></div>
</div>`
export type Leaflet = typeof import("leaflet");
export type InitCallback = ((L: Leaflet) => void);
export default class GeoMapWidget extends NoteContextAwareWidget {
map?: Map;
$container!: JQuery<HTMLElement>;
private initCallback?: InitCallback;
constructor(widgetMode: "type", initCallback?: InitCallback) {
super();
this.initCallback = initCallback;
}
doRender() {
this.$widget = $(TPL);
this.$container = this.$widget.find(".geo-map-container");
library_loader.requireLibrary(library_loader.LEAFLET)
.then(async () => {
const L = (await import("leaflet")).default;
const map = L.map(this.$container[0], {
worldCopyJump: true
});
this.map = map;
if (this.initCallback) {
this.initCallback(L);
}
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
});
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ interface Category {
id: number;
}
interface Icon {
export interface Icon {
name: string;
slug: string;
category_id: number;
@@ -4,6 +4,8 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import { loadElkIfNeeded } from "../services/mermaid.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
const TPL = `<div class="mermaid-widget">
<style>
@@ -39,8 +41,14 @@ const TPL = `<div class="mermaid-widget">
let idCounter = 1;
export default class MermaidWidget extends NoteContextAwareWidget {
private $display!: JQuery<HTMLElement>;
private $errorContainer!: JQuery<HTMLElement>;
private $errorMessage!: JQuery<HTMLElement>;
private dirtyAttachment?: boolean;
isEnabled() {
return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope.viewMode === "default";
return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
}
doRender() {
@@ -51,14 +59,14 @@ export default class MermaidWidget extends NoteContextAwareWidget {
this.$errorMessage = this.$errorContainer.find(".error-content");
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
this.$errorContainer.hide();
await libraryLoader.requireLibrary(libraryLoader.MERMAID);
mermaid.mermaidAPI.initialize({
startOnLoad: false,
...getMermaidConfig()
...getMermaidConfig() as any
});
this.$display.empty();
@@ -91,12 +99,11 @@ export default class MermaidWidget extends NoteContextAwareWidget {
this.$display.attr("id", `mermaid-render-${idCounter}`);
WZoom.create(`#mermaid-render-${idCounter}`, {
type: "html",
maxScale: 50,
speed: 1.3,
zoomOnClick: false
});
} catch (e) {
} catch (e: any) {
console.warn(e);
this.$errorMessage.text(e.message);
this.$errorContainer.show();
@@ -106,24 +113,28 @@ export default class MermaidWidget extends NoteContextAwareWidget {
async renderSvg() {
idCounter++;
if (!this.note) {
return "";
}
const blob = await this.note.getBlob();
const content = blob.content || "";
const content = blob?.content || "";
await loadElkIfNeeded(content);
const { svg } = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content);
return svg;
}
async entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteContentReloaded(this.noteId)) {
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
this.dirtyAttachment = true;
await this.refresh();
}
}
async exportSvgEvent({ ntxId }) {
if (!this.isNoteContext(ntxId) || this.note.type !== "mermaid") {
async exportSvgEvent({ ntxId }: EventData<"exportSvg">) {
if (!this.isNoteContext(ntxId) || this.note?.type !== "mermaid") {
return;
}
@@ -139,6 +150,7 @@ export function getMermaidConfig() {
return {
theme: mermaidTheme.trim(),
securityLevel: "antiscript",
// TODO: Are all these options correct?
flow: { useMaxWidth: false },
sequence: { useMaxWidth: false },
gantt: { useMaxWidth: false },
@@ -9,7 +9,9 @@ import { t } from "../../services/i18n.js";
const TPL = `<button type="button" class="action-button bx" style="padding-top: 10px;"></button>`;
class MobileDetailMenuWidget extends BasicWidget {
constructor(isHorizontalLayout) {
private isHorizontalLayout: boolean;
constructor(isHorizontalLayout: boolean) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}
@@ -31,9 +33,13 @@ class MobileDetailMenuWidget extends BasicWidget {
],
selectMenuItemHandler: async ({ command }) => {
if (command === "insertChildNote") {
noteCreateService.createNote(appContext.tabManager.getActiveContextNotePath());
noteCreateService.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
} else if (command === "delete") {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (!notePath) {
throw new Error("Cannot get note path to delete.");
}
const branchId = await treeService.getBranchIdFromUrl(notePath);
if (!branchId) {
@@ -46,7 +52,8 @@ class MobileDetailMenuWidget extends BasicWidget {
} else {
throw new Error(t("mobile_detail_menu.error_unrecognized_command", { command }));
}
}
},
forcePositionOnMobile: true
});
});
}
@@ -10,9 +10,9 @@ import type NoteContext from "../components/note_context.js";
class NoteContextAwareWidget extends BasicWidget {
protected noteContext?: NoteContext;
isNoteContext(ntxId: string | null | undefined) {
isNoteContext(ntxId: string | string[] | null | undefined) {
if (Array.isArray(ntxId)) {
return this.noteContext && ntxId.includes(this.noteContext.ntxId);
return this.noteContext && this.noteContext.ntxId && ntxId.includes(this.noteContext.ntxId);
} else {
return this.noteContext && this.noteContext.ntxId === ntxId;
}
@@ -54,7 +54,7 @@ class NoteContextAwareWidget extends BasicWidget {
*
* @returns true when an active note exists
*/
isEnabled() {
isEnabled(): boolean | null | undefined {
return !!this.note;
}
+6 -4
View File
@@ -31,6 +31,7 @@ import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
import MindMapWidget from "./type_widgets/mind_map.js";
import { getStylesheetUrl, isSyntaxHighlightEnabled } from "../services/syntax_highlight.js";
import GeoMapTypeWidget from "./type_widgets/geo_map.js";
const TPL = `
<div class="note-detail">
@@ -39,7 +40,7 @@ const TPL = `
font-family: var(--detail-font-family);
font-size: var(--detail-font-size);
}
.note-detail.full-height {
height: 100%;
}
@@ -67,7 +68,8 @@ const typeWidgetClasses = {
contentWidget: ContentWidgetTypeWidget,
attachmentDetail: AttachmentDetailTypeWidget,
attachmentList: AttachmentListTypeWidget,
mindMap: MindMapWidget
mindMap: MindMapWidget,
geoMap: GeoMapTypeWidget
};
export default class NoteDetailWidget extends NoteContextAwareWidget {
@@ -147,7 +149,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
// https://github.com/zadam/trilium/issues/2522
this.$widget.toggleClass(
"full-height",
(!this.noteContext.hasNoteList() && ["canvas", "webView", "noteMap", "mindMap"].includes(this.type) && this.mime !== "text/x-sqlite;schema=trilium") ||
(!this.noteContext.hasNoteList() && ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type) && this.mime !== "text/x-sqlite;schema=trilium") ||
this.noteContext.viewScope.viewMode === "attachments"
);
}
@@ -276,7 +278,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
<script src="${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script>
<script>
document.body.className += ' ck-content printed-content';
renderMathInElement(document.body, {trust: true});
</script>
`,
@@ -2,6 +2,9 @@ import { t } from "../services/i18n.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import attributeService from "../services/attributes.js";
import server from "../services/server.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
import type { Icon } from "./icon_list.js";
const TPL = `
<div class="note-icon-widget dropdown">
@@ -13,7 +16,7 @@ const TPL = `
width: 50px;
height: 50px;
}
.note-icon-widget button.note-icon {
font-size: 180%;
background-color: transparent;
@@ -22,38 +25,38 @@ const TPL = `
padding: 6px;
color: var(--main-text-color);
}
.note-icon-widget button.note-icon:hover {
border: 1px solid var(--main-border-color);
}
.note-icon-widget .dropdown-menu {
border-radius: 10px;
border-width: 2px;
box-shadow: 10px 10px 93px -25px black;
padding: 10px 15px 10px 15px !important;
}
.note-icon-widget .filter-row {
padding-top: 10px;
padding-bottom: 10px;
padding-right: 20px;
display: flex;
display: flex;
align-items: baseline;
}
.note-icon-widget .filter-row span {
display: block;
padding-left: 15px;
padding-right: 15px;
font-weight: bold;
}
.note-icon-widget .icon-list {
height: 500px;
overflow: auto;
}
.note-icon-widget .icon-list span {
display: inline-block;
padding: 10px;
@@ -61,25 +64,36 @@ const TPL = `
border: 1px solid transparent;
font-size: 180%;
}
.note-icon-widget .icon-list span:hover {
border: 1px solid var(--main-border-color);
}
</style>
<button class="btn dropdown-toggle note-icon" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="${t("note_icon.change_note_icon")}"></button>
<div class="dropdown-menu" aria-labelledby="note-path-list-button" style="width: 610px;">
<div class="filter-row">
<span>${t("note_icon.category")}</span> <select name="icon-category" class="form-control"></select>
<span>${t("note_icon.search")}</span> <input type="text" name="icon-search" class="form-control" />
</div>
<div class="icon-list"></div>
</div>
</div>`;
interface IconToCountCache {
iconClassToCountMap: Record<string, number>;
}
export default class NoteIconWidget extends NoteContextAwareWidget {
private $icon!: JQuery<HTMLElement>;
private $iconList!: JQuery<HTMLElement>;
private $iconCategory!: JQuery<HTMLElement>;
private $iconSearch!: JQuery<HTMLElement>;
private iconToCountCache!: Promise<IconToCountCache | null> | null;
doRender() {
this.$widget = $(TPL);
this.$icon = this.$widget.find("button.note-icon");
@@ -87,7 +101,9 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
this.$iconList.on("click", "span", async (e) => {
const clazz = $(e.target).attr("class");
await attributeService.setLabel(this.noteId, this.note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass", clazz);
if (this.noteId && this.note) {
await attributeService.setLabel(this.noteId, this.note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass", clazz);
}
});
this.$iconCategory = this.$widget.find("select[name='icon-category']");
@@ -97,7 +113,6 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
this.$iconSearch = this.$widget.find("input[name='icon-search']");
this.$iconSearch.on("input", () => this.renderDropdown());
this.$notePathList = this.$widget.find(".note-path-list");
this.$widget.on("show.bs.dropdown", async () => {
const { categories } = (await import("./icon_list.js")).default;
@@ -113,18 +128,18 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
});
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
this.$icon.removeClass().addClass(`${note.getIcon()} note-icon`);
}
async entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteReloaded(this.noteId)) {
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.noteId && loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
return;
}
for (const attr of loadResults.getAttributeRows()) {
if (attr.type === "label" && ["iconClass", "workspaceIconClass"].includes(attr.name) && attributeService.isAffecting(attr, this.note)) {
if (attr.type === "label" && ["iconClass", "workspaceIconClass"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.note)) {
this.refresh();
break;
}
@@ -141,14 +156,18 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
this.$iconList.append(
$(`<div style="text-align: center">`).append(
$(`<button class="btn btn-sm">${t("note_icon.reset-default")}</button>`).on("click", () =>
this.getIconLabels().forEach((label) => attributeService.removeAttributeById(this.noteId, label.attributeId))
this.getIconLabels().forEach((label) => {
if (this.noteId) {
attributeService.removeAttributeById(this.noteId, label.attributeId);
}
})
)
)
);
}
const categoryId = parseInt(this.$iconCategory.find("option:selected").val());
const search = this.$iconSearch.val().trim().toLowerCase();
const categoryId = parseInt(String(this.$iconCategory.find("option:selected")?.val()));
const search = String(this.$iconSearch.val())?.trim()?.toLowerCase();
const filteredIcons = icons.filter((icon) => {
if (categoryId && icon.category_id !== categoryId) {
@@ -164,12 +183,14 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
return true;
});
filteredIcons.sort((a, b) => {
const countA = iconToCount[a.className] || 0;
const countB = iconToCount[b.className] || 0;
if (iconToCount) {
filteredIcons.sort((a, b) => {
const countA = iconToCount[a.className ?? ""] || 0;
const countB = iconToCount[b.className ?? ""] || 0;
return countB - countA;
});
return countB - countA;
});
}
for (const icon of filteredIcons) {
this.$iconList.append(this.renderIcon(icon));
@@ -180,20 +201,23 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
async getIconToCountMap() {
if (!this.iconToCountCache) {
this.iconToCountCache = server.get("other/icon-usage");
this.iconToCountCache = server.get<typeof this.iconToCountCache>("other/icon-usage");
setTimeout(() => (this.iconToCountCache = null), 20000); // invalidate cache after 20 seconds
}
return (await this.iconToCountCache).iconClassToCountMap;
return (await this.iconToCountCache)?.iconClassToCountMap;
}
renderIcon(icon) {
renderIcon(icon: Icon) {
return $("<span>")
.addClass("bx " + icon.className)
.attr("title", icon.name);
}
getIconLabels() {
if (!this.note) {
return [];
}
return this.note.getOwnedLabels().filter((label) => ["workspaceIconClass", "iconClass"].includes(label.name));
}
}

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