diff --git a/bin/build-linux-x64.sh b/bin/build-linux-x64.sh index 68d67a8b5..6e2cf87bc 100755 --- a/bin/build-linux-x64.sh +++ b/bin/build-linux-x64.sh @@ -30,6 +30,7 @@ rm -r $BUILD_DIR/swiftshader cp bin/tpl/anonymize-database.sql $BUILD_DIR/ cp -r dump-db $BUILD_DIR/ +rm -rf $BUILD_DIR/dump-db/node_modules cp bin/tpl/trilium-portable.sh $BUILD_DIR/ chmod 755 $BUILD_DIR/trilium-portable.sh diff --git a/bin/build-mac-x64.sh b/bin/build-mac-x64.sh index fd16cc2a2..0e63c7fb2 100755 --- a/bin/build-mac-x64.sh +++ b/bin/build-mac-x64.sh @@ -26,6 +26,7 @@ mv "./dist/Trilium Notes-darwin-x64" $BUILD_DIR cp bin/tpl/anonymize-database.sql $BUILD_DIR/ cp -r dump-db $BUILD_DIR/ +rm -rf $BUILD_DIR/dump-db/node_modules echo "Zipping mac x64 electron distribution..." diff --git a/bin/build-server.sh b/bin/build-server.sh index 202cb82a3..accb11f32 100755 --- a/bin/build-server.sh +++ b/bin/build-server.sh @@ -31,6 +31,7 @@ chmod 755 $PKG_DIR/trilium.sh cp bin/tpl/anonymize-database.sql $PKG_DIR/ cp -r dump-db $PKG_DIR/ +rm -rf $PKG_DIR/dump-db/node_modules VERSION=`jq -r ".version" package.json` diff --git a/bin/build-win-x64.sh b/bin/build-win-x64.sh index 0f7040a04..9a347a30b 100755 --- a/bin/build-win-x64.sh +++ b/bin/build-win-x64.sh @@ -28,6 +28,7 @@ rm -r $BUILD_DIR/swiftshader cp bin/tpl/anonymize-database.sql $BUILD_DIR/ cp -r dump-db $BUILD_DIR/ +rm -rf $BUILD_DIR/dump-db/node_modules cp bin/tpl/trilium-{portable,no-cert-check,safe-mode}.{bat,ps1} $BUILD_DIR/ diff --git a/bin/copy-trilium.sh b/bin/copy-trilium.sh index e63ec9442..77f754d17 100755 --- a/bin/copy-trilium.sh +++ b/bin/copy-trilium.sh @@ -14,9 +14,6 @@ mkdir $DIR echo "Copying Trilium to build directory $DIR" -cp -r dump-db $DIR/ -rm -rf $DIR/dump-db/node_modules - cp -r images $DIR/ cp -r libraries $DIR/ cp -r src $DIR/ diff --git a/db/migrations/0200__create_hidden_subtree.js b/db/migrations/0200__create_hidden_subtree.js index aba83ac4e..990566283 100644 --- a/db/migrations/0200__create_hidden_subtree.js +++ b/db/migrations/0200__create_hidden_subtree.js @@ -1,5 +1,5 @@ module.exports = () => { - const hiddenSubtreeService = require('../../src/services/hidden_subtree.js'); + const hiddenSubtreeService = require('../../src/services/hidden_subtree'); const cls = require("../../src/services/cls"); const beccaLoader = require("../../src/becca/becca_loader"); diff --git a/dump-db/dump-db.js b/dump-db/dump-db.js index ece36f39e..2e37486c0 100755 --- a/dump-db/dump-db.js +++ b/dump-db/dump-db.js @@ -2,7 +2,7 @@ const yargs = require('yargs/yargs') const { hideBin } = require('yargs/helpers') -const dumpService = require("./inc/dump.js"); +const dumpService = require("./inc/dump"); yargs(hideBin(process.argv)) .command('$0 ', 'dump the contents of document.db into the target directory', (yargs) => { diff --git a/dump-db/inc/data_key.js b/dump-db/inc/data_key.js index c879bafad..fe47ddd6f 100644 --- a/dump-db/inc/data_key.js +++ b/dump-db/inc/data_key.js @@ -1,6 +1,6 @@ const crypto = require("crypto"); -const sql = require("./sql.js"); -const decryptService = require("./decrypt.js"); +const sql = require("./sql"); +const decryptService = require("./decrypt"); function getDataKey(password) { if (!password) { diff --git a/dump-db/inc/dump.js b/dump-db/inc/dump.js index 6145f040a..cd6ba5534 100644 --- a/dump-db/inc/dump.js +++ b/dump-db/inc/dump.js @@ -1,9 +1,9 @@ const fs = require("fs"); const sanitize = require("sanitize-filename"); -const sql = require("./sql.js"); -const decryptService = require("./decrypt.js"); -const dataKeyService = require("./data_key.js"); -const extensionService = require("./extension.js"); +const sql = require("./sql"); +const decryptService = require("./decrypt"); +const dataKeyService = require("./data_key"); +const extensionService = require("./extension"); function dumpDocument(documentPath, targetPath, options) { const stats = { diff --git a/libraries/boxicons/LICENSE b/libraries/boxicons/LICENSE new file mode 100644 index 000000000..2ab6d9df4 --- /dev/null +++ b/libraries/boxicons/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2021 Aniket Suvarna + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libraries/boxicons/fonts/boxicons.eot b/libraries/boxicons/fonts/boxicons.eot index c81a1dea3..6e58e9946 100644 Binary files a/libraries/boxicons/fonts/boxicons.eot and b/libraries/boxicons/fonts/boxicons.eot differ diff --git a/libraries/boxicons/fonts/boxicons.svg b/libraries/boxicons/fonts/boxicons.svg index edce3834f..4b27b5140 100644 --- a/libraries/boxicons/fonts/boxicons.svg +++ b/libraries/boxicons/fonts/boxicons.svg @@ -23,1631 +23,1638 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libraries/boxicons/fonts/boxicons.ttf b/libraries/boxicons/fonts/boxicons.ttf index 998f0598b..89a509681 100644 Binary files a/libraries/boxicons/fonts/boxicons.ttf and b/libraries/boxicons/fonts/boxicons.ttf differ diff --git a/libraries/boxicons/fonts/boxicons.woff b/libraries/boxicons/fonts/boxicons.woff index 3345c5ce0..841e1d41c 100644 Binary files a/libraries/boxicons/fonts/boxicons.woff and b/libraries/boxicons/fonts/boxicons.woff differ diff --git a/libraries/boxicons/fonts/boxicons.woff2 b/libraries/boxicons/fonts/boxicons.woff2 index 07d26184c..79c35e4c9 100644 Binary files a/libraries/boxicons/fonts/boxicons.woff2 and b/libraries/boxicons/fonts/boxicons.woff2 differ diff --git a/package-lock.json b/package-lock.json index 9da7d3d6c..3ea3c36e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "trilium", "version": "0.57.3", "hasInstallScript": true, "license": "AGPL-3.0-only", diff --git a/spec/search/search.spec.js b/spec/search/search.spec.js index e74d48107..ce826ed16 100644 --- a/spec/search/search.spec.js +++ b/spec/search/search.spec.js @@ -4,7 +4,7 @@ const Branch = require('../../src/becca/entities/branch'); const SearchContext = require('../../src/services/search/search_context'); const dateUtils = require('../../src/services/date_utils'); const becca = require('../../src/becca/becca'); -const {NoteBuilder, findNoteByTitle, note} = require('./becca_mocking.js'); +const {NoteBuilder, findNoteByTitle, note} = require('./becca_mocking'); describe("Search", () => { let rootNote; diff --git a/spec/search/value_extractor.spec.js b/spec/search/value_extractor.spec.js index 1e664279f..04fe3c303 100644 --- a/spec/search/value_extractor.spec.js +++ b/spec/search/value_extractor.spec.js @@ -1,4 +1,4 @@ -const {note} = require('./becca_mocking.js'); +const {note} = require('./becca_mocking'); const ValueExtractor = require('../../src/services/search/value_extractor'); const becca = require('../../src/becca/becca'); const SearchContext = require("../../src/services/search/search_context"); diff --git a/src/becca/becca_service.js b/src/becca/becca_service.js index 4337903b0..d666c502a 100644 --- a/src/becca/becca_service.js +++ b/src/becca/becca_service.js @@ -1,6 +1,6 @@ "use strict"; -const becca = require('./becca.js'); +const becca = require('./becca'); const cls = require('../services/cls'); const protectedSessionService = require('../services/protected_session'); const log = require('../services/log'); diff --git a/src/becca/entities/branch.js b/src/becca/entities/branch.js index 691a46e39..2ed3ec070 100644 --- a/src/becca/entities/branch.js +++ b/src/becca/entities/branch.js @@ -4,7 +4,7 @@ const Note = require('./note'); const AbstractEntity = require("./abstract_entity"); const sql = require("../../services/sql"); const dateUtils = require("../../services/date_utils"); -const utils = require("../../services/utils.js"); +const utils = require("../../services/utils"); const TaskContext = require("../../services/task_context"); const cls = require("../../services/cls"); const log = require("../../services/log"); diff --git a/src/becca/entities/note.js b/src/becca/entities/note.js index 4e2593c48..8693a4263 100644 --- a/src/becca/entities/note.js +++ b/src/becca/entities/note.js @@ -1329,7 +1329,7 @@ class Note extends AbstractEntity { } isLaunchBarConfig() { - return this.type === 'launcher' || ['lbRoot', 'lbAvailableShortcuts', 'lbVisibleShortcuts']; + return this.type === 'launcher' || ['lbRoot', 'lbAvailableLaunchers', 'lbVisibleLaunchers'].includes(this.noteId); } isOptions() { diff --git a/src/becca/similarity.js b/src/becca/similarity.js index f270960c0..efb8734d6 100644 --- a/src/becca/similarity.js +++ b/src/becca/similarity.js @@ -1,6 +1,6 @@ const becca = require('./becca'); const log = require('../services/log'); -const beccaService = require('./becca_service.js'); +const beccaService = require('./becca_service'); const dateUtils = require('../services/date_utils'); const { JSDOM } = require("jsdom"); diff --git a/src/errors/not_found_error.js b/src/errors/not_found_error.js new file mode 100644 index 000000000..af746b82c --- /dev/null +++ b/src/errors/not_found_error.js @@ -0,0 +1,7 @@ +class NotFoundError { + constructor(message) { + this.message = message; + } +} + +module.exports = NotFoundError; \ No newline at end of file diff --git a/src/errors/validation_error.js b/src/errors/validation_error.js new file mode 100644 index 000000000..1c9425669 --- /dev/null +++ b/src/errors/validation_error.js @@ -0,0 +1,7 @@ +class ValidationError { + constructor(message) { + this.message = message; + } +} + +module.exports = ValidationError; \ No newline at end of file diff --git a/src/etapi/app_info.js b/src/etapi/app_info.js index ede96d792..3dc4481ac 100644 --- a/src/etapi/app_info.js +++ b/src/etapi/app_info.js @@ -1,5 +1,5 @@ const appInfo = require('../services/app_info'); -const eu = require("./etapi_utils.js"); +const eu = require("./etapi_utils"); function register(router) { eu.route(router, 'get', '/etapi/app-info', (req, res, next) => { diff --git a/src/public/app/components/note_context.js b/src/public/app/components/note_context.js index 0b6eab882..0f042681e 100644 --- a/src/public/app/components/note_context.js +++ b/src/public/app/components/note_context.js @@ -63,7 +63,10 @@ class NoteContext extends Component { }); } - if (this.hoistedNoteId === 'root' && this.notePath.startsWith("root/hidden")) { + if (this.hoistedNoteId === 'root' + && this.notePath.startsWith("root/hidden") + && !this.note.hasLabel("keepCurrentHoisting") + ) { // hidden subtree displays only when hoisted so it doesn't make sense to keep root as hoisted note let hoistedNoteId = 'hidden'; diff --git a/src/public/app/entities/note_short.js b/src/public/app/entities/note_short.js index 3f42c1c23..7207c04de 100644 --- a/src/public/app/entities/note_short.js +++ b/src/public/app/entities/note_short.js @@ -828,7 +828,7 @@ class NoteShort { } isLaunchBarConfig() { - return this.type === 'launcher' || ['lbRoot', 'lbAvailableShortcuts', 'lbVisibleShortcuts'].includes(this.noteId); + return this.type === 'launcher' || ['lbRoot', 'lbAvailableLaunchers', 'lbVisibleLaunchers'].includes(this.noteId); } isOptions() { diff --git a/src/public/app/services/link.js b/src/public/app/services/link.js index 95065ee33..e799d33e0 100644 --- a/src/public/app/services/link.js +++ b/src/public/app/services/link.js @@ -90,27 +90,27 @@ function getNotePathFromLink($link) { return url ? getNotePathFromUrl(url) : null; } -function goToLink(e) { - const $link = $(e.target).closest("a,.block-link"); +function goToLink(evt) { + const $link = $(evt.target).closest("a,.block-link"); const address = $link.attr('href'); if (address?.startsWith("data:")) { return true; } - e.preventDefault(); - e.stopPropagation(); + evt.preventDefault(); + evt.stopPropagation(); const notePath = getNotePathFromLink($link); - const ctrlKey = (!utils.isMac() && e.ctrlKey) || (utils.isMac() && e.metaKey); + const ctrlKey = utils.isCtrlKey(evt); if (notePath) { - if ((e.which === 1 && ctrlKey) || e.which === 2) { + if ((evt.which === 1 && ctrlKey) || evt.which === 2) { appContext.tabManager.openTabWithNoteWithHoisting(notePath); } - else if (e.which === 1) { - const ntxId = $(e.target).closest("[data-ntx-id]").attr("data-ntx-id"); + else if (evt.which === 1) { + const ntxId = $(evt.target).closest("[data-ntx-id]").attr("data-ntx-id"); const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) @@ -124,7 +124,7 @@ function goToLink(e) { } } else { - if ((e.which === 1 && ctrlKey) || e.which === 2 + if ((evt.which === 1 && ctrlKey) || evt.which === 2 || $link.hasClass("ck-link-actions__preview") // within edit link dialog single click suffices || $link.closest("[contenteditable]").length === 0 // outside of CKEditor single click suffices ) { diff --git a/src/public/app/services/server.js b/src/public/app/services/server.js index e18663c61..feded7a6e 100644 --- a/src/public/app/services/server.js +++ b/src/public/app/services/server.js @@ -1,4 +1,5 @@ import utils from './utils.js'; +import ValidationError from "./validation_error.js"; const REQUEST_LOGGING_ENABLED = false; @@ -102,10 +103,15 @@ async function call(method, url, data, headers = {}) { return resp.body; } -async function reportError(method, url, status, error) { - const message = "Error when calling " + method + " " + url + ": " + status + " - " + error; - +async function reportError(method, url, status, response) { const toastService = (await import("./toast.js")).default; + + if ([400, 404].includes(status) && response && typeof response === 'object') { + toastService.showError(response.message); + throw new ValidationError(response); + } + + const message = "Error when calling " + method + " " + url + ": " + status + " - " + responseText; toastService.showError(message); toastService.throwError(message); } diff --git a/src/public/app/services/utils.js b/src/public/app/services/utils.js index e0a2fa5dd..d8805c14e 100644 --- a/src/public/app/services/utils.js +++ b/src/public/app/services/utils.js @@ -60,6 +60,11 @@ function isMac() { return navigator.platform.indexOf('Mac') > -1; } +function isCtrlKey(evt) { + return (!isMac() && evt.ctrlKey) + || (isMac() && evt.metaKey); +} + function assertArguments() { for (const i in arguments) { if (!arguments[i]) { @@ -362,6 +367,7 @@ export default { now, isElectron, isMac, + isCtrlKey, assertArguments, escapeHtml, stopWatch, diff --git a/src/public/app/services/validation_error.js b/src/public/app/services/validation_error.js new file mode 100644 index 000000000..6d3423e6c --- /dev/null +++ b/src/public/app/services/validation_error.js @@ -0,0 +1,7 @@ +export default class ValidationError { + constructor(resp) { + for (const key in resp) { + this[key] = resp[key]; + } + } +} \ No newline at end of file diff --git a/src/public/app/widgets/attribute_widgets/attribute_editor.js b/src/public/app/widgets/attribute_widgets/attribute_editor.js index 59e8b1906..d3bf0522b 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_editor.js +++ b/src/public/app/widgets/attribute_widgets/attribute_editor.js @@ -357,7 +357,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget { // disable spellcheck for attribute editor this.textEditor.editing.view.change(writer => writer.setAttribute('spellcheck', 'false', this.textEditor.editing.view.document.getRoot())); - //await import(/* webpackIgnore: true */'../../libraries/ckeditor/inspector.js'); + //await import(/* webpackIgnore: true */'../../libraries/ckeditor/inspector'); //CKEditorInspector.attach(this.textEditor); } diff --git a/src/public/app/widgets/buttons/launcher/note_launcher.js b/src/public/app/widgets/buttons/launcher/note_launcher.js index 91e483e80..87e85caec 100644 --- a/src/public/app/widgets/buttons/launcher/note_launcher.js +++ b/src/public/app/widgets/buttons/launcher/note_launcher.js @@ -1,23 +1,55 @@ import AbstractLauncher from "./abstract_launcher.js"; import dialogService from "../../../services/dialog.js"; import appContext from "../../../components/app_context.js"; +import utils from "../../../services/utils.js"; +import linkContextMenuService from "../../../menus/link_context_menu.js"; +// we're intentionally displaying the launcher title and icon instead of the target +// e.g. you want to make launchers to 2 mermaid diagrams which both have mermaid icon (ok), +// but on the launchpad you want them distinguishable. +// for titles, the note titles may follow a different scheme than maybe desirable on the launchpad +// another reason is the discrepancy between what user sees on the launchpad and in the config (esp. icons). +// The only downside is more work in setting up the typical case +// where you actually want to have both title and icon in sync, but for those cases there are bookmarks export default class NoteLauncher extends AbstractLauncher { constructor(launcherNote) { super(launcherNote); this.title(this.launcherNote.title) .icon(this.launcherNote.getIcon()) - .onClick(() => this.launch()); + .onClick((widget, evt) => this.launch(evt)) + .onAuxClick((widget, evt) => this.launch(evt)) + .onContextMenu(evt => { + const targetNoteId = this.getTargetNoteId(); + if (!targetNoteId) { + return; + } + + linkContextMenuService.openContextMenu(targetNoteId, evt); + }); } - launch() { - // we're intentionally displaying the launcher title and icon instead of the target - // e.g. you want to make launchers to 2 mermaid diagrams which both have mermaid icon (ok), - // but on the launchpad you want them distinguishable. - // for titles, the note titles may follow a different scheme than maybe desirable on the launchpad - // another reason is the discrepancy between what user sees on the launchpad and in the config (esp. icons). - // The only (but major) downside is more work in setting up the typical case where you actually want to have both title and icon in sync. + launch(evt) { + const targetNoteId = this.getTargetNoteId(); + if (!targetNoteId) { + return; + } + + if (!evt) { + // keyboard shortcut + appContext.tabManager.getActiveContext().setNote(targetNoteId) + return; + } + + const ctrlKey = utils.isCtrlKey(evt); + if ((evt.which === 1 && ctrlKey) || evt.which === 2) { + appContext.tabManager.openTabWithNoteWithHoisting(targetNoteId); + } else { + appContext.tabManager.getActiveContext().setNote(targetNoteId); + } + } + + getTargetNoteId() { const targetNoteId = this.launcherNote.getRelationValue('targetNote'); if (!targetNoteId) { @@ -25,7 +57,7 @@ export default class NoteLauncher extends AbstractLauncher { return; } - appContext.tabManager.openTabWithNoteWithHoisting(targetNoteId, true); + return targetNoteId; } getTitle() { diff --git a/src/public/app/widgets/buttons/onclick_button.js b/src/public/app/widgets/buttons/onclick_button.js index 00e724054..ea4fafd5f 100644 --- a/src/public/app/widgets/buttons/onclick_button.js +++ b/src/public/app/widgets/buttons/onclick_button.js @@ -13,10 +13,23 @@ export default class OnClickButtonWidget extends AbstractButtonWidget { } else { console.warn(`Button widget '${this.componentId}' has no defined click handler`, this.settings); } + + if (this.settings.onAuxClick) { + this.$widget.on("auxclick", e => { + this.$widget.tooltip("hide"); + + this.settings.onAuxClick(this, e); + }); + } } onClick(handler) { this.settings.onClick = handler; return this; } + + onAuxClick(handler) { + this.settings.onAuxClick = handler; + return this; + } } diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js index 350431efc..04524a645 100644 --- a/src/public/app/widgets/note_tree.js +++ b/src/public/app/widgets/note_tree.js @@ -452,7 +452,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) { const files = [...dataTransfer.files]; // chrome has issue that dataTransfer.files empties after async operation - const importService = await import('../services/import.js'); + const importService = await import('../services/import'); importService.uploadFiles(node.data.noteId, files, { safeImport: true, @@ -568,7 +568,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { $span.append($refreshSearchButton); } - if (!['search', 'launcher'].includes(note.type) && !note.isOptions()) { + if (!['search', 'launcher'].includes(note.type) && !note.isOptions() && !note.isLaunchBarConfig()) { const $createChildNoteButton = $(''); $span.append($createChildNoteButton); diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js index c41d020aa..8f53e7207 100644 --- a/src/public/app/widgets/type_widgets/editable_text.js +++ b/src/public/app/widgets/type_widgets/editable_text.js @@ -132,7 +132,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { this.textEditor.model.document.on('change:data', () => this.spacedUpdate.scheduleUpdate()); if (glob.isDev && ENABLE_INSPECTOR) { - await import(/* webpackIgnore: true */'../../../libraries/ckeditor/inspector.js'); + await import(/* webpackIgnore: true */'../../../libraries/ckeditor/inspector'); CKEditorInspector.attach(this.textEditor); } } diff --git a/src/routes/api/attributes.js b/src/routes/api/attributes.js index 6a2269228..ec4ab2c3f 100644 --- a/src/routes/api/attributes.js +++ b/src/routes/api/attributes.js @@ -5,6 +5,8 @@ const log = require('../../services/log'); const attributeService = require('../../services/attributes'); const Attribute = require('../../becca/entities/attribute'); const becca = require("../../becca/becca"); +const ValidationError = require("../../errors/validation_error"); +const NotFoundError = require("../../errors/not_found_error"); function getEffectiveNoteAttributes(req) { const note = becca.getNote(req.params.noteId); @@ -21,11 +23,11 @@ function updateNoteAttribute(req) { attribute = becca.getAttribute(body.attributeId); if (!attribute) { - return [404, `Attribute '${body.attributeId}' does not exist.`]; + throw new NotFoundError(`Attribute '${body.attributeId}' does not exist.`); } if (attribute.noteId !== noteId) { - return [400, `Attribute '${body.attributeId}' is not owned by ${noteId}`]; + throw new ValidationError(`Attribute '${body.attributeId}' is not owned by ${noteId}`); } if (body.type !== attribute.type @@ -106,7 +108,7 @@ function deleteNoteAttribute(req) { if (attribute) { if (attribute.noteId !== noteId) { - return [400, `Attribute ${attributeId} is not owned by ${noteId}`]; + throw new ValidationError(`Attribute ${attributeId} is not owned by ${noteId}`); } attribute.markAsDeleted(); diff --git a/src/routes/api/branches.js b/src/routes/api/branches.js index a44a93f1e..cb3f31b71 100644 --- a/src/routes/api/branches.js +++ b/src/routes/api/branches.js @@ -8,7 +8,9 @@ const noteService = require('../../services/notes'); const becca = require('../../becca/becca'); const TaskContext = require('../../services/task_context'); const branchService = require("../../services/branches"); -const log = require("../../services/log.js"); +const log = require("../../services/log"); +const ValidationError = require("../../errors/validation_error"); +const NotFoundError = require("../../errors/not_found_error"); /** * Code in this file deals with moving and cloning branches. Relationship between note and parent note is unique @@ -22,7 +24,7 @@ function moveBranchToParent(req) { const branchToMove = becca.getBranch(branchId); if (!parentBranch || !branchToMove) { - return [400, `One or both branches ${branchId}, ${parentBranchId} have not been found`]; + throw new ValidationError(`One or both branches ${branchId}, ${parentBranchId} have not been found`); } return branchService.moveBranchToBranch(branchToMove, parentBranch, branchId); @@ -35,11 +37,11 @@ function moveBranchBeforeNote(req) { const beforeBranch = becca.getBranch(beforeBranchId); if (!branchToMove) { - return [404, `Can't find branch ${branchId}`]; + throw new NotFoundError(`Can't find branch '${branchId}'`); } if (!beforeBranch) { - return [404, `Can't find branch ${beforeBranchId}`]; + throw new NotFoundError(`Can't find branch '${beforeBranchId}'`); } const validationResult = treeService.validateParentChild(beforeBranch.parentNoteId, branchToMove.noteId, branchId); @@ -193,7 +195,7 @@ function deleteBranch(req) { const branch = becca.getBranch(req.params.branchId); if (!branch) { - return [404, `Branch ${req.params.branchId} not found`]; + throw new NotFoundError(`Branch '${req.params.branchId}' not found`); } const taskContext = TaskContext.getInstance(req.query.taskId, 'delete-notes'); diff --git a/src/routes/api/export.js b/src/routes/api/export.js index 6acb19952..365c7e185 100644 --- a/src/routes/api/export.js +++ b/src/routes/api/export.js @@ -6,6 +6,7 @@ const opmlExportService = require('../../services/export/opml'); const becca = require('../../becca/becca'); const TaskContext = require("../../services/task_context"); const log = require("../../services/log"); +const NotFoundError = require("../../errors/not_found_error"); function exportBranch(req, res) { const {branchId, type, format, version, taskId} = req.params; @@ -34,11 +35,11 @@ function exportBranch(req, res) { opmlExportService.exportToOpml(taskContext, branch, version, res); } else { - return [404, "Unrecognized export format " + format]; + throw new NotFoundError(`Unrecognized export format '${format}'`); } } catch (e) { - const message = "Export failed with following error: '" + e.message + "'. More details might be in the logs."; + const message = `Export failed with following error: '${e.message}'. More details might be in the logs.`; taskContext.reportError(message); log.error(message + e.stack); diff --git a/src/routes/api/files.js b/src/routes/api/files.js index fc7ae6591..7e41af4ae 100644 --- a/src/routes/api/files.js +++ b/src/routes/api/files.js @@ -10,6 +10,7 @@ const { Readable } = require('stream'); const chokidar = require('chokidar'); const ws = require('../../services/ws'); const becca = require("../../becca/becca"); +const NotFoundError = require("../../errors/not_found_error"); function updateFile(req) { const {noteId} = req.params; @@ -18,7 +19,7 @@ function updateFile(req) { const note = becca.getNote(noteId); if (!note) { - return [404, `Note ${noteId} doesn't exist.`]; + throw new NotFoundError(`Note '${noteId}' doesn't exist.`); } note.saveNoteRevision(); @@ -116,7 +117,7 @@ function saveToTmpDir(req) { const note = becca.getNote(noteId); if (!note) { - return [404,`Note ${noteId} doesn't exist.`]; + throw new NotFoundError(`Note '${noteId}' doesn't exist.`); } const tmpObj = tmp.fileSync({postfix: getFilename(note)}); diff --git a/src/routes/api/image.js b/src/routes/api/image.js index f54395697..aa6151ce6 100644 --- a/src/routes/api/image.js +++ b/src/routes/api/image.js @@ -4,6 +4,8 @@ const imageService = require('../../services/image'); const becca = require('../../becca/becca'); const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; const fs = require('fs'); +const ValidationError = require("../../errors/validation_error"); +const NotFoundError = require("../../errors/not_found_error"); function returnImage(req, res) { const image = becca.getNote(req.params.noteId); @@ -51,11 +53,11 @@ function uploadImage(req) { const note = becca.getNote(noteId); if (!note) { - return [404, `Note ${noteId} doesn't exist.`]; + throw new NotFoundError(`Note '${noteId}' doesn't exist.`); } if (!["image/png", "image/jpg", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) { - return [400, "Unknown image type: " + file.mimetype]; + throw new ValidationError(`Unknown image type: ${file.mimetype}`); } const {url} = imageService.saveImage(noteId, file.buffer, file.originalname, true, true); @@ -73,7 +75,7 @@ function updateImage(req) { const note = becca.getNote(noteId); if (!note) { - return [404, `Note ${noteId} doesn't exist.`]; + throw new NotFoundError(`Note '${noteId}' doesn't exist.`); } if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) { diff --git a/src/routes/api/import.js b/src/routes/api/import.js index 80ecbb255..f62f37b7c 100644 --- a/src/routes/api/import.js +++ b/src/routes/api/import.js @@ -10,6 +10,8 @@ const becca = require('../../becca/becca'); const beccaLoader = require('../../becca/becca_loader'); const log = require('../../services/log'); const TaskContext = require('../../services/task_context'); +const ValidationError = require("../../errors/validation_error"); +const NotFoundError = require("../../errors/not_found_error"); async function importToBranch(req) { const {parentNoteId} = req.params; @@ -27,13 +29,13 @@ async function importToBranch(req) { const file = req.file; if (!file) { - return [400, "No file has been uploaded"]; + throw new ValidationError("No file has been uploaded"); } const parentNote = becca.getNote(parentNoteId); if (!parentNote) { - return [404, `Note ${parentNoteId} doesn't exist.`]; + throw new NotFoundError(`Note '${parentNoteId}' doesn't exist.`); } const extension = path.extname(file.originalname).toLowerCase(); diff --git a/src/routes/api/note_map.js b/src/routes/api/note_map.js index 09b593c7f..4485130a0 100644 --- a/src/routes/api/note_map.js +++ b/src/routes/api/note_map.js @@ -2,6 +2,7 @@ const becca = require("../../becca/becca"); const { JSDOM } = require("jsdom"); +const NotFoundError = require("../../errors/not_found_error"); function buildDescendantCountMap() { const noteIdToCountMap = {}; @@ -326,7 +327,7 @@ function getBacklinkCount(req) { const note = becca.getNote(noteId); if (!note) { - return [404, "Not found"]; + throw new NotFoundError(`Note '${noteId}' not found`); } else { return { @@ -340,7 +341,7 @@ function getBacklinks(req) { const note = becca.getNote(noteId); if (!note) { - return [404, `Note ${noteId} was not found`]; + throw new NotFoundError(`Note '${noteId}' was not found`); } let backlinksWithExcerptCount = 0; diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js index ebb345623..ba112b034 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.js @@ -6,16 +6,17 @@ const sql = require('../../services/sql'); const utils = require('../../services/utils'); const log = require('../../services/log'); const TaskContext = require('../../services/task_context'); -const protectedSessionService = require('../../services/protected_session'); const fs = require('fs'); const becca = require("../../becca/becca"); +const ValidationError = require("../../errors/validation_error"); +const NotFoundError = require("../../errors/not_found_error"); function getNote(req) { const noteId = req.params.noteId; const note = becca.getNote(noteId); if (!note) { - return [404, "Note " + noteId + " has not been found."]; + throw new NotFoundError(`Note '${noteId}' has not been found.`); } const pojo = note.getPojo(); @@ -197,11 +198,11 @@ function changeTitle(req) { const note = becca.getNote(noteId); if (!note) { - return [404, `Note '${noteId}' has not been found`]; + throw new NotFoundError(`Note '${noteId}' has not been found`); } if (!note.isContentAvailable()) { - return [400, `Note '${noteId}' is not available for change`]; + throw new ValidationError(`Note '${noteId}' is not available for change`); } const noteTitleChanged = note.title !== title; @@ -290,7 +291,7 @@ function uploadModifiedFile(req) { const note = becca.getNote(noteId); if (!note) { - return [404, `Note '${noteId}' has not been found`]; + throw new NotFoundError(`Note '${noteId}' has not been found`); } log.info(`Updating note '${noteId}' with content from ${filePath}`); @@ -300,7 +301,7 @@ function uploadModifiedFile(req) { const fileContent = fs.readFileSync(filePath); if (!fileContent) { - return [400, `File ${fileContent} is empty`]; + throw new ValidationError(`File '${fileContent}' is empty`); } note.setContent(fileContent); @@ -311,11 +312,11 @@ function forceSaveNoteRevision(req) { const note = becca.getNote(noteId); if (!note) { - return [404, `Note ${noteId} not found.`]; + throw new NotFoundError(`Note '${noteId}' not found.`); } if (!note.isContentAvailable()) { - return [400, `Note revision of a protected note cannot be created outside of a protected session.`]; + throw new ValidationError(`Note revision of a protected note cannot be created outside of a protected session.`); } note.saveNoteRevision(); diff --git a/src/routes/api/options.js b/src/routes/api/options.js index e8e7bfe08..a176bb45f 100644 --- a/src/routes/api/options.js +++ b/src/routes/api/options.js @@ -3,6 +3,7 @@ const optionService = require('../../services/options'); const log = require('../../services/log'); const searchService = require('../../services/search/services/search'); +const ValidationError = require("../../errors/validation_error"); // options allowed to be updated directly in options dialog const ALLOWED_OPTIONS = new Set([ @@ -82,7 +83,7 @@ function updateOption(req) { const {name, value} = req.params; if (!update(name, value)) { - return [400, "not allowed option to change"]; + throw new ValidationError("not allowed option to change"); } } diff --git a/src/routes/api/password.js b/src/routes/api/password.js index 47466bd7e..289a18716 100644 --- a/src/routes/api/password.js +++ b/src/routes/api/password.js @@ -1,6 +1,7 @@ "use strict"; const passwordService = require('../../services/password'); +const ValidationError = require("../../errors/validation_error"); function changePassword(req) { if (passwordService.isPasswordSet()) { @@ -14,7 +15,7 @@ function changePassword(req) { function resetPassword(req) { // protection against accidental call (not a security measure) if (req.query.really !== "yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes") { - return [400, "Incorrect password reset confirmation"]; + throw new ValidationError("Incorrect password reset confirmation"); } return passwordService.resetPassword(); diff --git a/src/routes/api/search.js b/src/routes/api/search.js index 929cc7c05..b9c1f5412 100644 --- a/src/routes/api/search.js +++ b/src/routes/api/search.js @@ -6,12 +6,14 @@ const searchService = require('../../services/search/services/search'); const bulkActionService = require("../../services/bulk_actions"); const cls = require("../../services/cls"); const {formatAttrForSearch} = require("../../services/attribute_formatter"); +const ValidationError = require("../../errors/validation_error"); +const NotFoundError = require("../../errors/not_found_error"); function searchFromNote(req) { const note = becca.getNote(req.params.noteId); if (!note) { - return [404, `Note ${req.params.noteId} has not been found.`]; + throw new NotFoundError(`Note '${req.params.noteId}' has not been found.`); } if (note.isDeleted) { @@ -20,7 +22,7 @@ function searchFromNote(req) { } if (note.type !== 'search') { - return [400, `Note ${req.params.noteId} is not a search note.`] + throw new ValidationError(`Note '${req.params.noteId}' is not a search note.`); } return searchService.searchFromNote(note); @@ -30,16 +32,16 @@ function searchAndExecute(req) { const note = becca.getNote(req.params.noteId); if (!note) { - return [404, `Note ${req.params.noteId} has not been found.`]; + throw new NotFoundError(`Note '${req.params.noteId}' has not been found.`); } if (note.isDeleted) { - // this can be triggered from recent changes and it's harmless to return empty list rather than fail + // this can be triggered from recent changes, and it's harmless to return empty list rather than fail return []; } if (note.type !== 'search') { - return [400, `Note ${req.params.noteId} is not a search note.`] + throw new ValidationError(`Note '${req.params.noteId}' is not a search note.`); } const {searchResultNoteIds} = searchService.searchFromNote(note); diff --git a/src/routes/api/similar_notes.js b/src/routes/api/similar_notes.js index 348a3ddde..e27968ebe 100644 --- a/src/routes/api/similar_notes.js +++ b/src/routes/api/similar_notes.js @@ -2,6 +2,7 @@ const similarityService = require('../../becca/similarity'); const becca = require("../../becca/becca"); +const NotFoundError = require("../../errors/not_found_error"); async function getSimilarNotes(req) { const noteId = req.params.noteId; @@ -9,7 +10,7 @@ async function getSimilarNotes(req) { const note = becca.getNote(noteId); if (!note) { - return [404, `Note ${noteId} not found.`]; + throw new NotFoundError(`Note '${noteId}' not found.`); } return await similarityService.findSimilarNotes(noteId); diff --git a/src/routes/api/sql.js b/src/routes/api/sql.js index b20f566e7..09e14cc86 100644 --- a/src/routes/api/sql.js +++ b/src/routes/api/sql.js @@ -2,6 +2,7 @@ const sql = require('../../services/sql'); const becca = require("../../becca/becca"); +const NotFoundError = require("../../errors/not_found_error"); function getSchema() { const tableNames = sql.getColumn(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`); @@ -21,7 +22,7 @@ function execute(req) { const note = becca.getNote(req.params.noteId); if (!note) { - return [404, `Note ${req.params.noteId} was not found.`]; + throw new NotFoundError(`Note '${req.params.noteId}' was not found.`); } const queries = note.getContent().split("\n---"); diff --git a/src/routes/api/stats.js b/src/routes/api/stats.js index b747a7e28..43aec3014 100644 --- a/src/routes/api/stats.js +++ b/src/routes/api/stats.js @@ -1,5 +1,6 @@ const sql = require('../../services/sql'); const becca = require('../../becca/becca'); +const NotFoundError = require("../../errors/not_found_error"); function getNoteSize(req) { const {noteId} = req.params; @@ -26,7 +27,7 @@ function getSubtreeSize(req) { const note = becca.notes[noteId]; if (!note) { - return [404, `Note ${noteId} was not found.`]; + throw new NotFoundError(`Note '${noteId}' was not found.`); } const subTreeNoteIds = note.getSubtreeNoteIds(); diff --git a/src/routes/api/tree.js b/src/routes/api/tree.js index 7874814c2..82b3ace04 100644 --- a/src/routes/api/tree.js +++ b/src/routes/api/tree.js @@ -2,6 +2,7 @@ const becca = require('../../becca/becca'); const log = require('../../services/log'); +const NotFoundError = require("../../errors/not_found_error"); function getNotesAndBranchesAndAttributes(noteIds) { noteIds = new Set(noteIds); @@ -141,7 +142,7 @@ function getTree(req) { } if (!(subTreeNoteId in becca.notes)) { - return [404, `Note ${subTreeNoteId} not found in the cache`]; + throw new NotFoundError(`Note '${subTreeNoteId}' not found in the cache`); } collect(becca.notes[subTreeNoteId]); diff --git a/src/routes/login.js b/src/routes/login.js index bf7a44226..f824931c6 100644 --- a/src/routes/login.js +++ b/src/routes/login.js @@ -6,6 +6,7 @@ const myScryptService = require('../services/my_scrypt'); const log = require('../services/log'); const passwordService = require("../services/password"); const assetPath = require("../services/asset_path"); +const ValidationError = require("../errors/validation_error"); function loginPage(req, res) { res.render('login', { @@ -23,7 +24,7 @@ function setPasswordPage(req, res) { function setPassword(req, res) { if (passwordService.isPasswordSet()) { - return [400, "Password has been already set"]; + throw new ValidationError("Password has been already set"); } let {password1, password2} = req.body; diff --git a/src/routes/routes.js b/src/routes/routes.js index 05ad3e430..ff1dde1fa 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -5,6 +5,7 @@ const loginRoute = require('./login'); const indexRoute = require('./index'); const utils = require('../services/utils'); const multer = require('multer'); +const ValidationError = require("../errors/validation_error"); // API routes const treeApiRoute = require('./api/tree'); @@ -61,6 +62,7 @@ const csurf = require('csurf'); const {createPartialContentHandler} = require("express-partial-content"); const rateLimit = require("express-rate-limit"); const AbstractEntity = require("../becca/entities/abstract_entity"); +const NotFoundError = require("../errors/not_found_error"); const csrfMiddleware = csurf({ cookie: true, @@ -169,13 +171,7 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio log.request(req, res, Date.now() - start, responseLength); }) - .catch(e => { - log.error(`${method} ${path} threw exception: ` + e.stack); - - res.setHeader("Content-Type", "text/plain") - .status(500) - .send(e.message); - }); + .catch(e => handleException(method, path, e, res)); } else { const responseLength = resultHandler(req, res, result); @@ -185,15 +181,33 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio } } catch (e) { - log.error(`${method} ${path} threw exception: ` + e.stack); - - res.setHeader("Content-Type", "text/plain") - .status(500) - .send(e.message); + handleException(method, path, e, res); } }); } +function handleException(method, path, e, res) { + log.error(`${method} ${path} threw exception: ` + e.stack); + + if (e instanceof ValidationError) { + res.setHeader("Content-Type", "application/json") + .status(400) + .send({ + message: e.message + }); + } if (e instanceof NotFoundError) { + res.setHeader("Content-Type", "application/json") + .status(404) + .send({ + message: e.message + }); + } else { + res.setHeader("Content-Type", "text/plain") + .status(500) + .send(e.message); + } +} + const MAX_ALLOWED_FILE_SIZE_MB = 250; const GET = 'get', POST = 'post', PUT = 'put', PATCH = 'patch', DELETE = 'delete'; diff --git a/src/services/builtin_attributes.js b/src/services/builtin_attributes.js index e1eb571aa..43f67cea2 100644 --- a/src/services/builtin_attributes.js +++ b/src/services/builtin_attributes.js @@ -60,6 +60,7 @@ module.exports = [ { type: 'label', name: 'template' }, { type: 'label', name: 'toc' }, { type: 'label', name: 'color' }, + { type: 'label', name: 'keepCurrentHoisting'}, // relation names { type: 'relation', name: 'internalLink' }, diff --git a/src/services/hidden_subtree.js b/src/services/hidden_subtree.js index 73bdebec3..64800471c 100644 --- a/src/services/hidden_subtree.js +++ b/src/services/hidden_subtree.js @@ -1,6 +1,6 @@ const becca = require("../becca/becca"); const noteService = require("./notes"); -const log = require("./log.js"); +const log = require("./log"); const LBTPL_ROOT = "lbTplRoot"; const LBTPL_BASE = "lbTplBase"; @@ -36,7 +36,8 @@ const HIDDEN_SUBTREE_DEFINITION = { title: 'Note Map', type: 'noteMap', attributes: [ - { type: 'label', name: 'mapRootId', value: 'hoisted' } + { type: 'label', name: 'mapRootNoteId', value: 'hoisted' }, + { type: 'label', name: 'keepCurrentHoisting' } ] }, { @@ -56,6 +57,12 @@ const HIDDEN_SUBTREE_DEFINITION = { title: 'Bulk action', type: 'doc', }, + { + // place for user scripts hidden stuff (scripts should not create notes directly under hidden root) + id: 'userHidden', + title: 'User Hidden', + type: 'text', + }, { id: LBTPL_ROOT, title: 'Launch Bar Templates', diff --git a/src/services/notes.js b/src/services/notes.js index 462e22910..aa7bc5c26 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -18,7 +18,8 @@ const Branch = require('../becca/entities/branch'); const Note = require('../becca/entities/note'); const Attribute = require('../becca/entities/attribute'); const dayjs = require("dayjs"); -const htmlSanitizer = require("./html_sanitizer.js"); +const htmlSanitizer = require("./html_sanitizer"); +const ValidationError = require("../errors/validation_error"); function getNewNotePosition(parentNoteId) { const note = becca.notes[parentNoteId]; @@ -107,15 +108,15 @@ function getAndValidateParent(params) { const parentNote = becca.notes[params.parentNoteId]; if (!parentNote) { - throw new Error(`Parent note "${params.parentNoteId}" not found.`); + throw new ValidationError(`Parent note "${params.parentNoteId}" not found.`); } - if (parentNote.type === 'launcher') { - throw new Error(`Launchers should not have child notes.`); + if (parentNote.type === 'launcher' && parentNote.noteId !== 'lbBookmarks') { + throw new ValidationError(`Creating child notes into launcher notes is not allowed.`); } - if (!params.ignoreForbiddenParents && (parentNote.isLaunchBarConfig() || parentNote.isOptions())) { - throw new Error(`Creating child notes into '${parentNote.noteId}' is not allowed.`); + if (!params.ignoreForbiddenParents && (['lbRoot', 'hidden'].includes(parentNote.noteId) || parentNote.isOptions())) { + throw new ValidationError(`Creating child notes into '${parentNote.noteId}' is not allowed.`); } return parentNote; diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.js index ad88449e2..2f72e0b1b 100644 --- a/src/services/search/services/parse.js +++ b/src/services/search/services/parse.js @@ -12,7 +12,7 @@ const PropertyComparisonExp = require('../expressions/property_comparison'); const AttributeExistsExp = require('../expressions/attribute_exists'); const LabelComparisonExp = require('../expressions/label_comparison'); const NoteFlatTextExp = require('../expressions/note_flat_text'); -const NoteContentFulltextExp = require('../expressions/note_content_fulltext.js'); +const NoteContentFulltextExp = require('../expressions/note_content_fulltext'); const OrderByAndLimitExp = require('../expressions/order_by_and_limit'); const AncestorExp = require("../expressions/ancestor"); const buildComparator = require('./build_comparator'); diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js index 42897ebc5..dee2816c6 100644 --- a/src/services/search/services/search.js +++ b/src/services/search/services/search.js @@ -10,7 +10,7 @@ const becca = require('../../../becca/becca'); const beccaService = require('../../../becca/becca_service'); const utils = require('../../utils'); const log = require('../../log'); -const scriptService = require("../../script.js"); +const scriptService = require("../../script"); function searchFromNote(note) { let searchResultNoteIds, highlightedTokens; diff --git a/src/services/special_notes.js b/src/services/special_notes.js index 5e318bd11..da272cfe0 100644 --- a/src/services/special_notes.js +++ b/src/services/special_notes.js @@ -4,7 +4,7 @@ const becca = require("../becca/becca"); const noteService = require("./notes"); const cls = require("./cls"); const dateUtils = require("./date_utils"); -const log = require("./log.js"); +const log = require("./log"); const hiddenSubtreeService = require("./hidden_subtree"); function getInboxNote(date) { diff --git a/src/services/task_context.js b/src/services/task_context.js index d59bc96b3..f86b6fd59 100644 --- a/src/services/task_context.js +++ b/src/services/task_context.js @@ -6,7 +6,7 @@ const ws = require('./ws'); const taskContexts = {}; class TaskContext { - constructor(taskId, taskType, data) { + constructor(taskId, taskType, data = null) { this.taskId = taskId; this.taskType = taskType; this.data = data; @@ -24,7 +24,7 @@ class TaskContext { } /** @returns {TaskContext} */ - static getInstance(taskId, taskType, data) { + static getInstance(taskId, taskType, data = null) { if (!taskContexts[taskId]) { taskContexts[taskId] = new TaskContext(taskId, taskType, data); } diff --git a/src/services/ws.js b/src/services/ws.js index 4581e8942..4a6d8ce90 100644 --- a/src/services/ws.js +++ b/src/services/ws.js @@ -53,6 +53,11 @@ function init(httpServer, sessionParser) { } }); }); + + webSocketServer.on('error', error => { + // https://github.com/zadam/trilium/issues/3374#issuecomment-1341053765 + console.log(error); + }); } function sendMessage(client, message) {