From cfb04f7d8a81b520f7c6496b23567829fabb0283 Mon Sep 17 00:00:00 2001 From: soulsands <407221377@qq.com> Date: Tue, 11 Apr 2023 00:16:49 +0800 Subject: [PATCH 01/21] chore: add eslint config --- .eslintignore | 9 ++ .eslintrc.js | 223 +++++++++++++++++++++++++++++++++++++++++++--- .gitignore | 1 + .husky/.gitignore | 1 + .prettierrc.js | 11 +++ package.json | 16 +++- 6 files changed, 248 insertions(+), 13 deletions(-) create mode 100644 .eslintignore create mode 100644 .husky/.gitignore create mode 100644 .prettierrc.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..c4461eed9 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,9 @@ +node_modules +dist +bin +docs +libraries +spec +spec-es6 +coverage +play diff --git a/.eslintrc.js b/.eslintrc.js index c554a263c..86b84a4cf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,16 +1,215 @@ module.exports = { - "env": { - "browser": true, - "commonjs": true, - "es2021": true, - "node": true + env: { + browser: true, + commonjs: true, + es2021: true, + node: true, }, - "extends": "eslint:recommended", - "overrides": [ + plugins: ['prettier'], + + extends: ['eslint:recommended', 'airbnb-base', 'plugin:jsonc/recommended-with-jsonc', 'prettier'], + overrides: [ + { + files: ['*.json', '*.json5', '*.jsonc'], + parser: 'jsonc-eslint-parser', + }, + { + files: ['package.json'], + parser: 'jsonc-eslint-parser', + rules: { + 'jsonc/sort-keys': [ + 'error', + { + pathPattern: '^$', + order: [ + 'name', + 'version', + 'private', + 'packageManager', + 'description', + 'type', + 'keywords', + 'homepage', + 'bugs', + 'license', + 'author', + 'contributors', + 'funding', + 'files', + 'main', + 'module', + 'exports', + 'unpkg', + 'jsdelivr', + 'browser', + 'bin', + 'man', + 'directories', + 'repository', + 'publishConfig', + 'scripts', + 'peerDependencies', + 'peerDependenciesMeta', + 'optionalDependencies', + 'dependencies', + 'devDependencies', + 'engines', + 'config', + 'overrides', + 'pnpm', + 'husky', + 'lint-staged', + 'eslintConfig', + ], + }, + { + pathPattern: '^(?:dev|peer|optional|bundled)?[Dd]ependencies$', + order: { type: 'asc' }, + }, + ], + }, + }, ], - "parserOptions": { - "ecmaVersion": "latest" + globals: { + $: true, + jQuery: true, + glob: true, + log: true, + EditorWatchdog: true, + baseApiUrl: true, + // \src\share\canvas_share.js + React: true, + appState: true, + ExcalidrawLib: true, + elements: true, + files: true, + ReactDOM: true, + // src\public\app\widgets\type_widgets\relation_map.js + jsPlumb: true, + panzoom: true, + logError: true, + // src\public\app\widgets\type_widgets\image.js + WZoom: true, + // \src\public\app\widgets\type_widgets\read_only_text.js + renderMathInElement: true, + // \src\public\app\widgets\type_widgets\editable_text.js + BalloonEditor: true, + CKEditorInspector: true, + // \src\public\app\widgets\type_widgets\editable_code.js + CodeMirror: true, + // \src\public\app\services\resizer.js + Split: true, + // \src\public\app\services\note_content_renderer.js + mermaid: true, + // src\public\app\services\frontend_script_api.js + dayjs: true, + // \src\public\app\widgets\dialogs\markdown_import.js + commonmark: true, + // \src\public\app\widgets\note_map.js + ForceGraph: true, + // \src\public\app\setup.js + ko: true, + syncInProgress: true, + // src\public\app\services\utils.js + logInfo: true, + __non_webpack_require__: true, + // }, - "rules": { - } -} + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + // hi adam, those rules are //temporary off, you can turn them on one by one to check if we really need it. (to be deleted) + + // eslint:recommended + 'no-unused-vars': 'off', + 'linebreak-style': 'off', + 'no-useless-escape': 'off', + 'no-empty': 'off', + 'no-constant-condition': 'off', + 'getter-return': 'off', + 'no-cond-assign': 'off', + 'no-async-promise-executor': 'off', + 'no-extra-semi': 'off', + 'no-inner-declarations': 'off', + + // prettier + 'prettier/prettier': ['error', { endOfLine: 'auto' }], + + // airbnb-base + 'no-console': 'off', + 'no-plusplus': 'off', + 'no-param-reassign': 'off', + 'global-require': 'off', + 'no-use-before-define': 'off', + 'no-await-in-loop': 'off', + radix: 'off', + 'import/order': 'off', + 'import/no-extraneous-dependencies': 'off', + 'prefer-destructuring': 'off', + 'no-shadow': 'off', + 'no-new': 'off', + 'no-restricted-syntax': 'off', + strict: 'off', + 'class-methods-use-this': 'off', + 'no-else-return': 'off', + 'import/no-dynamic-require': 'off', + 'no-underscore-dangle': 'off', + 'prefer-template': 'off', + 'consistent-return': 'off', + 'no-continue': 'off', + 'object-shorthand': 'off', + 'one-var': 'off', + 'prefer-const': 'off', + 'spaced-comment': 'off', + 'no-loop-func': 'off', + 'arrow-body-style': 'off', + + 'guard-for-in': 'off', + 'no-return-assign': 'off', + 'dot-notation': 'off', + + 'func-names': 'off', + 'import/no-useless-path-segments': 'off', + 'default-param-last': 'off', + 'prefer-arrow-callback': 'off', + 'no-unneeded-ternary': 'off', + 'no-return-await': 'off', + 'import/extensions': 'off', + + 'no-var': 'off', + 'import/newline-after-import': 'off', + 'no-restricted-globals': 'off', + 'operator-assignment': 'off', + 'no-eval': 'off', + 'max-classes-per-file': 'off', + 'vars-on-top': 'off', + 'no-bitwise': 'off', + 'no-lonely-if': 'off', + 'no-multi-assign': 'off', + 'no-promise-executor-return': 'off', + 'no-empty-function': 'off', + 'import/no-unresolved': 'off', + camelcase: 'off', + eqeqeq: 'off', + 'lines-between-class-members': 'off', + 'import/no-cycle': 'off', + 'new-cap': 'off', + 'prefer-object-spread': 'off', + 'no-new-func': 'off', + 'no-unused-expressions': 'off', + 'lines-around-directive': 'off', + 'prefer-exponentiation-operator': 'off', + 'no-restricted-properties': 'off', + 'prefer-rest-params': 'off', + 'no-unreachable-loop': 'off', + 'no-alert': 'off', + 'no-useless-return': 'off', + 'no-nested-ternary': 'off', + 'prefer-regex-literals': 'off', + 'import/no-named-as-default-member': 'off', + yoda: 'off', + 'no-script-url': 'off', + }, +}; diff --git a/.gitignore b/.gitignore index 01eac268c..6c7f73ae1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ server-package.json .idea/httpRequests/ data/ tmp/ +.eslintcache \ No newline at end of file diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 000000000..31354ec13 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..e2f476b94 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,11 @@ +//https://prettier.io/docs/en/options.html +module.exports = { + semi: true, + trailingComma: 'es5', + singleQuote: true, + printWidth: 120, + tabWidth: 4, + // useTabs: false, + // bracketSpacing: true, + // htmlWhitespaceSensitivity: 'ignore', +}; diff --git a/package.json b/package.json index 114689b85..3a1cbc79e 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "test-jasmine": "jasmine", "test-es6": "node -r esm spec-es6/attribute_parser.spec.js ", "test": "npm run test-jasmine && npm run test-es6", - "postinstall": "rimraf ./node_modules/canvas" + "postinstall": "rimraf ./node_modules/canvas", + "lint": "eslint . --cache", + "prepare": "husky install" }, "dependencies": { "@braintree/sanitize-url": "6.0.2", @@ -100,15 +102,27 @@ "electron-packager": "17.1.1", "electron-rebuild": "3.2.9", "eslint": "^8.38.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsonc": "^2.7.0", + "eslint-plugin-prettier": "^4.2.1", "esm": "3.2.25", + "husky": "^8.0.3", "jasmine": "4.5.0", "jsdoc": "4.0.1", + "jsonc-eslint-parser": "^2.2.0", + "lint-staged": "^13.2.1", "lorem-ipsum": "2.0.8", + "prettier": "2.8.7", "rcedit": "3.0.1", "webpack": "5.75.0", "webpack-cli": "5.0.1" }, "optionalDependencies": { "electron-installer-debian": "3.1.0" + }, + "lint-staged": { + "*.js": "eslint --cache --fix" } } From 185b206627856a7600c76cf441211e8fddfc86f8 Mon Sep 17 00:00:00 2001 From: soulsands <407221377@qq.com> Date: Tue, 11 Apr 2023 00:17:16 +0800 Subject: [PATCH 02/21] chore: husky --- .husky/pre-commit | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..36af21989 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged From d98c46a275ef41913e3783b99175a119f2240ef7 Mon Sep 17 00:00:00 2001 From: soulsands <407221377@qq.com> Date: Tue, 11 Apr 2023 00:17:37 +0800 Subject: [PATCH 03/21] chore: share editor config --- .vscode/extensions.json | 6 ++++++ .vscode/settings.json | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..b22b867bd --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..52d1921a4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "editor.formatOnSave": true, + "eslint.format.enable": true, + "eslint.probe": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "html", + "vue", + "markdown", + "json", + "jsonc" + ], + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "html", + "vue", + "markdown", + "json", + "jsonc" + ], + "files.eol": "\n", +} \ No newline at end of file From 988c5c6a25fd30c6ec3e94b683cdb98332fffa7c Mon Sep 17 00:00:00 2001 From: soulsands <407221377@qq.com> Date: Tue, 11 Apr 2023 00:37:38 +0800 Subject: [PATCH 04/21] chore: config --- .eslintrc.js | 7 +++++-- .vscode/settings.json | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 86b84a4cf..f944ca9a0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,7 +18,8 @@ module.exports = { parser: 'jsonc-eslint-parser', rules: { 'jsonc/sort-keys': [ - 'error', + // here is rule (to be deleted) + 'off', { pathPattern: '^$', order: [ @@ -135,7 +136,8 @@ module.exports = { 'no-inner-declarations': 'off', // prettier - 'prettier/prettier': ['error', { endOfLine: 'auto' }], + // all about formating, it might take a while to excute it. you can change the config in .prettierrc.js (to be deleted) + 'prettier/prettier': ['off', { endOfLine: 'auto' }], // airbnb-base 'no-console': 'off', @@ -211,5 +213,6 @@ module.exports = { 'import/no-named-as-default-member': 'off', yoda: 'off', 'no-script-url': 'off', + }, }; diff --git a/.vscode/settings.json b/.vscode/settings.json index 52d1921a4..0e4b77b5c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,9 @@ "[javascript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, + "[json]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, "editor.formatOnSave": true, "eslint.format.enable": true, "eslint.probe": [ From 5e5fe2ccf3da833d5ecc21c1f49e5460435e5025 Mon Sep 17 00:00:00 2001 From: baiyongjie <407221377@qq.com> Date: Wed, 12 Apr 2023 10:05:26 +0800 Subject: [PATCH 05/21] fix: upgrade version to support top level await --- libraries/codemirror/addon/lint/eslint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/codemirror/addon/lint/eslint.js b/libraries/codemirror/addon/lint/eslint.js index 5c310fa63..b1ab412e3 100644 --- a/libraries/codemirror/addon/lint/eslint.js +++ b/libraries/codemirror/addon/lint/eslint.js @@ -46,7 +46,7 @@ const errors = new eslint().verify(text, { root: true, parserOptions: { - ecmaVersion: 2019 + ecmaVersion: 2022 }, extends: ['eslint:recommended', 'airbnb-base'], env: { From ad8ec684439fffd8d9b437753548cd3883653042 Mon Sep 17 00:00:00 2001 From: baiyongjie <407221377@qq.com> Date: Thu, 13 Apr 2023 10:28:14 +0800 Subject: [PATCH 06/21] fix: correct active tree node after moving an clone node --- src/public/app/widgets/note_tree.js | 65 ++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js index 7837672d2..d438bba4b 100644 --- a/src/public/app/widgets/note_tree.js +++ b/src/public/app/widgets/note_tree.js @@ -223,7 +223,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { return false; }); - this.$treeSettingsPopup.on("click", e => { e.stopPropagation(); }); + this.$treeSettingsPopup.on("click", e => {e.stopPropagation();}); $(document).on('click', () => this.$treeSettingsPopup.hide()); @@ -251,12 +251,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { // code inspired by https://gist.github.com/jtsternberg/c272d7de5b967cec2d3d const isEnclosing = ($container, $sub) => { - const conOffset = $container.offset(); - const conDistanceFromTop = conOffset.top + $container.outerHeight(true); + const conOffset = $container.offset(); + const conDistanceFromTop = conOffset.top + $container.outerHeight(true); const conDistanceFromLeft = conOffset.left + $container.outerWidth(true); - const subOffset = $sub.offset(); - const subDistanceFromTop = subOffset.top + $sub.outerHeight(true); + const subOffset = $sub.offset(); + const subDistanceFromTop = subOffset.top + $sub.outerHeight(true); const subDistanceFromLeft = subOffset.left + $sub.outerWidth(true); return conDistanceFromTop > subDistanceFromTop @@ -673,7 +673,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { return noteList; } - updateNode(node) { + async updateNode(node) { const note = froca.getNoteFromCache(node.data.noteId); const branch = froca.getBranch(node.data.branchId); @@ -697,7 +697,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { node.title = utils.escapeHtml(title); if (node.isExpanded() !== branch.isExpanded) { - node.setExpanded(branch.isExpanded, {noEvents: true, noAnimation: true}); + await node.setExpanded(branch.isExpanded, {noEvents: true, noAnimation: true}); } node.renderTitle(); @@ -849,7 +849,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { await this.setExpandedStatusForSubtree(node, false); } - collapseTreeEvent() { this.collapseTree(); } + collapseTreeEvent() {this.collapseTree();} /** * @returns {FancytreeNode|null} @@ -920,7 +920,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { } if (expand) { - await parentNode.setExpanded(true, {noAnimation: true}); + if (!parentNode.isExpanded()) { + await parentNode.setExpanded(true, {noAnimation: true}); + } // although previous line should set the expanded status, it seems to happen asynchronously, // so we need to make sure it is set properly before calling updateNode which uses this flag @@ -928,7 +930,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { branch.isExpanded = true; } - this.updateNode(parentNode); + await this.updateNode(parentNode); let foundChildNode = this.findChildNode(parentNode, childNoteId); @@ -1096,10 +1098,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { const activeNode = this.getActiveNode(); const activeNodeFocused = activeNode && activeNode.hasFocus(); const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null; - const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null; + let activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null; const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null; - const activeNoteId = activeNode ? activeNode.data.noteId : null; + let activeNoteId = activeNode ? activeNode.data.noteId : null; const noteIdsToUpdate = new Set(); const noteIdsToReload = new Set(); @@ -1142,7 +1144,14 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { } } - for (const ecBranch of loadResults.getBranches()) { + // activeNode is supposed to be moved when we find out activeNode is deleted but not all branches are deleted. save it for fixing activeNodePath after all nodes loaded. + let movedActiveNode = null; + let parentsOfAddedNodes = []; + + const allBranches = loadResults.getBranches(); + const allBranchesDeleted = allBranches.every(branch => !!branch.isDeleted); + + for (const ecBranch of allBranches) { if (ecBranch.parentNoteId === '_share') { // all shared notes have a sign in the tree, even the descendants of shared notes noteIdsToReload.add(ecBranch.noteId); @@ -1155,12 +1164,16 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { for (const node of this.getNodesByBranch(ecBranch)) { if (ecBranch.isDeleted) { if (node.isActive()) { - const newActiveNode = node.getNextSibling() - || node.getPrevSibling() - || node.getParent(); + if (allBranchesDeleted) { + const newActiveNode = node.getNextSibling() + || node.getPrevSibling() + || node.getParent(); - if (newActiveNode) { - newActiveNode.setActive(true, {noEvents: true, noFocus: true}); + if (newActiveNode) { + newActiveNode.setActive(true, {noEvents: true, noFocus: true}); + } + } else { + movedActiveNode = node; } } @@ -1174,12 +1187,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { if (!ecBranch.isDeleted) { for (const parentNode of this.getNodesByNoteId(ecBranch.parentNoteId)) { + parentsOfAddedNodes.push(parentNode) + if (parentNode.isFolder() && !parentNode.isLoaded()) { continue; } const found = (parentNode.getChildren() || []).find(child => child.data.noteId === ecBranch.noteId); - if (!found) { // make sure it's loaded await froca.getNote(ecBranch.noteId); @@ -1222,7 +1236,18 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { // for some reason node update cannot be in the batchUpdate() block (node is not re-rendered) for (const noteId of noteIdsToUpdate) { for (const node of this.getNodesByNoteId(noteId)) { - this.updateNode(node); + await this.updateNode(node); + } + } + + if (movedActiveNode) { + for (const parentNode of parentsOfAddedNodes) { + const found = (parentNode.getChildren() || []).find(child => child.data.noteId === movedActiveNode.data.noteId); + if (found) { + activeNotePath = treeService.getNotePath(found); + activeNoteId = found.data.noteId; + break + } } } From ea64adc2f91111625ae78053d186682f61be914a Mon Sep 17 00:00:00 2001 From: Castor <407221377@qq.com> Date: Thu, 13 Apr 2023 17:56:28 +0800 Subject: [PATCH 07/21] chore: remove install in script switch-electron hi. No need to install since it's already in position. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eff8721e1..3e0bb275a 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "start-electron": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron --inspect=5858 .", "start-electron-no-dir": "cross-env TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 electron --inspect=5858 .", "switch-server": "rm -rf ./node_modules/better-sqlite3 && npm install", - "switch-electron": "rm -rf ./node_modules/better-sqlite3 && npm install && ./node_modules/.bin/electron-rebuild", + "switch-electron": "./node_modules/.bin/electron-rebuild", "build-backend-docs": "rm -rf ./docs/backend_api && ./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/becca/entities/*.js src/services/backend_script_api.js src/services/sql.js", "build-frontend-docs": "rm -rf ./docs/frontend_api && ./node_modules/.bin/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/right_panel_widget.js", "build-docs": "npm run build-backend-docs && npm run build-frontend-docs", From 0c08126752da7453fd520cd6aef189b9d785bd05 Mon Sep 17 00:00:00 2001 From: soulsands <407221377@qq.com> Date: Thu, 13 Apr 2023 21:16:10 +0800 Subject: [PATCH 08/21] activating after removal of subContexts conforms to mainContexts --- src/public/app/components/tab_manager.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/public/app/components/tab_manager.js b/src/public/app/components/tab_manager.js index 92e94b218..e5ca32c45 100644 --- a/src/public/app/components/tab_manager.js +++ b/src/public/app/components/tab_manager.js @@ -388,7 +388,12 @@ export default class TabManager extends Component { await this.triggerEvent('beforeNoteContextRemove', { ntxIds: ntxIdsToRemove }); if (!noteContextToRemove.isMainContext()) { - await this.activateNoteContext(noteContextToRemove.getMainContext().ntxId); + const siblings = noteContextToRemove.getMainContext().getSubContexts(); + const idx = siblings.findIndex(nc => nc.ntxId === noteContextToRemove.ntxId); + const contextToActivateIdx = idx === siblings.length - 1 ? idx - 1 : idx + 1; + const contextToActivate = siblings[contextToActivateIdx]; + + await this.activateNoteContext(contextToActivate.ntxId); } else if (this.mainNoteContexts.length <= 1) { await this.openAndActivateEmptyTab(); From e4376bb9f202d7a82c6018264d44dbb65224fb08 Mon Sep 17 00:00:00 2001 From: zadam Date: Fri, 14 Apr 2023 20:24:28 +0200 Subject: [PATCH 09/21] isRoot check --- src/public/app/services/froca_updater.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/app/services/froca_updater.js b/src/public/app/services/froca_updater.js index 070d6c2a2..76662b08a 100644 --- a/src/public/app/services/froca_updater.js +++ b/src/public/app/services/froca_updater.js @@ -141,7 +141,7 @@ async function processBranchChange(loadResults, ec) { const childNote = froca.notes[ec.entity.noteId]; let parentNote = froca.notes[ec.entity.parentNoteId]; - if (childNote && !parentNote) { + if (childNote && !childNote.isRoot() && !parentNote) { // a branch cannot exist without the parent // a note loaded into froca has to also contain all its ancestors // this problem happened e.g. in sharing where _share was hidden and thus not loaded From 7aa26580ba8b0efb2687343f5ed00ab070fb6278 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 15 Apr 2023 00:06:13 +0200 Subject: [PATCH 10/21] refactoring of "some path" WIP --- src/becca/becca_service.js | 50 +++++++--- src/becca/entities/bnote.js | 70 +++++++++++-- src/public/app/entities/fnote.js | 98 ++++++++++++------- src/public/app/services/branches.js | 4 +- src/public/app/services/note_autocomplete.js | 4 +- src/public/app/services/note_tooltip.js | 8 +- src/public/app/services/tree.js | 45 ++------- .../attribute_widgets/attribute_detail.js | 5 +- .../attribute_widgets/attribute_editor.js | 3 +- .../app/widgets/dialogs/recent_changes.js | 3 +- .../app/widgets/ribbon_widgets/note_paths.js | 2 +- src/public/app/widgets/shared_switch.js | 2 +- .../app/widgets/type_widgets/editable_text.js | 3 +- src/routes/api/cloning.js | 6 +- src/routes/routes.js | 2 +- src/services/bulk_actions.js | 2 +- src/services/cloning.js | 10 +- 17 files changed, 193 insertions(+), 124 deletions(-) diff --git a/src/becca/becca_service.js b/src/becca/becca_service.js index 80942c564..47e7d39e2 100644 --- a/src/becca/becca_service.js +++ b/src/becca/becca_service.js @@ -120,7 +120,7 @@ function getNoteTitleForPath(notePathArray) { } /** - * Returns notePath for noteId from cache. Note hoisting is respected. + * Returns notePath for noteId. Note hoisting is respected. * Archived (and hidden) notes are also returned, but non-archived paths are preferred if available * - this means that archived paths is returned only if there's no non-archived path * - you can check whether returned path is archived using isArchived @@ -136,20 +136,20 @@ function getSomePath(note, path = []) { /** * @param {BNote} note - * @param {string[]} path - * @param {boolean}respectHoisting + * @param {string[]} parentPath + * @param {boolean} respectHoisting * @returns {string[]|false} */ -function getSomePathInner(note, path, respectHoisting) { +function getSomePathInner(note, parentPath, respectHoisting) { + const childPath = [...parentPath, note.noteId]; if (note.isRoot()) { - const foundPath = [...path, note.noteId]; - foundPath.reverse(); + childPath.reverse(); - if (respectHoisting && !foundPath.includes(cls.getHoistedNoteId())) { + if (respectHoisting && !childPath.includes(cls.getHoistedNoteId())) { return false; } - return foundPath; + return childPath; } const parents = note.parents; @@ -159,15 +159,35 @@ function getSomePathInner(note, path, respectHoisting) { return false; } - for (const parentNote of parents) { - const retPath = getSomePathInner(parentNote, [...path, note.noteId], respectHoisting); + const completeNotePaths = parents.map(parentNote => getSomePathInner(parentNote, childPath, respectHoisting)); - if (retPath) { - return retPath; - } + if (completeNotePaths.length === 0) { + return false; + } else if (completeNotePaths.length === 1) { + return completeNotePaths[0]; + } else { + completeNotePaths.sort((a, b) => { + if (a.isInHoistedSubTree !== b.isInHoistedSubTree) { + return a.isInHoistedSubTree ? -1 : 1; + } else if (a.isSearch !== b.isSearch) { + return a.isSearch ? 1 : -1; + } else if (a.isArchived !== b.isArchived) { + return a.isArchived ? 1 : -1; + } else if (a.isHidden !== b.isHidden) { + return a.isHidden ? 1 : -1; + } else { + return a.notePath.length - b.notePath.length; + } + }); + + // if there are multiple valid paths, take the shortest one + const shortestNotePath = completeNotePaths.reduce((shortestPath, nextPath) => + nextPath.length < shortestPath.length + ? nextPath + : shortestPath, completeNotePaths[0]); + + return shortestNotePath; } - - return false; } function getNotePath(noteId) { diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 2e9febadb..625ba1f4b 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -12,6 +12,7 @@ const TaskContext = require("../../services/task_context"); const dayjs = require("dayjs"); const utc = require('dayjs/plugin/utc'); const eventService = require("../../services/events"); +const froca = require("../../public/app/services/froca.js"); dayjs.extend(utc); const LABEL = 'label'; @@ -1150,6 +1151,8 @@ class BNote extends AbstractBeccaEntity { } /** + * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles) + * * @returns {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path) */ getAllNotePaths() { @@ -1157,18 +1160,73 @@ class BNote extends AbstractBeccaEntity { return [['root']]; } - const notePaths = []; + const parentNotes = this.getParentNotes(); + let notePaths = []; - for (const parentNote of this.getParentNotes()) { - for (const parentPath of parentNote.getAllNotePaths()) { - parentPath.push(this.noteId); - notePaths.push(parentPath); - } + if (parentNotes.length === 1) { // optimization for most common case + notePaths = parentNotes[0].getAllNotePaths(); + } else { + notePaths = parentNotes.flatMap(parentNote => parentNote.getAllNotePaths()); + } + + for (const notePath of notePaths) { + notePath.push(this.noteId); } return notePaths; } + /** + * @param {string} [hoistedNoteId='root'] + * @return {{isArchived: boolean, isInHoistedSubTree: boolean, notePath: string[], isHidden: boolean}[]} + */ + getSortedNotePathRecords(hoistedNoteId = 'root') { + const isHoistedRoot = hoistedNoteId === 'root'; + + const notePaths = this.getAllNotePaths().map(path => ({ + notePath: path, + isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId), + isArchived: path.some(noteId => froca.notes[noteId].isArchived), + isHidden: path.includes('_hidden') + })); + + notePaths.sort((a, b) => { + if (a.isInHoistedSubTree !== b.isInHoistedSubTree) { + return a.isInHoistedSubTree ? -1 : 1; + } else if (a.isArchived !== b.isArchived) { + return a.isArchived ? 1 : -1; + } else if (a.isHidden !== b.isHidden) { + return a.isHidden ? 1 : -1; + } else { + return a.notePath.length - b.notePath.length; + } + }); + + return notePaths; + } + + /** + * Returns note path considered to be the "best" + * + * @param {string} [hoistedNoteId='root'] + * @return {string[]} array of noteIds constituting the particular note path + */ + getBestNotePath(hoistedNoteId = 'root') { + return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath; + } + + /** + * Returns note path considered to be the "best" + * + * @param {string} [hoistedNoteId='root'] + * @return {string} serialized note path (e.g. 'root/a1h315/js725h') + */ + getBestNotePathString(hoistedNoteId = 'root') { + const notePath = this.getBestNotePath(hoistedNoteId); + + return notePath?.join("/"); + } + /** * @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree */ diff --git a/src/public/app/entities/fnote.js b/src/public/app/entities/fnote.js index 1066a9341..ad442e999 100644 --- a/src/public/app/entities/fnote.js +++ b/src/public/app/entities/fnote.js @@ -247,6 +247,11 @@ class FNote { return this.__filterAttrs(this.__getCachedAttributes([]), type, name); } + /** + * @param {string[]} path + * @return {FAttribute[]} + * @private + */ __getCachedAttributes(path) { // notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates // when template instance is a parent of template itself @@ -299,63 +304,49 @@ class FNote { return this.noteId === 'root'; } - getAllNotePaths(encounteredNoteIds = null) { + /** + * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles) + * + * @returns {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path) + */ + getAllNotePaths() { if (this.noteId === 'root') { return [['root']]; } - if (!encounteredNoteIds) { - encounteredNoteIds = new Set(); - } - - encounteredNoteIds.add(this.noteId); - const parentNotes = this.getParentNotes(); - let paths; + let notePaths = []; - if (parentNotes.length === 1) { // optimization for the most common case - if (encounteredNoteIds.has(parentNotes[0].noteId)) { - return []; - } - else { - paths = parentNotes[0].getAllNotePaths(encounteredNoteIds); - } - } - else { - paths = []; - - for (const parentNote of parentNotes) { - if (encounteredNoteIds.has(parentNote.noteId)) { - continue; - } - - const newSet = new Set(encounteredNoteIds); - - paths.push(...parentNote.getAllNotePaths(newSet)); - } + if (parentNotes.length === 1) { // optimization for most common case + notePaths = parentNotes[0].getAllNotePaths(); + } else { + notePaths = parentNotes.flatMap(parentNote => parentNote.getAllNotePaths()); } - for (const path of paths) { - path.push(this.noteId); + for (const notePath of notePaths) { + notePath.push(this.noteId); } - return paths; + return notePaths; } - getSortedNotePaths(hoistedNotePath = 'root') { + /** + * @param {string} [hoistedNoteId='root'] + * @return {{isArchived: boolean, isInHoistedSubTree: boolean, notePath: string[], isHidden: boolean}[]} + */ + getSortedNotePathRecords(hoistedNoteId = 'root') { + const isHoistedRoot = hoistedNoteId === 'root'; + const notePaths = this.getAllNotePaths().map(path => ({ notePath: path, - isInHoistedSubTree: path.includes(hoistedNotePath), - isArchived: path.find(noteId => froca.notes[noteId].isArchived), - isSearch: path.find(noteId => froca.notes[noteId].type === 'search'), + isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId), + isArchived: path.some(noteId => froca.notes[noteId].isArchived), isHidden: path.includes('_hidden') })); notePaths.sort((a, b) => { if (a.isInHoistedSubTree !== b.isInHoistedSubTree) { return a.isInHoistedSubTree ? -1 : 1; - } else if (a.isSearch !== b.isSearch) { - return a.isSearch ? 1 : -1; } else if (a.isArchived !== b.isArchived) { return a.isArchived ? 1 : -1; } else if (a.isHidden !== b.isHidden) { @@ -368,6 +359,28 @@ class FNote { return notePaths; } + /** + * Returns note path considered to be the "best" + * + * @param {string} [hoistedNoteId='root'] + * @return {string[]} array of noteIds constituting the particular note path + */ + getBestNotePath(hoistedNoteId = 'root') { + return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath; + } + + /** + * Returns note path considered to be the "best" + * + * @param {string} [hoistedNoteId='root'] + * @return {string} serialized note path (e.g. 'root/a1h315/js725h') + */ + getBestNotePathString(hoistedNoteId = 'root') { + const notePath = this.getBestNotePath(hoistedNoteId); + + return notePath?.join("/"); + } + /** * @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree */ @@ -391,6 +404,13 @@ class FNote { return true; } + /** + * @param {FAttribute[]} attributes + * @param {string} type + * @param {string} name + * @return {FAttribute[]} + * @private + */ __filterAttrs(attributes, type, name) { this.__validateTypeName(type, name); @@ -527,7 +547,9 @@ class FNote { * @returns {boolean} true if note has an attribute with given type and name (including inherited) */ hasAttribute(type, name) { - return !!this.getAttribute(type, name); + const attributes = this.getAttributes(); + + return attributes.some(attr => attr.name === name && attr.type === type); } /** diff --git a/src/public/app/services/branches.js b/src/public/app/services/branches.js index c414cf257..b0cf129a2 100644 --- a/src/public/app/services/branches.js +++ b/src/public/app/services/branches.js @@ -227,7 +227,7 @@ async function cloneNoteToBranch(childNoteId, parentBranchId, prefix) { } } -async function cloneNoteToNote(childNoteId, parentNoteId, prefix) { +async function cloneNoteToParentNote(childNoteId, parentNoteId, prefix) { const resp = await server.put(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, { prefix: prefix }); @@ -254,5 +254,5 @@ export default { moveNodeUpInHierarchy, cloneNoteAfter, cloneNoteToBranch, - cloneNoteToNote, + cloneNoteToParentNote, }; diff --git a/src/public/app/services/note_autocomplete.js b/src/public/app/services/note_autocomplete.js index 07c75d447..c548d6f0f 100644 --- a/src/public/app/services/note_autocomplete.js +++ b/src/public/app/services/note_autocomplete.js @@ -2,7 +2,6 @@ import server from "./server.js"; import appContext from "../components/app_context.js"; import utils from './utils.js'; import noteCreateService from './note_create.js'; -import treeService from './tree.js'; import froca from "./froca.js"; // this key needs to have this value, so it's hit by the tooltip @@ -188,7 +187,8 @@ function initNoteAutocomplete($el, options) { templateNoteId: templateNoteId }); - suggestion.notePath = treeService.getSomeNotePath(note); + const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId; + suggestion.notePath = note.getBestNotePathString(hoistedNoteId); } $el.setSelectedNotePath(suggestion.notePath); diff --git a/src/public/app/services/note_tooltip.js b/src/public/app/services/note_tooltip.js index b83296cd8..ceff12b49 100644 --- a/src/public/app/services/note_tooltip.js +++ b/src/public/app/services/note_tooltip.js @@ -4,6 +4,7 @@ import froca from "./froca.js"; import utils from "./utils.js"; import attributeRenderer from "./attribute_renderer.js"; import noteContentRenderer from "./note_content_renderer.js"; +import appContext from "../components/app_context.js"; function setupGlobalTooltip() { $(document).on("mouseenter", "a", mouseEnterHandler); @@ -83,13 +84,14 @@ async function renderTooltip(note) { return '
Note has been deleted.
'; } - const someNotePath = treeService.getSomeNotePath(note); + const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId; + const bestNotePath = note.getBestNotePathString(hoistedNoteId); - if (!someNotePath) { + if (!bestNotePath) { return; } - let content = `
${(await treeService.getNoteTitleWithPathAsSuffix(someNotePath)).prop('outerHTML')}
`; + let content = `
${(await treeService.getNoteTitleWithPathAsSuffix(bestNotePath)).prop('outerHTML')}
`; const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note); diff --git a/src/public/app/services/tree.js b/src/public/app/services/tree.js index 3c4925f3e..88f13cdde 100644 --- a/src/public/app/services/tree.js +++ b/src/public/app/services/tree.js @@ -79,14 +79,10 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr You can ignore this message as it is mostly harmless.`); } - const someNotePath = getSomeNotePath(child, hoistedNoteId); + const bestNotePath = child.getBestNotePath(hoistedNoteId); - if (someNotePath) { // in case it's root the path may be empty - const pathToRoot = someNotePath.split("/").reverse().slice(1); - - if (!pathToRoot.includes("root")) { - pathToRoot.push('root'); - } + if (bestNotePath) { + const pathToRoot = bestNotePath.reverse().slice(1); for (const noteId of pathToRoot) { effectivePathSegments.push(noteId); @@ -109,31 +105,17 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr else { const note = await froca.getNote(getNoteIdFromNotePath(notePath)); - const someNotePathSegments = getSomeNotePathSegments(note, hoistedNoteId); + const bestNotePath = note.getBestNotePath(hoistedNoteId); - if (!someNotePathSegments) { - throw new Error(`Did not find any path segments for ${note.toString()}, hoisted note ${hoistedNoteId}`); + if (!bestNotePath) { + throw new Error(`Did not find any path segments for '${note.toString()}', hoisted note '${hoistedNoteId}'`); } // if there isn't actually any note path with hoisted note then return the original resolved note path - return someNotePathSegments.includes(hoistedNoteId) ? someNotePathSegments : effectivePathSegments; + return bestNotePath.includes(hoistedNoteId) ? bestNotePath : effectivePathSegments; } } -function getSomeNotePathSegments(note, hoistedNotePath = 'root') { - utils.assertArguments(note); - - const notePaths = note.getSortedNotePaths(hoistedNotePath); - - return notePaths.length > 0 ? notePaths[0].notePath : null; -} - -function getSomeNotePath(note, hoistedNotePath = 'root') { - const notePath = getSomeNotePathSegments(note, hoistedNotePath); - - return notePath === null ? null : notePath.join('/'); -} - ws.subscribeToMessages(message => { if (message.type === 'openNote') { appContext.tabManager.activateOrOpenNote(message.noteId); @@ -311,16 +293,6 @@ function isNotePathInAddress() { || (notePath === '' && !!ntxId); } -function parseNotePath(notePath) { - let noteIds = notePath.split('/'); - - if (noteIds[0] !== 'root') { - noteIds = ['root'].concat(noteIds); - } - - return noteIds; -} - function isNotePathInHiddenSubtree(notePath) { return notePath?.includes("root/_hidden"); } @@ -328,8 +300,6 @@ function isNotePathInHiddenSubtree(notePath) { export default { resolveNotePath, resolveNotePathToSegments, - getSomeNotePath, - getSomeNotePathSegments, getParentProtectedStatus, getNotePath, getNoteIdFromNotePath, @@ -340,6 +310,5 @@ export default { getNoteTitleWithPathAsSuffix, getHashValueFromAddress, isNotePathInAddress, - parseNotePath, isNotePathInHiddenSubtree }; diff --git a/src/public/app/widgets/attribute_widgets/attribute_detail.js b/src/public/app/widgets/attribute_widgets/attribute_detail.js index 80efc31ba..dba64da70 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_detail.js +++ b/src/public/app/widgets/attribute_widgets/attribute_detail.js @@ -1,6 +1,5 @@ import server from "../../services/server.js"; import froca from "../../services/froca.js"; -import treeService from "../../services/tree.js"; import linkService from "../../services/link.js"; import attributeAutocompleteService from "../../services/attribute_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js"; @@ -9,6 +8,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js"; import SpacedUpdate from "../../services/spaced_update.js"; import utils from "../../services/utils.js"; import shortcutService from "../../services/shortcuts.js"; +import appContext from "../../components/app_context.js"; const TPL = `
@@ -598,9 +598,10 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { const displayedResults = results.length <= DISPLAYED_NOTES ? results : results.slice(0, DISPLAYED_NOTES); const displayedNotes = await froca.getNotes(displayedResults.map(res => res.noteId)); + const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId; for (const note of displayedNotes) { - const notePath = treeService.getSomeNotePath(note); + const notePath = note.getBestNotePathString(hoistedNoteId); const $noteLink = await linkService.createNoteLink(notePath, {showNotePath: true}); this.$relatedNotesList.append( diff --git a/src/public/app/widgets/attribute_widgets/attribute_editor.js b/src/public/app/widgets/attribute_widgets/attribute_editor.js index 217717f05..88d3e837a 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_editor.js +++ b/src/public/app/widgets/attribute_widgets/attribute_editor.js @@ -7,7 +7,6 @@ import libraryLoader from "../../services/library_loader.js"; import froca from "../../services/froca.js"; import attributeRenderer from "../../services/attribute_renderer.js"; import noteCreateService from "../../services/note_create.js"; -import treeService from "../../services/tree.js"; import attributeService from "../../services/attributes.js"; const HELP_TEXT = ` @@ -503,7 +502,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget { title: title }); - return treeService.getSomeNotePath(note); + return note.getBestNotePathString(); } async updateAttributeList(attributes) { diff --git a/src/public/app/widgets/dialogs/recent_changes.js b/src/public/app/widgets/dialogs/recent_changes.js index 75b477276..21d38d016 100644 --- a/src/public/app/widgets/dialogs/recent_changes.js +++ b/src/public/app/widgets/dialogs/recent_changes.js @@ -1,7 +1,6 @@ import linkService from '../../services/link.js'; import utils from '../../services/utils.js'; import server from '../../services/server.js'; -import treeService from "../../services/tree.js"; import froca from "../../services/froca.js"; import appContext from "../../components/app_context.js"; import hoistedNoteService from "../../services/hoisted_note.js"; @@ -108,7 +107,7 @@ export default class RecentChangesDialog extends BasicWidget { } } else { const note = await froca.getNote(change.noteId); - const notePath = treeService.getSomeNotePath(note); + const notePath = note.getBestNotePathString(); if (notePath) { $noteLink = await linkService.createNoteLink(notePath, { diff --git a/src/public/app/widgets/ribbon_widgets/note_paths.js b/src/public/app/widgets/ribbon_widgets/note_paths.js index 3270e0eb4..7a6977b17 100644 --- a/src/public/app/widgets/ribbon_widgets/note_paths.js +++ b/src/public/app/widgets/ribbon_widgets/note_paths.js @@ -72,7 +72,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget { return; } - const sortedNotePaths = this.note.getSortedNotePaths(this.hoistedNoteId) + const sortedNotePaths = this.note.getSortedNotePathRecords(this.hoistedNoteId) .filter(notePath => !notePath.isHidden); if (sortedNotePaths.length > 0) { diff --git a/src/public/app/widgets/shared_switch.js b/src/public/app/widgets/shared_switch.js index 61dd140db..a7aaa1e52 100644 --- a/src/public/app/widgets/shared_switch.js +++ b/src/public/app/widgets/shared_switch.js @@ -25,7 +25,7 @@ export default class SharedSwitchWidget extends SwitchWidget { } async switchOn() { - await branchService.cloneNoteToNote(this.noteId, '_share'); + await branchService.cloneNoteToParentNote(this.noteId, '_share'); syncService.syncNow(true); } diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js index aa1015242..2c6cf55f2 100644 --- a/src/public/app/widgets/type_widgets/editable_text.js +++ b/src/public/app/widgets/type_widgets/editable_text.js @@ -4,7 +4,6 @@ import mimeTypesService from '../../services/mime_types.js'; import utils from "../../services/utils.js"; import keyboardActionService from "../../services/keyboard_actions.js"; import froca from "../../services/froca.js"; -import treeService from "../../services/tree.js"; import noteCreateService from "../../services/note_create.js"; import AbstractTextTypeWidget from "./abstract_text_type_widget.js"; import link from "../../services/link.js"; @@ -378,7 +377,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { return; } - return treeService.getSomeNotePath(resp.note); + return resp.note.getBestNotePathString(); } async refreshIncludedNoteEvent({noteId}) { diff --git a/src/routes/api/cloning.js b/src/routes/api/cloning.js index da557715c..75a42e675 100644 --- a/src/routes/api/cloning.js +++ b/src/routes/api/cloning.js @@ -9,11 +9,11 @@ function cloneNoteToBranch(req) { return cloningService.cloneNoteToBranch(noteId, parentBranchId, prefix); } -function cloneNoteToNote(req) { +function cloneNoteToParentNote(req) { const {noteId, parentNoteId} = req.params; const {prefix} = req.body; - return cloningService.cloneNoteToNote(noteId, parentNoteId, prefix); + return cloningService.cloneNoteToParentNote(noteId, parentNoteId, prefix); } function cloneNoteAfter(req) { @@ -30,7 +30,7 @@ function toggleNoteInParent(req) { module.exports = { cloneNoteToBranch, - cloneNoteToNote, + cloneNoteToParentNote, cloneNoteAfter, toggleNoteInParent }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 9cc87bb37..a52a067f5 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -143,7 +143,7 @@ function register(app) { apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch); apiRoute(PUT, '/api/notes/:noteId/toggle-in-parent/:parentNoteId/:present', cloningApiRoute.toggleNoteInParent); - apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote); + apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToParentNote); apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter); route(GET, '/api/notes/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch); diff --git a/src/services/bulk_actions.js b/src/services/bulk_actions.js index eaa57a1fb..c51ba1d5e 100644 --- a/src/services/bulk_actions.js +++ b/src/services/bulk_actions.js @@ -83,7 +83,7 @@ const ACTION_HANDLERS = { let res; if (note.getParentBranches().length > 1) { - res = cloningService.cloneNoteToNote(note.noteId, action.targetParentNoteId); + res = cloningService.cloneNoteToParentNote(note.noteId, action.targetParentNoteId); } else { res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId); diff --git a/src/services/cloning.js b/src/services/cloning.js index dad3d3906..b08b3d6b3 100644 --- a/src/services/cloning.js +++ b/src/services/cloning.js @@ -8,7 +8,7 @@ const becca = require("../becca/becca"); const beccaService = require("../becca/becca_service"); const log = require("./log"); -function cloneNoteToNote(noteId, parentNoteId, prefix) { +function cloneNoteToParentNote(noteId, parentNoteId, prefix) { const parentNote = becca.getNote(parentNoteId); if (parentNote.type === 'search') { @@ -19,7 +19,7 @@ function cloneNoteToNote(noteId, parentNoteId, prefix) { } if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) { - return { success: false, message: 'Note is deleted.' }; + return { success: false, message: 'Note cannot be cloned because either the cloned note or the intended parent is deleted.' }; } const validationResult = treeService.validateParentChild(parentNoteId, noteId); @@ -35,7 +35,7 @@ function cloneNoteToNote(noteId, parentNoteId, prefix) { isExpanded: 0 }).save(); - log.info(`Cloned note '${noteId}' to new parent note '${parentNoteId}' with prefix '${prefix}'`); + log.info(`Cloned note '${noteId}' to a new parent note '${parentNoteId}' with prefix '${prefix}'`); return { success: true, @@ -51,7 +51,7 @@ function cloneNoteToBranch(noteId, parentBranchId, prefix) { return { success: false, message: `Parent branch ${parentBranchId} does not exist.` }; } - const ret = cloneNoteToNote(noteId, parentBranch.noteId, prefix); + const ret = cloneNoteToParentNote(noteId, parentBranch.noteId, prefix); parentBranch.isExpanded = true; // the new target should be expanded, so it immediately shows up to the user parentBranch.save(); @@ -182,7 +182,7 @@ function isNoteDeleted(noteId) { module.exports = { cloneNoteToBranch, - cloneNoteToNote, + cloneNoteToParentNote, ensureNoteIsPresentInParent, ensureNoteIsAbsentFromParent, toggleNoteInParent, From 65d2389b2ec3ef4b7da0dc70758ac2c1ab2c8c5a Mon Sep 17 00:00:00 2001 From: baiyongjie <407221377@qq.com> Date: Sat, 15 Apr 2023 09:57:50 +0800 Subject: [PATCH 11/21] update eslint config & fix lint --- .eslintignore | 2 -- .eslintrc.js | 9 ++------- .husky/pre-commit | 2 +- docker_healthcheck.js | 1 - src/public/app/widgets/dialogs/note_revisions.js | 2 +- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.eslintignore b/.eslintignore index c4461eed9..cf778bb2d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,7 +3,5 @@ dist bin docs libraries -spec -spec-es6 coverage play diff --git a/.eslintrc.js b/.eslintrc.js index f944ca9a0..9ed9faf1f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,8 +5,7 @@ module.exports = { es2021: true, node: true, }, - plugins: ['prettier'], - + // plugins: ['prettier'], // to be activated extends: ['eslint:recommended', 'airbnb-base', 'plugin:jsonc/recommended-with-jsonc', 'prettier'], overrides: [ { @@ -18,7 +17,6 @@ module.exports = { parser: 'jsonc-eslint-parser', rules: { 'jsonc/sort-keys': [ - // here is rule (to be deleted) 'off', { pathPattern: '^$', @@ -121,8 +119,6 @@ module.exports = { sourceType: 'module', }, rules: { - // hi adam, those rules are //temporary off, you can turn them on one by one to check if we really need it. (to be deleted) - // eslint:recommended 'no-unused-vars': 'off', 'linebreak-style': 'off', @@ -136,7 +132,6 @@ module.exports = { 'no-inner-declarations': 'off', // prettier - // all about formating, it might take a while to excute it. you can change the config in .prettierrc.js (to be deleted) 'prettier/prettier': ['off', { endOfLine: 'auto' }], // airbnb-base @@ -213,6 +208,6 @@ module.exports = { 'import/no-named-as-default-member': 'off', yoda: 'off', 'no-script-url': 'off', - + 'no-prototype-builtins':'off' }, }; diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af21989..d5b5fd41c 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx lint-staged +#npx lint-staged diff --git a/docker_healthcheck.js b/docker_healthcheck.js index 2602cd706..9213c401f 100755 --- a/docker_healthcheck.js +++ b/docker_healthcheck.js @@ -8,7 +8,6 @@ if (config.https) { // built-in TLS (terminated by trilium) is not supported yet, PRs are welcome // for reverse proxy terminated TLS this will works since config.https will be false process.exit(0); - return; } const port = require('./src/services/port'); diff --git a/src/public/app/widgets/dialogs/note_revisions.js b/src/public/app/widgets/dialogs/note_revisions.js index 9eb224891..ce768265d 100644 --- a/src/public/app/widgets/dialogs/note_revisions.js +++ b/src/public/app/widgets/dialogs/note_revisions.js @@ -242,7 +242,7 @@ export default class NoteRevisionsDialog extends BasicWidget { if (this.$content.find('span.math-tex').length > 0) { await libraryLoader.requireLibrary(libraryLoader.KATEX); - renderMathInElement($content[0], {trust: true}); + renderMathInElement(this.$content[0], {trust: true}); } } else if (revisionItem.type === 'code' || revisionItem.type === 'mermaid') { this.$content.html($("
").text(fullNoteRevision.content));

From 25df8f9c5230a2127251a218c5f5a48c88f5abd7 Mon Sep 17 00:00:00 2001
From: baiyongjie <407221377@qq.com>
Date: Sat, 15 Apr 2023 11:01:24 +0800
Subject: [PATCH 12/21] change match-hightlighter config

---
 src/public/app/widgets/type_widgets/editable_code.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/public/app/widgets/type_widgets/editable_code.js b/src/public/app/widgets/type_widgets/editable_code.js
index abb55b36b..a183601ce 100644
--- a/src/public/app/widgets/type_widgets/editable_code.js
+++ b/src/public/app/widgets/type_widgets/editable_code.js
@@ -53,7 +53,7 @@ export default class EditableCodeTypeWidget extends TypeWidget {
             matchBrackets: true,
             keyMap: options.is('vimKeymapEnabled') ? "vim": "default",
             matchTags: {bothTags: true},
-            highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
+            highlightSelectionMatches: {showToken: false, annotateScrollbar: false},
             lint: true,
             gutters: ["CodeMirror-lint-markers"],
             lineNumbers: true,
@@ -62,7 +62,7 @@ export default class EditableCodeTypeWidget extends TypeWidget {
             // all the way to the bottom of the note. With line wrap there's no horizontal scrollbar so no problem
             lineWrapping: options.is('codeLineWrapEnabled'),
             dragDrop: false, // with true the editor inlines dropped files which is not what we expect
-            placeholder: "Type the content of your code note here..."
+            placeholder: "Type the content of your code note here...",
         });
 
         this.codeEditor.on('change', () => this.spacedUpdate.scheduleUpdate());

From 30a8c6b5791c13324e3c96a752d25f0e417de66d Mon Sep 17 00:00:00 2001
From: baiyongjie <407221377@qq.com>
Date: Sat, 15 Apr 2023 17:39:26 +0800
Subject: [PATCH 13/21] perf: building improvement

---
 package.json                                  |  2 +-
 webpack-desktop.config.js                     | 16 ----------------
 webpack-setup.config.js                       | 16 ----------------
 webpack-mobile.config.js => webpack.config.js |  6 ++++--
 4 files changed, 5 insertions(+), 35 deletions(-)
 delete mode 100644 webpack-desktop.config.js
 delete mode 100644 webpack-setup.config.js
 rename webpack-mobile.config.js => webpack.config.js (69%)

diff --git a/package.json b/package.json
index eff8721e1..3090803fe 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
     "build-backend-docs": "rm -rf ./docs/backend_api && ./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/becca/entities/*.js src/services/backend_script_api.js src/services/sql.js",
     "build-frontend-docs": "rm -rf ./docs/frontend_api && ./node_modules/.bin/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/right_panel_widget.js",
     "build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
-    "webpack": "npx webpack -c webpack-desktop.config.js && npx webpack -c webpack-mobile.config.js && npx webpack -c webpack-setup.config.js",
+    "webpack": "webpack -c webpack.config.js",
     "test-jasmine": "jasmine",
     "test-es6": "node -r esm spec-es6/attribute_parser.spec.js ",
     "test": "npm run test-jasmine && npm run test-es6",
diff --git a/webpack-desktop.config.js b/webpack-desktop.config.js
deleted file mode 100644
index 8fbf0e152..000000000
--- a/webpack-desktop.config.js
+++ /dev/null
@@ -1,16 +0,0 @@
-const path = require('path');
-const assetPath = require('./src/services/asset_path');
-
-module.exports = {
-    mode: 'production',
-    entry: {
-        mobile: './src/public/app/desktop.js',
-    },
-    output: {
-        publicPath: `${assetPath}/app-dist/`,
-        path: path.resolve(__dirname, 'src/public/app-dist'),
-        filename: 'desktop.js'
-    },
-    devtool: 'source-map',
-    target: 'electron-renderer'
-};
diff --git a/webpack-setup.config.js b/webpack-setup.config.js
deleted file mode 100644
index dee04f090..000000000
--- a/webpack-setup.config.js
+++ /dev/null
@@ -1,16 +0,0 @@
-const path = require('path');
-const assetPath = require('./src/services/asset_path');
-
-module.exports = {
-    mode: 'production',
-    entry: {
-        mobile: './src/public/app/setup.js',
-    },
-    output: {
-        publicPath: `${assetPath}/app-dist/`,
-        path: path.resolve(__dirname, 'src/public/app-dist'),
-        filename: 'setup.js'
-    },
-    devtool: 'source-map',
-    target: 'electron-renderer'
-};
diff --git a/webpack-mobile.config.js b/webpack.config.js
similarity index 69%
rename from webpack-mobile.config.js
rename to webpack.config.js
index 4fc72b8be..41077c00e 100644
--- a/webpack-mobile.config.js
+++ b/webpack.config.js
@@ -4,13 +4,15 @@ const assetPath = require('./src/services/asset_path');
 module.exports = {
     mode: 'production',
     entry: {
+        setup: './src/public/app/setup.js',
         mobile: './src/public/app/mobile.js',
+        desktop: './src/public/app/desktop.js',
     },
     output: {
         publicPath: `${assetPath}/app-dist/`,
         path: path.resolve(__dirname, 'src/public/app-dist'),
-        filename: 'mobile.js'
+        filename: '[name].js',
     },
     devtool: 'source-map',
-    target: 'electron-renderer'
+    target: 'electron-renderer',
 };

From ead4242735512b46457161430363c3af060d7614 Mon Sep 17 00:00:00 2001
From: baiyongjie <407221377@qq.com>
Date: Sat, 15 Apr 2023 17:31:55 +0800
Subject: [PATCH 14/21] chore: dev auto reload

---
 nodemon.json       | 13 +++++++++++++
 package.json       |  5 +++--
 src/services/ws.js | 12 ++++++++++++
 3 files changed, 28 insertions(+), 2 deletions(-)
 create mode 100644 nodemon.json

diff --git a/nodemon.json b/nodemon.json
new file mode 100644
index 000000000..df14c4a84
--- /dev/null
+++ b/nodemon.json
@@ -0,0 +1,13 @@
+{
+    "restartable": "rs",
+    "ignore": [".git", "node_modules/**/node_modules", "src/public/"],
+    "verbose": false,
+    "execMap": {
+        "js": "node --harmony"
+    },
+    "watch": ["src/"],
+    "env": {
+        "NODE_ENV": "development"
+    },
+    "ext": "js,json"
+}
diff --git a/package.json b/package.json
index eff8721e1..b02a438a6 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,8 @@
     "url": "https://github.com/zadam/trilium.git"
   },
   "scripts": {
-    "start-server": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 node ./src/www",
-    "start-server-no-dir": "cross-env TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 node ./src/www",
+    "start-server": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon ./src/www",
+    "start-server-no-dir": "cross-env TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon ./src/www",
     "start-electron": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron --inspect=5858 .",
     "start-electron-no-dir": "cross-env TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 electron --inspect=5858 .",
     "switch-server": "rm -rf ./node_modules/better-sqlite3 && npm install",
@@ -104,6 +104,7 @@
     "jasmine": "4.6.0",
     "jsdoc": "4.0.2",
     "lorem-ipsum": "2.0.8",
+    "nodemon": "^2.0.22",
     "rcedit": "3.0.1",
     "webpack": "5.78.0",
     "webpack-cli": "5.0.1"
diff --git a/src/services/ws.js b/src/services/ws.js
index ac81c367d..5702f25d4 100644
--- a/src/services/ws.js
+++ b/src/services/ws.js
@@ -9,6 +9,18 @@ const protectedSessionService = require('./protected_session');
 const becca = require("../becca/becca");
 const AbstractBeccaEntity = require("../becca/entities/abstract_becca_entity");
 
+const env = require('./env');
+if (env.isDev()) {
+    const chokidar = require('chokidar');
+    const debounce = require('debounce');
+    const debounceReloadFronted = debounce(reloadFrontend, 200);
+    chokidar
+        .watch('src/public')
+        .on('add', debounceReloadFronted)
+        .on('change', debounceReloadFronted)
+        .on('unlink', debounceReloadFronted);
+}
+
 let webSocketServer;
 let lastSyncedPush = null;
 

From d2d2a28885776886668bba40930cfd029affcf11 Mon Sep 17 00:00:00 2001
From: Castor <407221377@qq.com>
Date: Sat, 15 Apr 2023 19:42:09 +0800
Subject: [PATCH 15/21] Update ws.js

---
 src/services/ws.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/services/ws.js b/src/services/ws.js
index 5702f25d4..f019691c3 100644
--- a/src/services/ws.js
+++ b/src/services/ws.js
@@ -13,12 +13,12 @@ const env = require('./env');
 if (env.isDev()) {
     const chokidar = require('chokidar');
     const debounce = require('debounce');
-    const debounceReloadFronted = debounce(reloadFrontend, 200);
+    const debouncedReloadFrontend = debounce(reloadFrontend, 200);
     chokidar
         .watch('src/public')
-        .on('add', debounceReloadFronted)
-        .on('change', debounceReloadFronted)
-        .on('unlink', debounceReloadFronted);
+        .on('add', debouncedReloadFrontend)
+        .on('change', debouncedReloadFrontend)
+        .on('unlink', debouncedReloadFrontend);
 }
 
 let webSocketServer;

From a1d4e062ed2e06800d93f5f2a2de90e16af41cc0 Mon Sep 17 00:00:00 2001
From: zadam 
Date: Sun, 16 Apr 2023 09:22:24 +0200
Subject: [PATCH 16/21] refactoring of "some path" WIP

---
 src/becca/becca_service.js                    | 128 +-----------------
 src/becca/entities/bnote.js                   |  15 ++
 src/becca/similarity.js                       |   2 +-
 src/routes/api/recent_changes.js              |  22 +--
 .../search/expressions/note_flat_text.js      |  13 +-
 src/services/search/services/search.js        |   2 +-
 6 files changed, 45 insertions(+), 137 deletions(-)

diff --git a/src/becca/becca_service.js b/src/becca/becca_service.js
index 47e7d39e2..d4a4269bb 100644
--- a/src/becca/becca_service.js
+++ b/src/becca/becca_service.js
@@ -24,49 +24,12 @@ function isNotePathArchived(notePath) {
     return false;
 }
 
-/**
- * This assumes that note is available. "archived" note means that there isn't a single non-archived note-path
- * leading to this note.
- *
- * @param noteId
- */
-function isArchived(noteId) {
-    const notePath = getSomePath(noteId);
-
-    return isNotePathArchived(notePath);
-}
-
-/**
- * @param {string} noteId
- * @param {string} ancestorNoteId
- * @returns {boolean} - true if given noteId has ancestorNoteId in any of its paths (even archived)
- */
-function isInAncestor(noteId, ancestorNoteId) {
-    if (ancestorNoteId === 'root' || ancestorNoteId === noteId) {
-        return true;
-    }
-
-    const note = becca.notes[noteId];
-
-    if (!note) {
-        return false;
-    }
-
-    for (const parentNote of note.parents) {
-        if (isInAncestor(parentNote.noteId, ancestorNoteId)) {
-            return true;
-        }
-    }
-
-    return false;
-}
-
 function getNoteTitle(childNoteId, parentNoteId) {
     const childNote = becca.notes[childNoteId];
     const parentNote = becca.notes[parentNoteId];
 
     if (!childNote) {
-        log.info(`Cannot find note in cache for noteId '${childNoteId}'`);
+        log.info(`Cannot find note '${childNoteId}'`);
         return "[error fetching title]";
     }
 
@@ -119,86 +82,15 @@ function getNoteTitleForPath(notePathArray) {
     return titles.join(' / ');
 }
 
-/**
- * Returns notePath for noteId. Note hoisting is respected.
- * Archived (and hidden) notes are also returned, but non-archived paths are preferred if available
- * - this means that archived paths is returned only if there's no non-archived path
- * - you can check whether returned path is archived using isArchived
- *
- * @param {BNote} note
- * @param {string[]} path
- */
-function getSomePath(note, path = []) {
-    // first try to find note within hoisted note, otherwise take any existing note path
-    return getSomePathInner(note, path, true)
-        || getSomePathInner(note, path, false);
-}
-
-/**
- * @param {BNote} note
- * @param {string[]} parentPath
- * @param {boolean} respectHoisting
- * @returns {string[]|false}
- */
-function getSomePathInner(note, parentPath, respectHoisting) {
-    const childPath = [...parentPath, note.noteId];
-    if (note.isRoot()) {
-        childPath.reverse();
-
-        if (respectHoisting && !childPath.includes(cls.getHoistedNoteId())) {
-            return false;
-        }
-
-        return childPath;
-    }
-
-    const parents = note.parents;
-    if (parents.length === 0) {
-        console.log(`Note '${note.noteId}' - '${note.title}' has no parents.`);
-
-        return false;
-    }
-
-    const completeNotePaths = parents.map(parentNote => getSomePathInner(parentNote, childPath, respectHoisting));
-
-    if (completeNotePaths.length === 0) {
-        return false;
-    } else if (completeNotePaths.length === 1) {
-        return completeNotePaths[0];
-    } else {
-        completeNotePaths.sort((a, b) => {
-            if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
-                return a.isInHoistedSubTree ? -1 : 1;
-            } else if (a.isSearch !== b.isSearch) {
-                return a.isSearch ? 1 : -1;
-            } else if (a.isArchived !== b.isArchived) {
-                return a.isArchived ? 1 : -1;
-            } else if (a.isHidden !== b.isHidden) {
-                return a.isHidden ? 1 : -1;
-            } else {
-                return a.notePath.length - b.notePath.length;
-            }
-        });
-
-        // if there are multiple valid paths, take the shortest one
-        const shortestNotePath = completeNotePaths.reduce((shortestPath, nextPath) =>
-            nextPath.length < shortestPath.length
-                ? nextPath
-                : shortestPath, completeNotePaths[0]);
-
-        return shortestNotePath;
-    }
-}
-
 function getNotePath(noteId) {
     const note = becca.notes[noteId];
 
     if (!note) {
-        console.trace(`Cannot find note '${noteId}' in cache.`);
+        console.trace(`Cannot find note '${noteId}'.`);
         return;
     }
 
-    const retPath = getSomePath(note);
+    const retPath = note.getBestNotePath();
 
     if (retPath) {
         const noteTitle = getNoteTitleForPath(retPath);
@@ -223,23 +115,9 @@ function getNotePath(noteId) {
     }
 }
 
-/**
- * @param noteId
- * @returns {boolean} - true if note exists (is not deleted) and is available in current note hoisting
- */
-function isAvailable(noteId) {
-    const notePath = getNotePath(noteId);
-
-    return !!notePath;
-}
-
 module.exports = {
-    getSomePath,
     getNotePath,
     getNoteTitle,
     getNoteTitleForPath,
-    isAvailable,
-    isArchived,
-    isInAncestor,
     isNotePathArchived
 };
diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js
index 625ba1f4b..0f1b3ea8b 100644
--- a/src/becca/entities/bnote.js
+++ b/src/becca/entities/bnote.js
@@ -748,6 +748,21 @@ class BNote extends AbstractBeccaEntity {
         return this.hasAttribute('label', 'archived');
     }
 
+    areAllNotePathsArchived() {
+        // there's a slight difference between note being itself archived and all its note paths being archived
+        // - note is archived when it itself has an archived label or inherits it
+        // - note does not have or inherit archived label, but each note paths contains a note with (non-inheritable)
+        //   archived label
+
+        const bestNotePathRecord = this.getSortedNotePathRecords()[0];
+
+        if (!bestNotePathRecord) {
+            throw new Error(`No note path available for note '${this.noteId}'`);
+        }
+
+        return bestNotePathRecord.isArchived;
+    }
+
     hasInheritableArchivedLabel() {
         for (const attr of this.getAttributes()) {
             if (attr.name === 'archived' && attr.type === LABEL && attr.isInheritable) {
diff --git a/src/becca/similarity.js b/src/becca/similarity.js
index 2e7750100..8900bb87d 100644
--- a/src/becca/similarity.js
+++ b/src/becca/similarity.js
@@ -404,7 +404,7 @@ async function findSimilarNotes(noteId) {
         let score = computeScore(candidateNote);
 
         if (score >= 1.5) {
-            const notePath = beccaService.getSomePath(candidateNote);
+            const notePath = candidateNote.getBestNotePath();
 
             // this takes care of note hoisting
             if (!notePath) {
diff --git a/src/routes/api/recent_changes.js b/src/routes/api/recent_changes.js
index 2d54af93b..646898e9c 100644
--- a/src/routes/api/recent_changes.js
+++ b/src/routes/api/recent_changes.js
@@ -3,14 +3,14 @@
 const sql = require('../../services/sql');
 const protectedSessionService = require('../../services/protected_session');
 const noteService = require('../../services/notes');
-const beccaService = require('../../becca/becca_service');
+const becca = require("../../becca/becca");
 
 function getRecentChanges(req) {
     const {ancestorNoteId} = req.params;
 
     let recentChanges = [];
 
-    const noteRevisions = sql.getRows(`
+    const noteRevisionRows = sql.getRows(`
         SELECT 
             notes.noteId,
             notes.isDeleted AS current_isDeleted,
@@ -24,16 +24,18 @@ function getRecentChanges(req) {
             note_revisions
             JOIN notes USING(noteId)`);
 
-    for (const noteRevision of noteRevisions) {
-        if (beccaService.isInAncestor(noteRevision.noteId, ancestorNoteId)) {
-            recentChanges.push(noteRevision);
+    for (const noteRevisionRow of noteRevisionRows) {
+        const note = becca.getNote(noteRevisionRow.noteId);
+
+        if (note?.hasAncestor(ancestorNoteId)) {
+            recentChanges.push(noteRevisionRow);
         }
     }
 
     // now we need to also collect date points not represented in note revisions:
     // 1. creation for all notes (dateCreated)
     // 2. deletion for deleted notes (dateModified)
-    const notes = sql.getRows(`
+    const noteRows = sql.getRows(`
             SELECT
                 notes.noteId,
                 notes.isDeleted AS current_isDeleted,
@@ -57,9 +59,11 @@ function getRecentChanges(req) {
             FROM notes
             WHERE notes.isDeleted = 1`);
 
-    for (const note of notes) {
-        if (beccaService.isInAncestor(note.noteId, ancestorNoteId)) {
-            recentChanges.push(note);
+    for (const noteRow of noteRows) {
+        const note = becca.getNote(noteRow.noteId);
+
+        if (note?.hasAncestor(ancestorNoteId)) {
+            recentChanges.push(noteRow);
         }
     }
 
diff --git a/src/services/search/expressions/note_flat_text.js b/src/services/search/expressions/note_flat_text.js
index 863573a13..024160985 100644
--- a/src/services/search/expressions/note_flat_text.js
+++ b/src/services/search/expressions/note_flat_text.js
@@ -24,7 +24,7 @@ class NoteFlatTextExp extends Expression {
          */
         function searchDownThePath(note, tokens, path) {
             if (tokens.length === 0) {
-                const retPath = beccaService.getSomePath(note, path);
+                const retPath = this.getNotePath(note, path);
 
                 if (retPath) {
                     const noteId = retPath[retPath.length - 1];
@@ -131,6 +131,17 @@ class NoteFlatTextExp extends Expression {
         return resultNoteSet;
     }
 
+    getNotePath(note, path) {
+        if (path.length === 0) {
+            return note.getBestNotePath();
+        } else {
+            const closestNoteId = path[0];
+            const closestNoteBestNotePath = becca.getNote(closestNoteId).getBestNotePathString();
+
+            return [...closestNoteBestNotePath, ...path.slice(1)];
+        }
+    }
+
     /**
      * Returns noteIds which have at least one matching tokens
      *
diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js
index f1adc72d7..3e36040ab 100644
--- a/src/services/search/services/search.js
+++ b/src/services/search/services/search.js
@@ -157,7 +157,7 @@ function findResultsWithExpression(expression, searchContext) {
     const searchResults = noteSet.notes
         .filter(note => !note.isDeleted)
         .map(note => {
-            const notePathArray = executionContext.noteIdToNotePath[note.noteId] || beccaService.getSomePath(note);
+            const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath();
 
             if (!notePathArray) {
                 throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`);

From 2e794ee38ffda471980f361a57da6e517d16d382 Mon Sep 17 00:00:00 2001
From: zadam 
Date: Sun, 16 Apr 2023 09:26:52 +0200
Subject: [PATCH 17/21] fixes

---
 src/becca/entities/bnote.js                       | 3 +--
 src/services/search/expressions/note_flat_text.js | 6 ++++--
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js
index 0f1b3ea8b..b8e58c740 100644
--- a/src/becca/entities/bnote.js
+++ b/src/becca/entities/bnote.js
@@ -12,7 +12,6 @@ const TaskContext = require("../../services/task_context");
 const dayjs = require("dayjs");
 const utc = require('dayjs/plugin/utc');
 const eventService = require("../../services/events");
-const froca = require("../../public/app/services/froca.js");
 dayjs.extend(utc);
 
 const LABEL = 'label';
@@ -1201,7 +1200,7 @@ class BNote extends AbstractBeccaEntity {
         const notePaths = this.getAllNotePaths().map(path => ({
             notePath: path,
             isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
-            isArchived: path.some(noteId => froca.notes[noteId].isArchived),
+            isArchived: path.some(noteId => this.becca.notes[noteId].isArchived),
             isHidden: path.includes('_hidden')
         }));
 
diff --git a/src/services/search/expressions/note_flat_text.js b/src/services/search/expressions/note_flat_text.js
index 024160985..9dbf8ba9c 100644
--- a/src/services/search/expressions/note_flat_text.js
+++ b/src/services/search/expressions/note_flat_text.js
@@ -22,10 +22,12 @@ class NoteFlatTextExp extends Expression {
          * @param {string[]} tokens
          * @param {string[]} path
          */
-        function searchDownThePath(note, tokens, path) {
+        const searchDownThePath = (note, tokens, path) => {
             if (tokens.length === 0) {
                 const retPath = this.getNotePath(note, path);
 
+                console.log(retPath);
+
                 if (retPath) {
                     const noteId = retPath[retPath.length - 1];
 
@@ -136,7 +138,7 @@ class NoteFlatTextExp extends Expression {
             return note.getBestNotePath();
         } else {
             const closestNoteId = path[0];
-            const closestNoteBestNotePath = becca.getNote(closestNoteId).getBestNotePathString();
+            const closestNoteBestNotePath = becca.getNote(closestNoteId).getBestNotePath();
 
             return [...closestNoteBestNotePath, ...path.slice(1)];
         }

From 63e044ffdf6b69690cfde67c426bfbdc8bf38e65 Mon Sep 17 00:00:00 2001
From: zadam 
Date: Sun, 16 Apr 2023 11:28:24 +0200
Subject: [PATCH 18/21] refactoring

---
 src/becca/becca_service.js                    | 34 -------------------
 src/routes/api/note_revisions.js              | 28 ++++++++++++++-
 src/services/cloning.js                       |  2 +-
 .../search/expressions/note_flat_text.js      |  2 --
 4 files changed, 28 insertions(+), 38 deletions(-)

diff --git a/src/becca/becca_service.js b/src/becca/becca_service.js
index d4a4269bb..aad6fff5b 100644
--- a/src/becca/becca_service.js
+++ b/src/becca/becca_service.js
@@ -82,41 +82,7 @@ function getNoteTitleForPath(notePathArray) {
     return titles.join(' / ');
 }
 
-function getNotePath(noteId) {
-    const note = becca.notes[noteId];
-
-    if (!note) {
-        console.trace(`Cannot find note '${noteId}'.`);
-        return;
-    }
-
-    const retPath = note.getBestNotePath();
-
-    if (retPath) {
-        const noteTitle = getNoteTitleForPath(retPath);
-
-        let branchId;
-
-        if (note.isRoot()) {
-            branchId = 'none_root';
-        }
-        else {
-            const parentNote = note.parents[0];
-            branchId = becca.getBranchFromChildAndParent(noteId, parentNote.noteId).branchId;
-        }
-
-        return {
-            noteId: noteId,
-            branchId: branchId,
-            title: noteTitle,
-            notePath: retPath,
-            path: retPath.join('/')
-        };
-    }
-}
-
 module.exports = {
-    getNotePath,
     getNoteTitle,
     getNoteTitleForPath,
     isNotePathArchived
diff --git a/src/routes/api/note_revisions.js b/src/routes/api/note_revisions.js
index 7448eb7da..f28239f2e 100644
--- a/src/routes/api/note_revisions.js
+++ b/src/routes/api/note_revisions.js
@@ -135,7 +135,7 @@ function getEditedNotesOnDate(req) {
     notes = notes.map(note => note.getPojo());
 
     for (const note of notes) {
-        const notePath = note.isDeleted ? null : beccaService.getNotePath(note.noteId);
+        const notePath = note.isDeleted ? null : getNotePathData(note);
 
         note.notePath = notePath ? notePath.notePath : null;
     }
@@ -143,6 +143,32 @@ function getEditedNotesOnDate(req) {
     return notes;
 }
 
+function getNotePathData(note) {
+    const retPath = note.getBestNotePath();
+
+    if (retPath) {
+        const noteTitle = beccaService.getNoteTitleForPath(retPath);
+
+        let branchId;
+
+        if (note.isRoot()) {
+            branchId = 'none_root';
+        }
+        else {
+            const parentNote = note.parents[0];
+            branchId = becca.getBranchFromChildAndParent(note.noteId, parentNote.noteId).branchId;
+        }
+
+        return {
+            noteId: note.noteId,
+            branchId: branchId,
+            title: noteTitle,
+            notePath: retPath,
+            path: retPath.join('/')
+        };
+    }
+}
+
 module.exports = {
     getNoteRevisions,
     getNoteRevision,
diff --git a/src/services/cloning.js b/src/services/cloning.js
index b08b3d6b3..f62b8724b 100644
--- a/src/services/cloning.js
+++ b/src/services/cloning.js
@@ -40,7 +40,7 @@ function cloneNoteToParentNote(noteId, parentNoteId, prefix) {
     return {
         success: true,
         branchId: branch.branchId,
-        notePath: `${beccaService.getNotePath(parentNoteId).path}/${noteId}`
+        notePath: `${parentNote.getBestNotePathString()}/${noteId}`
     };
 }
 
diff --git a/src/services/search/expressions/note_flat_text.js b/src/services/search/expressions/note_flat_text.js
index 9dbf8ba9c..62985f590 100644
--- a/src/services/search/expressions/note_flat_text.js
+++ b/src/services/search/expressions/note_flat_text.js
@@ -26,8 +26,6 @@ class NoteFlatTextExp extends Expression {
             if (tokens.length === 0) {
                 const retPath = this.getNotePath(note, path);
 
-                console.log(retPath);
-
                 if (retPath) {
                     const noteId = retPath[retPath.length - 1];
 

From 8226f62ded75d9fea8b844e4d5a7c9167b3435f1 Mon Sep 17 00:00:00 2001
From: zadam 
Date: Sun, 16 Apr 2023 23:11:18 +0200
Subject: [PATCH 19/21] download offline images from libreoffice, fixes #3842

---
 package-lock.json                 |  1 +
 src/public/app/widgets/mermaid.js |  1 -
 src/services/notes.js             | 20 +++++++++++++++++++-
 3 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index f6fd5eaef..59a3f9bd6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "trilium",
       "version": "0.59.3",
       "hasInstallScript": true,
       "license": "AGPL-3.0-only",
diff --git a/src/public/app/widgets/mermaid.js b/src/public/app/widgets/mermaid.js
index d07ce83e0..4a75e643b 100644
--- a/src/public/app/widgets/mermaid.js
+++ b/src/public/app/widgets/mermaid.js
@@ -1,7 +1,6 @@
 import libraryLoader from "../services/library_loader.js";
 import NoteContextAwareWidget from "./note_context_aware_widget.js";
 import froca from "../services/froca.js";
-import server from "../services/server.js";
 
 const TPL = `