mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	Merge branch 'master' into next60
This commit is contained in:
		
							
								
								
									
										7
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					dist
 | 
				
			||||||
 | 
					bin
 | 
				
			||||||
 | 
					docs
 | 
				
			||||||
 | 
					libraries
 | 
				
			||||||
 | 
					coverage
 | 
				
			||||||
 | 
					play
 | 
				
			||||||
							
								
								
									
										221
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										221
									
								
								.eslintrc.js
									
									
									
									
									
								
							@@ -1,16 +1,213 @@
 | 
				
			|||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    "env": {
 | 
					    env: {
 | 
				
			||||||
        "browser": true,
 | 
					        browser: true,
 | 
				
			||||||
        "commonjs": true,
 | 
					        commonjs: true,
 | 
				
			||||||
        "es2021": true,
 | 
					        es2021: true,
 | 
				
			||||||
        "node": true
 | 
					        node: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "extends": "eslint:recommended",
 | 
					    // plugins: ['prettier'], // to be activated
 | 
				
			||||||
    "overrides": [
 | 
					    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': [
 | 
				
			||||||
 | 
					                    'off',
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        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',
 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
    "parserOptions": {
 | 
					 | 
				
			||||||
        "ecmaVersion": "latest"
 | 
					 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
    "rules": {
 | 
					                    {
 | 
				
			||||||
    }
 | 
					                        pathPattern: '^(?:dev|peer|optional|bundled)?[Dd]ependencies$',
 | 
				
			||||||
}
 | 
					                        order: { type: 'asc' },
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    parserOptions: {
 | 
				
			||||||
 | 
					        ecmaVersion: 'latest',
 | 
				
			||||||
 | 
					        sourceType: 'module',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    rules: {
 | 
				
			||||||
 | 
					        // 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': ['off', { 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',
 | 
				
			||||||
 | 
					        'no-prototype-builtins':'off'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -12,3 +12,4 @@ server-package.json
 | 
				
			|||||||
.idea/httpRequests/
 | 
					.idea/httpRequests/
 | 
				
			||||||
data/
 | 
					data/
 | 
				
			||||||
tmp/
 | 
					tmp/
 | 
				
			||||||
 | 
					.eslintcache
 | 
				
			||||||
							
								
								
									
										1
									
								
								.husky/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.husky/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					_
 | 
				
			||||||
							
								
								
									
										4
									
								
								.husky/pre-commit
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.husky/pre-commit
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					#!/bin/sh
 | 
				
			||||||
 | 
					. "$(dirname "$0")/_/husky.sh"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#npx lint-staged
 | 
				
			||||||
							
								
								
									
										11
									
								
								.prettierrc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.prettierrc.js
									
									
									
									
									
										Normal file
									
								
							@@ -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',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										6
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "recommendations": [
 | 
				
			||||||
 | 
					    "dbaeumer.vscode-eslint",
 | 
				
			||||||
 | 
					    "esbenp.prettier-vscode",
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										33
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "[javascript]": {
 | 
				
			||||||
 | 
					    "editor.defaultFormatter": "dbaeumer.vscode-eslint"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "[json]": {
 | 
				
			||||||
 | 
					    "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",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -8,7 +8,6 @@ if (config.https) {
 | 
				
			|||||||
    // built-in TLS (terminated by trilium) is not supported yet, PRs are welcome
 | 
					    // 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
 | 
					    // for reverse proxy terminated TLS this will works since config.https will be false
 | 
				
			||||||
    process.exit(0);
 | 
					    process.exit(0);
 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const port = require('./src/services/port');
 | 
					const port = require('./src/services/port');
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								libraries/codemirror/addon/lint/eslint.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								libraries/codemirror/addon/lint/eslint.js
									
									
									
									
										vendored
									
									
								
							@@ -46,7 +46,7 @@
 | 
				
			|||||||
        const errors = new eslint().verify(text, {
 | 
					        const errors = new eslint().verify(text, {
 | 
				
			||||||
            root: true,
 | 
					            root: true,
 | 
				
			||||||
            parserOptions: {
 | 
					            parserOptions: {
 | 
				
			||||||
                ecmaVersion: 2019
 | 
					                ecmaVersion: 2022
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            extends: ['eslint:recommended', 'airbnb-base'],
 | 
					            extends: ['eslint:recommended', 'airbnb-base'],
 | 
				
			||||||
            env: {
 | 
					            env: {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								nodemon.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								nodemon.json
									
									
									
									
									
										Normal file
									
								
							@@ -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"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								package.json
									
									
									
									
									
								
							@@ -2,7 +2,7 @@
 | 
				
			|||||||
  "name": "trilium",
 | 
					  "name": "trilium",
 | 
				
			||||||
  "productName": "Trilium Notes",
 | 
					  "productName": "Trilium Notes",
 | 
				
			||||||
  "description": "Trilium Notes",
 | 
					  "description": "Trilium Notes",
 | 
				
			||||||
  "version": "0.59.3",
 | 
					  "version": "0.59.4",
 | 
				
			||||||
  "license": "AGPL-3.0-only",
 | 
					  "license": "AGPL-3.0-only",
 | 
				
			||||||
  "main": "electron.js",
 | 
					  "main": "electron.js",
 | 
				
			||||||
  "bin": {
 | 
					  "bin": {
 | 
				
			||||||
@@ -13,20 +13,22 @@
 | 
				
			|||||||
    "url": "https://github.com/zadam/trilium.git"
 | 
					    "url": "https://github.com/zadam/trilium.git"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "start-server": "cross-env TRILIUM_DATA_DIR=./data 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 node ./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": "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 .",
 | 
					    "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-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-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-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",
 | 
					    "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-jasmine": "jasmine",
 | 
				
			||||||
    "test-es6": "node -r esm spec-es6/attribute_parser.spec.js ",
 | 
					    "test-es6": "node -r esm spec-es6/attribute_parser.spec.js ",
 | 
				
			||||||
    "test": "npm run test-jasmine && npm run test-es6",
 | 
					    "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": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@braintree/sanitize-url": "6.0.2",
 | 
					    "@braintree/sanitize-url": "6.0.2",
 | 
				
			||||||
@@ -100,15 +102,28 @@
 | 
				
			|||||||
    "electron-packager": "17.1.1",
 | 
					    "electron-packager": "17.1.1",
 | 
				
			||||||
    "electron-rebuild": "3.2.9",
 | 
					    "electron-rebuild": "3.2.9",
 | 
				
			||||||
    "eslint": "^8.38.0",
 | 
					    "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",
 | 
					    "esm": "3.2.25",
 | 
				
			||||||
 | 
					    "husky": "^8.0.3",
 | 
				
			||||||
 | 
					    "jsonc-eslint-parser": "^2.2.0",
 | 
				
			||||||
 | 
					    "lint-staged": "^13.2.1",
 | 
				
			||||||
    "jasmine": "4.6.0",
 | 
					    "jasmine": "4.6.0",
 | 
				
			||||||
    "jsdoc": "4.0.2",
 | 
					    "jsdoc": "4.0.2",
 | 
				
			||||||
    "lorem-ipsum": "2.0.8",
 | 
					    "lorem-ipsum": "2.0.8",
 | 
				
			||||||
 | 
					    "prettier": "2.8.7",
 | 
				
			||||||
 | 
					    "nodemon": "^2.0.22",
 | 
				
			||||||
    "rcedit": "3.0.1",
 | 
					    "rcedit": "3.0.1",
 | 
				
			||||||
    "webpack": "5.78.0",
 | 
					    "webpack": "5.78.0",
 | 
				
			||||||
    "webpack-cli": "5.0.1"
 | 
					    "webpack-cli": "5.0.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "optionalDependencies": {
 | 
					  "optionalDependencies": {
 | 
				
			||||||
    "electron-installer-debian": "3.1.0"
 | 
					    "electron-installer-debian": "3.1.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "lint-staged": {
 | 
				
			||||||
 | 
					    "*.js": "eslint --cache --fix"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,49 +24,12 @@ function isNotePathArchived(notePath) {
 | 
				
			|||||||
    return false;
 | 
					    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) {
 | 
					function getNoteTitle(childNoteId, parentNoteId) {
 | 
				
			||||||
    const childNote = becca.notes[childNoteId];
 | 
					    const childNote = becca.notes[childNoteId];
 | 
				
			||||||
    const parentNote = becca.notes[parentNoteId];
 | 
					    const parentNote = becca.notes[parentNoteId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!childNote) {
 | 
					    if (!childNote) {
 | 
				
			||||||
        log.info(`Cannot find note in cache for noteId '${childNoteId}'`);
 | 
					        log.info(`Cannot find note '${childNoteId}'`);
 | 
				
			||||||
        return "[error fetching title]";
 | 
					        return "[error fetching title]";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -119,107 +82,8 @@ function getNoteTitleForPath(notePathArray) {
 | 
				
			|||||||
    return titles.join(' / ');
 | 
					    return titles.join(' / ');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Returns notePath for noteId from cache. 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[]} path
 | 
					 | 
				
			||||||
 * @param {boolean}respectHoisting
 | 
					 | 
				
			||||||
 * @returns {string[]|false}
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function getSomePathInner(note, path, respectHoisting) {
 | 
					 | 
				
			||||||
    if (note.isRoot()) {
 | 
					 | 
				
			||||||
        const foundPath = [...path, note.noteId];
 | 
					 | 
				
			||||||
        foundPath.reverse();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (respectHoisting && !foundPath.includes(cls.getHoistedNoteId())) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return foundPath;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const parents = note.parents;
 | 
					 | 
				
			||||||
    if (parents.length === 0) {
 | 
					 | 
				
			||||||
        console.log(`Note '${note.noteId}' - '${note.title}' has no parents.`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const parentNote of parents) {
 | 
					 | 
				
			||||||
        const retPath = getSomePathInner(parentNote, [...path, note.noteId], respectHoisting);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (retPath) {
 | 
					 | 
				
			||||||
            return retPath;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function getNotePath(noteId) {
 | 
					 | 
				
			||||||
    const note = becca.notes[noteId];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!note) {
 | 
					 | 
				
			||||||
        console.trace(`Cannot find note '${noteId}' in cache.`);
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const retPath = getSomePath(note);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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('/')
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * @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 = {
 | 
					module.exports = {
 | 
				
			||||||
    getSomePath,
 | 
					 | 
				
			||||||
    getNotePath,
 | 
					 | 
				
			||||||
    getNoteTitle,
 | 
					    getNoteTitle,
 | 
				
			||||||
    getNoteTitleForPath,
 | 
					    getNoteTitleForPath,
 | 
				
			||||||
    isAvailable,
 | 
					 | 
				
			||||||
    isArchived,
 | 
					 | 
				
			||||||
    isInAncestor,
 | 
					 | 
				
			||||||
    isNotePathArchived
 | 
					    isNotePathArchived
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -688,6 +688,21 @@ class BNote extends AbstractBeccaEntity {
 | 
				
			|||||||
        return this.hasAttribute('label', 'archived');
 | 
					        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() {
 | 
					    hasInheritableArchivedLabel() {
 | 
				
			||||||
        for (const attr of this.getAttributes()) {
 | 
					        for (const attr of this.getAttributes()) {
 | 
				
			||||||
            if (attr.name === 'archived' && attr.type === LABEL && attr.isInheritable) {
 | 
					            if (attr.name === 'archived' && attr.type === LABEL && attr.isInheritable) {
 | 
				
			||||||
@@ -1118,6 +1133,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)
 | 
					     * @returns {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path)
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getAllNotePaths() {
 | 
					    getAllNotePaths() {
 | 
				
			||||||
@@ -1125,18 +1142,73 @@ class BNote extends AbstractBeccaEntity {
 | 
				
			|||||||
            return [['root']];
 | 
					            return [['root']];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const notePaths = [];
 | 
					        const parentNotes = this.getParentNotes();
 | 
				
			||||||
 | 
					        let notePaths = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const parentNote of this.getParentNotes()) {
 | 
					        if (parentNotes.length === 1) { // optimization for most common case
 | 
				
			||||||
            for (const parentPath of parentNote.getAllNotePaths()) {
 | 
					            notePaths = parentNotes[0].getAllNotePaths();
 | 
				
			||||||
                parentPath.push(this.noteId);
 | 
					        } else {
 | 
				
			||||||
                notePaths.push(parentPath);
 | 
					            notePaths = parentNotes.flatMap(parentNote => parentNote.getAllNotePaths());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const notePath of notePaths) {
 | 
				
			||||||
 | 
					            notePath.push(this.noteId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return notePaths;
 | 
					        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 => this.becca.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
 | 
					     * @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -404,7 +404,7 @@ async function findSimilarNotes(noteId) {
 | 
				
			|||||||
        let score = computeScore(candidateNote);
 | 
					        let score = computeScore(candidateNote);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (score >= 1.5) {
 | 
					        if (score >= 1.5) {
 | 
				
			||||||
            const notePath = beccaService.getSomePath(candidateNote);
 | 
					            const notePath = candidateNote.getBestNotePath();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // this takes care of note hoisting
 | 
					            // this takes care of note hoisting
 | 
				
			||||||
            if (!notePath) {
 | 
					            if (!notePath) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -413,7 +413,12 @@ export default class TabManager extends Component {
 | 
				
			|||||||
            await this.triggerEvent('beforeNoteContextRemove', { ntxIds: ntxIdsToRemove });
 | 
					            await this.triggerEvent('beforeNoteContextRemove', { ntxIds: ntxIdsToRemove });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!noteContextToRemove.isMainContext()) {
 | 
					            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) {
 | 
					            else if (this.mainNoteContexts.length <= 1) {
 | 
				
			||||||
                await this.openAndActivateEmptyTab();
 | 
					                await this.openAndActivateEmptyTab();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -268,6 +268,11 @@ class FNote {
 | 
				
			|||||||
        return this.__filterAttrs(this.__getCachedAttributes([]), type, name);
 | 
					        return this.__filterAttrs(this.__getCachedAttributes([]), type, name);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {string[]} path
 | 
				
			||||||
 | 
					     * @return {FAttribute[]}
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    __getCachedAttributes(path) {
 | 
					    __getCachedAttributes(path) {
 | 
				
			||||||
        // notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates
 | 
					        // 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
 | 
					        // when template instance is a parent of template itself
 | 
				
			||||||
@@ -320,63 +325,49 @@ class FNote {
 | 
				
			|||||||
        return this.noteId === 'root';
 | 
					        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') {
 | 
					        if (this.noteId === 'root') {
 | 
				
			||||||
            return [['root']];
 | 
					            return [['root']];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!encounteredNoteIds) {
 | 
					 | 
				
			||||||
            encounteredNoteIds = new Set();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        encounteredNoteIds.add(this.noteId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const parentNotes = this.getParentNotes();
 | 
					        const parentNotes = this.getParentNotes();
 | 
				
			||||||
        let paths;
 | 
					        let notePaths = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (parentNotes.length === 1) { // optimization for the most common case
 | 
					        if (parentNotes.length === 1) { // optimization for most common case
 | 
				
			||||||
            if (encounteredNoteIds.has(parentNotes[0].noteId)) {
 | 
					            notePaths = parentNotes[0].getAllNotePaths();
 | 
				
			||||||
                return [];
 | 
					        } else {
 | 
				
			||||||
            }
 | 
					            notePaths = parentNotes.flatMap(parentNote => parentNote.getAllNotePaths());
 | 
				
			||||||
            else {
 | 
					 | 
				
			||||||
                paths = parentNotes[0].getAllNotePaths(encounteredNoteIds);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        else {
 | 
					 | 
				
			||||||
            paths = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for (const parentNote of parentNotes) {
 | 
					 | 
				
			||||||
                if (encounteredNoteIds.has(parentNote.noteId)) {
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                const newSet = new Set(encounteredNoteIds);
 | 
					        for (const notePath of notePaths) {
 | 
				
			||||||
 | 
					            notePath.push(this.noteId);
 | 
				
			||||||
                paths.push(...parentNote.getAllNotePaths(newSet));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const path of paths) {
 | 
					        return notePaths;
 | 
				
			||||||
            path.push(this.noteId);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return paths;
 | 
					    /**
 | 
				
			||||||
    }
 | 
					     * @param {string} [hoistedNoteId='root']
 | 
				
			||||||
 | 
					     * @return {{isArchived: boolean, isInHoistedSubTree: boolean, notePath: string[], isHidden: boolean}[]}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getSortedNotePathRecords(hoistedNoteId = 'root') {
 | 
				
			||||||
 | 
					        const isHoistedRoot = hoistedNoteId === 'root';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getSortedNotePaths(hoistedNotePath = 'root') {
 | 
					 | 
				
			||||||
        const notePaths = this.getAllNotePaths().map(path => ({
 | 
					        const notePaths = this.getAllNotePaths().map(path => ({
 | 
				
			||||||
            notePath: path,
 | 
					            notePath: path,
 | 
				
			||||||
            isInHoistedSubTree: path.includes(hoistedNotePath),
 | 
					            isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
 | 
				
			||||||
            isArchived: path.find(noteId => froca.notes[noteId].isArchived),
 | 
					            isArchived: path.some(noteId => froca.notes[noteId].isArchived),
 | 
				
			||||||
            isSearch: path.find(noteId => froca.notes[noteId].type === 'search'),
 | 
					 | 
				
			||||||
            isHidden: path.includes('_hidden')
 | 
					            isHidden: path.includes('_hidden')
 | 
				
			||||||
        }));
 | 
					        }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        notePaths.sort((a, b) => {
 | 
					        notePaths.sort((a, b) => {
 | 
				
			||||||
            if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
 | 
					            if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
 | 
				
			||||||
                return a.isInHoistedSubTree ? -1 : 1;
 | 
					                return a.isInHoistedSubTree ? -1 : 1;
 | 
				
			||||||
            } else if (a.isSearch !== b.isSearch) {
 | 
					 | 
				
			||||||
                return a.isSearch ? 1 : -1;
 | 
					 | 
				
			||||||
            } else if (a.isArchived !== b.isArchived) {
 | 
					            } else if (a.isArchived !== b.isArchived) {
 | 
				
			||||||
                return a.isArchived ? 1 : -1;
 | 
					                return a.isArchived ? 1 : -1;
 | 
				
			||||||
            } else if (a.isHidden !== b.isHidden) {
 | 
					            } else if (a.isHidden !== b.isHidden) {
 | 
				
			||||||
@@ -389,6 +380,28 @@ class FNote {
 | 
				
			|||||||
        return notePaths;
 | 
					        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
 | 
					     * @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
@@ -412,6 +425,13 @@ class FNote {
 | 
				
			|||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {FAttribute[]} attributes
 | 
				
			||||||
 | 
					     * @param {string} type
 | 
				
			||||||
 | 
					     * @param {string} name
 | 
				
			||||||
 | 
					     * @return {FAttribute[]}
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    __filterAttrs(attributes, type, name) {
 | 
					    __filterAttrs(attributes, type, name) {
 | 
				
			||||||
        this.__validateTypeName(type, name);
 | 
					        this.__validateTypeName(type, name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -541,7 +561,9 @@ class FNote {
 | 
				
			|||||||
     * @returns {boolean} true if note has an attribute with given type and name (including inherited)
 | 
					     * @returns {boolean} true if note has an attribute with given type and name (including inherited)
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    hasAttribute(type, name) {
 | 
					    hasAttribute(type, name) {
 | 
				
			||||||
        return !!this.getAttribute(type, name);
 | 
					        const attributes = this.getAttributes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return attributes.some(attr => attr.name === name && attr.type === type);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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}`, {
 | 
					    const resp = await server.put(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
 | 
				
			||||||
        prefix: prefix
 | 
					        prefix: prefix
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -254,5 +254,5 @@ export default {
 | 
				
			|||||||
    moveNodeUpInHierarchy,
 | 
					    moveNodeUpInHierarchy,
 | 
				
			||||||
    cloneNoteAfter,
 | 
					    cloneNoteAfter,
 | 
				
			||||||
    cloneNoteToBranch,
 | 
					    cloneNoteToBranch,
 | 
				
			||||||
    cloneNoteToNote,
 | 
					    cloneNoteToParentNote,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -140,7 +140,7 @@ async function processBranchChange(loadResults, ec) {
 | 
				
			|||||||
    const childNote = froca.notes[ec.entity.noteId];
 | 
					    const childNote = froca.notes[ec.entity.noteId];
 | 
				
			||||||
    let parentNote = froca.notes[ec.entity.parentNoteId];
 | 
					    let parentNote = froca.notes[ec.entity.parentNoteId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (childNote && !parentNote) {
 | 
					    if (childNote && !childNote.isRoot() && !parentNote) {
 | 
				
			||||||
        // a branch cannot exist without the parent
 | 
					        // a branch cannot exist without the parent
 | 
				
			||||||
        // a note loaded into froca has to also contain all its ancestors
 | 
					        // 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
 | 
					        // this problem happened e.g. in sharing where _share was hidden and thus not loaded
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@ import server from "./server.js";
 | 
				
			|||||||
import appContext from "../components/app_context.js";
 | 
					import appContext from "../components/app_context.js";
 | 
				
			||||||
import utils from './utils.js';
 | 
					import utils from './utils.js';
 | 
				
			||||||
import noteCreateService from './note_create.js';
 | 
					import noteCreateService from './note_create.js';
 | 
				
			||||||
import treeService from './tree.js';
 | 
					 | 
				
			||||||
import froca from "./froca.js";
 | 
					import froca from "./froca.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// this key needs to have this value, so it's hit by the tooltip
 | 
					// this key needs to have this value, so it's hit by the tooltip
 | 
				
			||||||
@@ -188,7 +187,8 @@ function initNoteAutocomplete($el, options) {
 | 
				
			|||||||
                templateNoteId: templateNoteId
 | 
					                templateNoteId: templateNoteId
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            suggestion.notePath = treeService.getSomeNotePath(note);
 | 
					            const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
 | 
				
			||||||
 | 
					            suggestion.notePath = note.getBestNotePathString(hoistedNoteId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $el.setSelectedNotePath(suggestion.notePath);
 | 
					        $el.setSelectedNotePath(suggestion.notePath);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import froca from "./froca.js";
 | 
				
			|||||||
import utils from "./utils.js";
 | 
					import utils from "./utils.js";
 | 
				
			||||||
import attributeRenderer from "./attribute_renderer.js";
 | 
					import attributeRenderer from "./attribute_renderer.js";
 | 
				
			||||||
import noteContentRenderer from "./note_content_renderer.js";
 | 
					import noteContentRenderer from "./note_content_renderer.js";
 | 
				
			||||||
 | 
					import appContext from "../components/app_context.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setupGlobalTooltip() {
 | 
					function setupGlobalTooltip() {
 | 
				
			||||||
    $(document).on("mouseenter", "a", mouseEnterHandler);
 | 
					    $(document).on("mouseenter", "a", mouseEnterHandler);
 | 
				
			||||||
@@ -77,13 +78,14 @@ async function renderTooltip(note) {
 | 
				
			|||||||
        return '<div>Note has been deleted.</div>';
 | 
					        return '<div>Note has been deleted.</div>';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const someNotePath = treeService.getSomeNotePath(note);
 | 
					    const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
 | 
				
			||||||
 | 
					    const bestNotePath = note.getBestNotePathString(hoistedNoteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!someNotePath) {
 | 
					    if (!bestNotePath) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let content = `<h5 class="note-tooltip-title">${(await treeService.getNoteTitleWithPathAsSuffix(someNotePath)).prop('outerHTML')}</h5>`;
 | 
					    let content = `<h5 class="note-tooltip-title">${(await treeService.getNoteTitleWithPathAsSuffix(bestNotePath)).prop('outerHTML')}</h5>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
 | 
					    const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -79,14 +79,10 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
 | 
				
			|||||||
                        You can ignore this message as it is mostly harmless.`);
 | 
					                        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
 | 
					                if (bestNotePath) {
 | 
				
			||||||
                    const pathToRoot = someNotePath.split("/").reverse().slice(1);
 | 
					                    const pathToRoot = bestNotePath.reverse().slice(1);
 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if (!pathToRoot.includes("root")) {
 | 
					 | 
				
			||||||
                        pathToRoot.push('root');
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    for (const noteId of pathToRoot) {
 | 
					                    for (const noteId of pathToRoot) {
 | 
				
			||||||
                        effectivePathSegments.push(noteId);
 | 
					                        effectivePathSegments.push(noteId);
 | 
				
			||||||
@@ -109,31 +105,17 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
 | 
				
			|||||||
    else {
 | 
					    else {
 | 
				
			||||||
        const note = await froca.getNote(getNoteIdFromNotePath(notePath));
 | 
					        const note = await froca.getNote(getNoteIdFromNotePath(notePath));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const someNotePathSegments = getSomeNotePathSegments(note, hoistedNoteId);
 | 
					        const bestNotePath = note.getBestNotePath(hoistedNoteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!someNotePathSegments) {
 | 
					        if (!bestNotePath) {
 | 
				
			||||||
            throw new Error(`Did not find any path segments for ${note.toString()}, hoisted note ${hoistedNoteId}`);
 | 
					            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
 | 
					        // 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 => {
 | 
					ws.subscribeToMessages(message => {
 | 
				
			||||||
   if (message.type === 'openNote') {
 | 
					   if (message.type === 'openNote') {
 | 
				
			||||||
       appContext.tabManager.activateOrOpenNote(message.noteId);
 | 
					       appContext.tabManager.activateOrOpenNote(message.noteId);
 | 
				
			||||||
@@ -341,16 +323,6 @@ function isNotePathInAddress() {
 | 
				
			|||||||
        || (notePath === '' && !!ntxId);
 | 
					        || (notePath === '' && !!ntxId);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function parseNotePath(notePath) {
 | 
					 | 
				
			||||||
    let noteIds = notePath.split('/');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (noteIds[0] !== 'root') {
 | 
					 | 
				
			||||||
        noteIds = ['root'].concat(noteIds);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return noteIds;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function isNotePathInHiddenSubtree(notePath) {
 | 
					function isNotePathInHiddenSubtree(notePath) {
 | 
				
			||||||
    return notePath?.includes("root/_hidden");
 | 
					    return notePath?.includes("root/_hidden");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -358,8 +330,6 @@ function isNotePathInHiddenSubtree(notePath) {
 | 
				
			|||||||
export default {
 | 
					export default {
 | 
				
			||||||
    resolveNotePath,
 | 
					    resolveNotePath,
 | 
				
			||||||
    resolveNotePathToSegments,
 | 
					    resolveNotePathToSegments,
 | 
				
			||||||
    getSomeNotePath,
 | 
					 | 
				
			||||||
    getSomeNotePathSegments,
 | 
					 | 
				
			||||||
    getParentProtectedStatus,
 | 
					    getParentProtectedStatus,
 | 
				
			||||||
    getNotePath,
 | 
					    getNotePath,
 | 
				
			||||||
    getNoteIdFromNotePath,
 | 
					    getNoteIdFromNotePath,
 | 
				
			||||||
@@ -370,6 +340,5 @@ export default {
 | 
				
			|||||||
    getNoteTitleWithPathAsSuffix,
 | 
					    getNoteTitleWithPathAsSuffix,
 | 
				
			||||||
    parseNavigationStateFromAddress,
 | 
					    parseNavigationStateFromAddress,
 | 
				
			||||||
    isNotePathInAddress,
 | 
					    isNotePathInAddress,
 | 
				
			||||||
    parseNotePath,
 | 
					 | 
				
			||||||
    isNotePathInHiddenSubtree
 | 
					    isNotePathInHiddenSubtree
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
import server from "../../services/server.js";
 | 
					import server from "../../services/server.js";
 | 
				
			||||||
import froca from "../../services/froca.js";
 | 
					import froca from "../../services/froca.js";
 | 
				
			||||||
import treeService from "../../services/tree.js";
 | 
					 | 
				
			||||||
import linkService from "../../services/link.js";
 | 
					import linkService from "../../services/link.js";
 | 
				
			||||||
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
 | 
					import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
 | 
				
			||||||
import noteAutocompleteService from "../../services/note_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 SpacedUpdate from "../../services/spaced_update.js";
 | 
				
			||||||
import utils from "../../services/utils.js";
 | 
					import utils from "../../services/utils.js";
 | 
				
			||||||
import shortcutService from "../../services/shortcuts.js";
 | 
					import shortcutService from "../../services/shortcuts.js";
 | 
				
			||||||
 | 
					import appContext from "../../components/app_context.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="attr-detail">
 | 
					<div class="attr-detail">
 | 
				
			||||||
@@ -598,9 +598,10 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            const displayedResults = results.length <= DISPLAYED_NOTES ? results : results.slice(0, DISPLAYED_NOTES);
 | 
					            const displayedResults = results.length <= DISPLAYED_NOTES ? results : results.slice(0, DISPLAYED_NOTES);
 | 
				
			||||||
            const displayedNotes = await froca.getNotes(displayedResults.map(res => res.noteId));
 | 
					            const displayedNotes = await froca.getNotes(displayedResults.map(res => res.noteId));
 | 
				
			||||||
 | 
					            const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for (const note of displayedNotes) {
 | 
					            for (const note of displayedNotes) {
 | 
				
			||||||
                const notePath = treeService.getSomeNotePath(note);
 | 
					                const notePath = note.getBestNotePathString(hoistedNoteId);
 | 
				
			||||||
                const $noteLink = await linkService.createNoteLink(notePath, {showNotePath: true});
 | 
					                const $noteLink = await linkService.createNoteLink(notePath, {showNotePath: true});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                this.$relatedNotesList.append(
 | 
					                this.$relatedNotesList.append(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,6 @@ import libraryLoader from "../../services/library_loader.js";
 | 
				
			|||||||
import froca from "../../services/froca.js";
 | 
					import froca from "../../services/froca.js";
 | 
				
			||||||
import attributeRenderer from "../../services/attribute_renderer.js";
 | 
					import attributeRenderer from "../../services/attribute_renderer.js";
 | 
				
			||||||
import noteCreateService from "../../services/note_create.js";
 | 
					import noteCreateService from "../../services/note_create.js";
 | 
				
			||||||
import treeService from "../../services/tree.js";
 | 
					 | 
				
			||||||
import attributeService from "../../services/attributes.js";
 | 
					import attributeService from "../../services/attributes.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const HELP_TEXT = `
 | 
					const HELP_TEXT = `
 | 
				
			||||||
@@ -503,7 +502,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
            title: title
 | 
					            title: title
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return treeService.getSomeNotePath(note);
 | 
					        return note.getBestNotePathString();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async updateAttributeList(attributes) {
 | 
					    async updateAttributeList(attributes) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -240,7 +240,7 @@ export default class NoteRevisionsDialog extends BasicWidget {
 | 
				
			|||||||
            if (this.$content.find('span.math-tex').length > 0) {
 | 
					            if (this.$content.find('span.math-tex').length > 0) {
 | 
				
			||||||
                await libraryLoader.requireLibrary(libraryLoader.KATEX);
 | 
					                await libraryLoader.requireLibrary(libraryLoader.KATEX);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                renderMathInElement($content[0], {trust: true});
 | 
					                renderMathInElement(this.$content[0], {trust: true});
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else if (revisionItem.type === 'code' || revisionItem.type === 'mermaid') {
 | 
					        } else if (revisionItem.type === 'code' || revisionItem.type === 'mermaid') {
 | 
				
			||||||
            this.$content.html($("<pre>").text(fullNoteRevision.content));
 | 
					            this.$content.html($("<pre>").text(fullNoteRevision.content));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
import linkService from '../../services/link.js';
 | 
					import linkService from '../../services/link.js';
 | 
				
			||||||
import utils from '../../services/utils.js';
 | 
					import utils from '../../services/utils.js';
 | 
				
			||||||
import server from '../../services/server.js';
 | 
					import server from '../../services/server.js';
 | 
				
			||||||
import treeService from "../../services/tree.js";
 | 
					 | 
				
			||||||
import froca from "../../services/froca.js";
 | 
					import froca from "../../services/froca.js";
 | 
				
			||||||
import appContext from "../../components/app_context.js";
 | 
					import appContext from "../../components/app_context.js";
 | 
				
			||||||
import hoistedNoteService from "../../services/hoisted_note.js";
 | 
					import hoistedNoteService from "../../services/hoisted_note.js";
 | 
				
			||||||
@@ -108,7 +107,7 @@ export default class RecentChangesDialog extends BasicWidget {
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    const note = await froca.getNote(change.noteId);
 | 
					                    const note = await froca.getNote(change.noteId);
 | 
				
			||||||
                    const notePath = treeService.getSomeNotePath(note);
 | 
					                    const notePath = note.getBestNotePathString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (notePath) {
 | 
					                    if (notePath) {
 | 
				
			||||||
                        $noteLink = await linkService.createNoteLink(notePath, {
 | 
					                        $noteLink = await linkService.createNoteLink(notePath, {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
import libraryLoader from "../services/library_loader.js";
 | 
					import libraryLoader from "../services/library_loader.js";
 | 
				
			||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
					import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
				
			||||||
import froca from "../services/froca.js";
 | 
					import froca from "../services/froca.js";
 | 
				
			||||||
import server from "../services/server.js";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `<div class="mermaid-widget">
 | 
					const TPL = `<div class="mermaid-widget">
 | 
				
			||||||
    <style>
 | 
					    <style>
 | 
				
			||||||
@@ -74,6 +73,8 @@ export default class MermaidWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        const wheelZoomLoaded = libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM);
 | 
					        const wheelZoomLoaded = libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$errorContainer.hide();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            await this.renderSvg(async renderedSvg => {
 | 
					            await this.renderSvg(async renderedSvg => {
 | 
				
			||||||
                this.$display.html(renderedSvg);
 | 
					                this.$display.html(renderedSvg);
 | 
				
			||||||
@@ -88,8 +89,6 @@ export default class MermaidWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
                    speed: 20,
 | 
					                    speed: 20,
 | 
				
			||||||
                    zoomOnClick: false
 | 
					                    zoomOnClick: false
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					 | 
				
			||||||
                this.$errorContainer.hide();
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        } catch (e) {
 | 
					        } catch (e) {
 | 
				
			||||||
            this.$errorMessage.text(e.message);
 | 
					            this.$errorMessage.text(e.message);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -212,7 +212,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.$treeSettingsPopup.on("click", e => { e.stopPropagation(); });
 | 
					        this.$treeSettingsPopup.on("click", e => {e.stopPropagation();});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $(document).on('click', () => this.$treeSettingsPopup.hide());
 | 
					        $(document).on('click', () => this.$treeSettingsPopup.hide());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -653,7 +653,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        return noteList;
 | 
					        return noteList;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    updateNode(node) {
 | 
					    async updateNode(node) {
 | 
				
			||||||
        const note = froca.getNoteFromCache(node.data.noteId);
 | 
					        const note = froca.getNoteFromCache(node.data.noteId);
 | 
				
			||||||
        const branch = froca.getBranch(node.data.branchId);
 | 
					        const branch = froca.getBranch(node.data.branchId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -677,7 +677,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        node.title = utils.escapeHtml(title);
 | 
					        node.title = utils.escapeHtml(title);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (node.isExpanded() !== branch.isExpanded) {
 | 
					        if (node.isExpanded() !== branch.isExpanded) {
 | 
				
			||||||
            node.setExpanded(branch.isExpanded, {noEvents: true, noAnimation: true});
 | 
					            await node.setExpanded(branch.isExpanded, {noEvents: true, noAnimation: true});
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        node.renderTitle();
 | 
					        node.renderTitle();
 | 
				
			||||||
@@ -829,7 +829,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        await this.setExpandedStatusForSubtree(node, false);
 | 
					        await this.setExpandedStatusForSubtree(node, false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    collapseTreeEvent() { this.collapseTree(); }
 | 
					    collapseTreeEvent() {this.collapseTree();}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @returns {FancytreeNode|null}
 | 
					     * @returns {FancytreeNode|null}
 | 
				
			||||||
@@ -900,7 +900,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (expand) {
 | 
					                if (expand) {
 | 
				
			||||||
 | 
					                    if (!parentNode.isExpanded()) {
 | 
				
			||||||
                        await parentNode.setExpanded(true, {noAnimation: true});
 | 
					                        await parentNode.setExpanded(true, {noAnimation: true});
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // although previous line should set the expanded status, it seems to happen asynchronously,
 | 
					                    // 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
 | 
					                    // so we need to make sure it is set properly before calling updateNode which uses this flag
 | 
				
			||||||
@@ -908,7 +910,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
                    branch.isExpanded = true;
 | 
					                    branch.isExpanded = true;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                this.updateNode(parentNode);
 | 
					                await this.updateNode(parentNode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let foundChildNode = this.findChildNode(parentNode, childNoteId);
 | 
					                let foundChildNode = this.findChildNode(parentNode, childNoteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1076,10 +1078,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        const activeNode = this.getActiveNode();
 | 
					        const activeNode = this.getActiveNode();
 | 
				
			||||||
        const activeNodeFocused = activeNode && activeNode.hasFocus();
 | 
					        const activeNodeFocused = activeNode && activeNode.hasFocus();
 | 
				
			||||||
        const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
 | 
					        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 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 noteIdsToUpdate = new Set();
 | 
				
			||||||
        const noteIdsToReload = new Set();
 | 
					        const noteIdsToReload = new Set();
 | 
				
			||||||
@@ -1122,7 +1124,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') {
 | 
					            if (ecBranch.parentNoteId === '_share') {
 | 
				
			||||||
                // all shared notes have a sign in the tree, even the descendants of shared notes
 | 
					                // all shared notes have a sign in the tree, even the descendants of shared notes
 | 
				
			||||||
                noteIdsToReload.add(ecBranch.noteId);
 | 
					                noteIdsToReload.add(ecBranch.noteId);
 | 
				
			||||||
@@ -1135,6 +1144,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
            for (const node of this.getNodesByBranch(ecBranch)) {
 | 
					            for (const node of this.getNodesByBranch(ecBranch)) {
 | 
				
			||||||
                if (ecBranch.isDeleted) {
 | 
					                if (ecBranch.isDeleted) {
 | 
				
			||||||
                    if (node.isActive()) {
 | 
					                    if (node.isActive()) {
 | 
				
			||||||
 | 
					                        if (allBranchesDeleted) {
 | 
				
			||||||
                            const newActiveNode = node.getNextSibling()
 | 
					                            const newActiveNode = node.getNextSibling()
 | 
				
			||||||
                                || node.getPrevSibling()
 | 
					                                || node.getPrevSibling()
 | 
				
			||||||
                                || node.getParent();
 | 
					                                || node.getParent();
 | 
				
			||||||
@@ -1142,6 +1152,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
                            if (newActiveNode) {
 | 
					                            if (newActiveNode) {
 | 
				
			||||||
                                newActiveNode.setActive(true, {noEvents: true, noFocus: true});
 | 
					                                newActiveNode.setActive(true, {noEvents: true, noFocus: true});
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            movedActiveNode = node;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (node.getParent()) {
 | 
					                    if (node.getParent()) {
 | 
				
			||||||
@@ -1154,12 +1167,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            if (!ecBranch.isDeleted) {
 | 
					            if (!ecBranch.isDeleted) {
 | 
				
			||||||
                for (const parentNode of this.getNodesByNoteId(ecBranch.parentNoteId)) {
 | 
					                for (const parentNode of this.getNodesByNoteId(ecBranch.parentNoteId)) {
 | 
				
			||||||
 | 
					                    parentsOfAddedNodes.push(parentNode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (parentNode.isFolder() && !parentNode.isLoaded()) {
 | 
					                    if (parentNode.isFolder() && !parentNode.isLoaded()) {
 | 
				
			||||||
                        continue;
 | 
					                        continue;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    const found = (parentNode.getChildren() || []).find(child => child.data.noteId === ecBranch.noteId);
 | 
					                    const found = (parentNode.getChildren() || []).find(child => child.data.noteId === ecBranch.noteId);
 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if (!found) {
 | 
					                    if (!found) {
 | 
				
			||||||
                        // make sure it's loaded
 | 
					                        // make sure it's loaded
 | 
				
			||||||
                        await froca.getNote(ecBranch.noteId);
 | 
					                        await froca.getNote(ecBranch.noteId);
 | 
				
			||||||
@@ -1202,7 +1216,18 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        // for some reason node update cannot be in the batchUpdate() block (node is not re-rendered)
 | 
					        // for some reason node update cannot be in the batchUpdate() block (node is not re-rendered)
 | 
				
			||||||
        for (const noteId of noteIdsToUpdate) {
 | 
					        for (const noteId of noteIdsToUpdate) {
 | 
				
			||||||
            for (const node of this.getNodesByNoteId(noteId)) {
 | 
					            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
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,7 +72,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const sortedNotePaths = this.note.getSortedNotePaths(this.hoistedNoteId)
 | 
					        const sortedNotePaths = this.note.getSortedNotePathRecords(this.hoistedNoteId)
 | 
				
			||||||
            .filter(notePath => !notePath.isHidden);
 | 
					            .filter(notePath => !notePath.isHidden);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (sortedNotePaths.length > 0) {
 | 
					        if (sortedNotePaths.length > 0) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@ export default class SharedSwitchWidget extends SwitchWidget {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async switchOn() {
 | 
					    async switchOn() {
 | 
				
			||||||
        await branchService.cloneNoteToNote(this.noteId, '_share');
 | 
					        await branchService.cloneNoteToParentNote(this.noteId, '_share');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        syncService.syncNow(true);
 | 
					        syncService.syncNow(true);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,7 +53,7 @@ export default class EditableCodeTypeWidget extends TypeWidget {
 | 
				
			|||||||
            matchBrackets: true,
 | 
					            matchBrackets: true,
 | 
				
			||||||
            keyMap: options.is('vimKeymapEnabled') ? "vim": "default",
 | 
					            keyMap: options.is('vimKeymapEnabled') ? "vim": "default",
 | 
				
			||||||
            matchTags: {bothTags: true},
 | 
					            matchTags: {bothTags: true},
 | 
				
			||||||
            highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
 | 
					            highlightSelectionMatches: {showToken: false, annotateScrollbar: false},
 | 
				
			||||||
            lint: true,
 | 
					            lint: true,
 | 
				
			||||||
            gutters: ["CodeMirror-lint-markers"],
 | 
					            gutters: ["CodeMirror-lint-markers"],
 | 
				
			||||||
            lineNumbers: true,
 | 
					            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
 | 
					            // all the way to the bottom of the note. With line wrap there's no horizontal scrollbar so no problem
 | 
				
			||||||
            lineWrapping: options.is('codeLineWrapEnabled'),
 | 
					            lineWrapping: options.is('codeLineWrapEnabled'),
 | 
				
			||||||
            dragDrop: false, // with true the editor inlines dropped files which is not what we expect
 | 
					            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());
 | 
					        this.codeEditor.on('change', () => this.spacedUpdate.scheduleUpdate());
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,6 @@ import mimeTypesService from '../../services/mime_types.js';
 | 
				
			|||||||
import utils from "../../services/utils.js";
 | 
					import utils from "../../services/utils.js";
 | 
				
			||||||
import keyboardActionService from "../../services/keyboard_actions.js";
 | 
					import keyboardActionService from "../../services/keyboard_actions.js";
 | 
				
			||||||
import froca from "../../services/froca.js";
 | 
					import froca from "../../services/froca.js";
 | 
				
			||||||
import treeService from "../../services/tree.js";
 | 
					 | 
				
			||||||
import noteCreateService from "../../services/note_create.js";
 | 
					import noteCreateService from "../../services/note_create.js";
 | 
				
			||||||
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
 | 
					import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
 | 
				
			||||||
import link from "../../services/link.js";
 | 
					import link from "../../services/link.js";
 | 
				
			||||||
@@ -378,7 +377,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return treeService.getSomeNotePath(resp.note);
 | 
					        return resp.note.getBestNotePathString();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async refreshIncludedNoteEvent({noteId}) {
 | 
					    async refreshIncludedNoteEvent({noteId}) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,11 +9,11 @@ function cloneNoteToBranch(req) {
 | 
				
			|||||||
    return cloningService.cloneNoteToBranch(noteId, parentBranchId, prefix);
 | 
					    return cloningService.cloneNoteToBranch(noteId, parentBranchId, prefix);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function cloneNoteToNote(req) {
 | 
					function cloneNoteToParentNote(req) {
 | 
				
			||||||
    const {noteId, parentNoteId} = req.params;
 | 
					    const {noteId, parentNoteId} = req.params;
 | 
				
			||||||
    const {prefix} = req.body;
 | 
					    const {prefix} = req.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return cloningService.cloneNoteToNote(noteId, parentNoteId, prefix);
 | 
					    return cloningService.cloneNoteToParentNote(noteId, parentNoteId, prefix);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function cloneNoteAfter(req) {
 | 
					function cloneNoteAfter(req) {
 | 
				
			||||||
@@ -30,7 +30,7 @@ function toggleNoteInParent(req) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    cloneNoteToBranch,
 | 
					    cloneNoteToBranch,
 | 
				
			||||||
    cloneNoteToNote,
 | 
					    cloneNoteToParentNote,
 | 
				
			||||||
    cloneNoteAfter,
 | 
					    cloneNoteAfter,
 | 
				
			||||||
    toggleNoteInParent
 | 
					    toggleNoteInParent
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -129,7 +129,7 @@ function getEditedNotesOnDate(req) {
 | 
				
			|||||||
    notes = notes.map(note => note.getPojo());
 | 
					    notes = notes.map(note => note.getPojo());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const note of notes) {
 | 
					    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;
 | 
					        note.notePath = notePath ? notePath.notePath : null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -137,6 +137,32 @@ function getEditedNotesOnDate(req) {
 | 
				
			|||||||
    return notes;
 | 
					    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 = {
 | 
					module.exports = {
 | 
				
			||||||
    getNoteRevisions,
 | 
					    getNoteRevisions,
 | 
				
			||||||
    getNoteRevision,
 | 
					    getNoteRevision,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,14 +3,14 @@
 | 
				
			|||||||
const sql = require('../../services/sql');
 | 
					const sql = require('../../services/sql');
 | 
				
			||||||
const protectedSessionService = require('../../services/protected_session');
 | 
					const protectedSessionService = require('../../services/protected_session');
 | 
				
			||||||
const noteService = require('../../services/notes');
 | 
					const noteService = require('../../services/notes');
 | 
				
			||||||
const beccaService = require('../../becca/becca_service');
 | 
					const becca = require("../../becca/becca");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getRecentChanges(req) {
 | 
					function getRecentChanges(req) {
 | 
				
			||||||
    const {ancestorNoteId} = req.params;
 | 
					    const {ancestorNoteId} = req.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let recentChanges = [];
 | 
					    let recentChanges = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const noteRevisions = sql.getRows(`
 | 
					    const noteRevisionRows = sql.getRows(`
 | 
				
			||||||
        SELECT 
 | 
					        SELECT 
 | 
				
			||||||
            notes.noteId,
 | 
					            notes.noteId,
 | 
				
			||||||
            notes.isDeleted AS current_isDeleted,
 | 
					            notes.isDeleted AS current_isDeleted,
 | 
				
			||||||
@@ -24,16 +24,18 @@ function getRecentChanges(req) {
 | 
				
			|||||||
            note_revisions
 | 
					            note_revisions
 | 
				
			||||||
            JOIN notes USING(noteId)`);
 | 
					            JOIN notes USING(noteId)`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const noteRevision of noteRevisions) {
 | 
					    for (const noteRevisionRow of noteRevisionRows) {
 | 
				
			||||||
        if (beccaService.isInAncestor(noteRevision.noteId, ancestorNoteId)) {
 | 
					        const note = becca.getNote(noteRevisionRow.noteId);
 | 
				
			||||||
            recentChanges.push(noteRevision);
 | 
					
 | 
				
			||||||
 | 
					        if (note?.hasAncestor(ancestorNoteId)) {
 | 
				
			||||||
 | 
					            recentChanges.push(noteRevisionRow);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // now we need to also collect date points not represented in note revisions:
 | 
					    // now we need to also collect date points not represented in note revisions:
 | 
				
			||||||
    // 1. creation for all notes (dateCreated)
 | 
					    // 1. creation for all notes (dateCreated)
 | 
				
			||||||
    // 2. deletion for deleted notes (dateModified)
 | 
					    // 2. deletion for deleted notes (dateModified)
 | 
				
			||||||
    const notes = sql.getRows(`
 | 
					    const noteRows = sql.getRows(`
 | 
				
			||||||
            SELECT
 | 
					            SELECT
 | 
				
			||||||
                notes.noteId,
 | 
					                notes.noteId,
 | 
				
			||||||
                notes.isDeleted AS current_isDeleted,
 | 
					                notes.isDeleted AS current_isDeleted,
 | 
				
			||||||
@@ -57,9 +59,11 @@ function getRecentChanges(req) {
 | 
				
			|||||||
            FROM notes
 | 
					            FROM notes
 | 
				
			||||||
            WHERE notes.isDeleted = 1`);
 | 
					            WHERE notes.isDeleted = 1`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const note of notes) {
 | 
					    for (const noteRow of noteRows) {
 | 
				
			||||||
        if (beccaService.isInAncestor(note.noteId, ancestorNoteId)) {
 | 
					        const note = becca.getNote(noteRow.noteId);
 | 
				
			||||||
            recentChanges.push(note);
 | 
					
 | 
				
			||||||
 | 
					        if (note?.hasAncestor(ancestorNoteId)) {
 | 
				
			||||||
 | 
					            recentChanges.push(noteRow);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -125,7 +125,7 @@ function register(app) {
 | 
				
			|||||||
    apiRoute(PST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile);
 | 
					    apiRoute(PST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile);
 | 
				
			||||||
    apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch);
 | 
					    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/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);
 | 
					    apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
 | 
				
			||||||
    route(PUT, '/api/notes/:noteId/file', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware],
 | 
					    route(PUT, '/api/notes/:noteId/file', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware],
 | 
				
			||||||
        filesRoute.updateFile, apiResultHandler);
 | 
					        filesRoute.updateFile, apiResultHandler);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1 @@
 | 
				
			|||||||
module.exports = { buildDate:"", buildRevision: "9881e6de3e4966af39ec6245562dca6ac7b25eaa" };
 | 
					module.exports = { buildDate:"2023-04-17T21:40:35+02:00", buildRevision: "1d3272e9f8c27106a66227fbb580677ae5d70427" };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -83,7 +83,7 @@ const ACTION_HANDLERS = {
 | 
				
			|||||||
        let res;
 | 
					        let res;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (note.getParentBranches().length > 1) {
 | 
					        if (note.getParentBranches().length > 1) {
 | 
				
			||||||
            res = cloningService.cloneNoteToNote(note.noteId, action.targetParentNoteId);
 | 
					            res = cloningService.cloneNoteToParentNote(note.noteId, action.targetParentNoteId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId);
 | 
					            res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ const becca = require("../becca/becca");
 | 
				
			|||||||
const beccaService = require("../becca/becca_service");
 | 
					const beccaService = require("../becca/becca_service");
 | 
				
			||||||
const log = require("./log");
 | 
					const log = require("./log");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function cloneNoteToNote(noteId, parentNoteId, prefix) {
 | 
					function cloneNoteToParentNote(noteId, parentNoteId, prefix) {
 | 
				
			||||||
    const parentNote = becca.getNote(parentNoteId);
 | 
					    const parentNote = becca.getNote(parentNoteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (parentNote.type === 'search') {
 | 
					    if (parentNote.type === 'search') {
 | 
				
			||||||
@@ -19,7 +19,7 @@ function cloneNoteToNote(noteId, parentNoteId, prefix) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) {
 | 
					    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);
 | 
					    const validationResult = treeService.validateParentChild(parentNoteId, noteId);
 | 
				
			||||||
@@ -35,12 +35,12 @@ function cloneNoteToNote(noteId, parentNoteId, prefix) {
 | 
				
			|||||||
        isExpanded: 0
 | 
					        isExpanded: 0
 | 
				
			||||||
    }).save();
 | 
					    }).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 {
 | 
					    return {
 | 
				
			||||||
        success: true,
 | 
					        success: true,
 | 
				
			||||||
        branchId: branch.branchId,
 | 
					        branchId: branch.branchId,
 | 
				
			||||||
        notePath: `${beccaService.getNotePath(parentNoteId).path}/${noteId}`
 | 
					        notePath: `${parentNote.getBestNotePathString()}/${noteId}`
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -51,7 +51,7 @@ function cloneNoteToBranch(noteId, parentBranchId, prefix) {
 | 
				
			|||||||
        return { success: false, message: `Parent branch ${parentBranchId} does not exist.` };
 | 
					        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.isExpanded = true; // the new target should be expanded, so it immediately shows up to the user
 | 
				
			||||||
    parentBranch.save();
 | 
					    parentBranch.save();
 | 
				
			||||||
@@ -182,7 +182,7 @@ function isNoteDeleted(noteId) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    cloneNoteToBranch,
 | 
					    cloneNoteToBranch,
 | 
				
			||||||
    cloneNoteToNote,
 | 
					    cloneNoteToParentNote,
 | 
				
			||||||
    ensureNoteIsPresentInParent,
 | 
					    ensureNoteIsPresentInParent,
 | 
				
			||||||
    ensureNoteIsAbsentFromParent,
 | 
					    ensureNoteIsAbsentFromParent,
 | 
				
			||||||
    toggleNoteInParent,
 | 
					    toggleNoteInParent,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@ const dayjs = require("dayjs");
 | 
				
			|||||||
const htmlSanitizer = require("./html_sanitizer");
 | 
					const htmlSanitizer = require("./html_sanitizer");
 | 
				
			||||||
const ValidationError = require("../errors/validation_error");
 | 
					const ValidationError = require("../errors/validation_error");
 | 
				
			||||||
const noteTypesService = require("./note_types");
 | 
					const noteTypesService = require("./note_types");
 | 
				
			||||||
 | 
					const fs = require("fs");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** @param {BNote} parentNote */
 | 
					/** @param {BNote} parentNote */
 | 
				
			||||||
function getNewNotePosition(parentNote) {
 | 
					function getNewNotePosition(parentNote) {
 | 
				
			||||||
@@ -395,7 +396,24 @@ const imageUrlToAttachmentIdMapping = {};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async function downloadImage(noteId, imageUrl) {
 | 
					async function downloadImage(noteId, imageUrl) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        const imageBuffer = await request.getImage(imageUrl);
 | 
					        let imageBuffer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (imageUrl.toLowerCase().startsWith("file://")) {
 | 
				
			||||||
 | 
					            imageBuffer = await new Promise((res, rej) => {
 | 
				
			||||||
 | 
					                const localFilePath = imageUrl.substr("file://".length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return fs.readFile(localFilePath, (err, data) => {
 | 
				
			||||||
 | 
					                    if (err) {
 | 
				
			||||||
 | 
					                        rej(err);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        res(data);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            imageBuffer = await request.getImage(imageUrl);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const parsedUrl = url.parse(imageUrl);
 | 
					        const parsedUrl = url.parse(imageUrl);
 | 
				
			||||||
        const title = path.basename(parsedUrl.pathname);
 | 
					        const title = path.basename(parsedUrl.pathname);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,9 +22,9 @@ class NoteFlatTextExp extends Expression {
 | 
				
			|||||||
         * @param {string[]} tokens
 | 
					         * @param {string[]} tokens
 | 
				
			||||||
         * @param {string[]} path
 | 
					         * @param {string[]} path
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        function searchDownThePath(note, tokens, path) {
 | 
					        const searchDownThePath = (note, tokens, path) => {
 | 
				
			||||||
            if (tokens.length === 0) {
 | 
					            if (tokens.length === 0) {
 | 
				
			||||||
                const retPath = beccaService.getSomePath(note, path);
 | 
					                const retPath = this.getNotePath(note, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (retPath) {
 | 
					                if (retPath) {
 | 
				
			||||||
                    const noteId = retPath[retPath.length - 1];
 | 
					                    const noteId = retPath[retPath.length - 1];
 | 
				
			||||||
@@ -131,6 +131,17 @@ class NoteFlatTextExp extends Expression {
 | 
				
			|||||||
        return resultNoteSet;
 | 
					        return resultNoteSet;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getNotePath(note, path) {
 | 
				
			||||||
 | 
					        if (path.length === 0) {
 | 
				
			||||||
 | 
					            return note.getBestNotePath();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            const closestNoteId = path[0];
 | 
				
			||||||
 | 
					            const closestNoteBestNotePath = becca.getNote(closestNoteId).getBestNotePath();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return [...closestNoteBestNotePath, ...path.slice(1)];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Returns noteIds which have at least one matching tokens
 | 
					     * Returns noteIds which have at least one matching tokens
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -157,7 +157,7 @@ function findResultsWithExpression(expression, searchContext) {
 | 
				
			|||||||
    const searchResults = noteSet.notes
 | 
					    const searchResults = noteSet.notes
 | 
				
			||||||
        .filter(note => !note.isDeleted)
 | 
					        .filter(note => !note.isDeleted)
 | 
				
			||||||
        .map(note => {
 | 
					        .map(note => {
 | 
				
			||||||
            const notePathArray = executionContext.noteIdToNotePath[note.noteId] || beccaService.getSomePath(note);
 | 
					            const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!notePathArray) {
 | 
					            if (!notePathArray) {
 | 
				
			||||||
                throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`);
 | 
					                throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,18 @@ const protectedSessionService = require('./protected_session');
 | 
				
			|||||||
const becca = require("../becca/becca");
 | 
					const becca = require("../becca/becca");
 | 
				
			||||||
const AbstractBeccaEntity = require("../becca/entities/abstract_becca_entity");
 | 
					const AbstractBeccaEntity = require("../becca/entities/abstract_becca_entity");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const env = require('./env');
 | 
				
			||||||
 | 
					if (env.isDev()) {
 | 
				
			||||||
 | 
					    const chokidar = require('chokidar');
 | 
				
			||||||
 | 
					    const debounce = require('debounce');
 | 
				
			||||||
 | 
					    const debouncedReloadFrontend = debounce(reloadFrontend, 200);
 | 
				
			||||||
 | 
					    chokidar
 | 
				
			||||||
 | 
					        .watch('src/public')
 | 
				
			||||||
 | 
					        .on('add', debouncedReloadFrontend)
 | 
				
			||||||
 | 
					        .on('change', debouncedReloadFrontend)
 | 
				
			||||||
 | 
					        .on('unlink', debouncedReloadFrontend);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let webSocketServer;
 | 
					let webSocketServer;
 | 
				
			||||||
let lastSyncedPush = null;
 | 
					let lastSyncedPush = 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'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@@ -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'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@@ -4,13 +4,15 @@ const assetPath = require('./src/services/asset_path');
 | 
				
			|||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    mode: 'production',
 | 
					    mode: 'production',
 | 
				
			||||||
    entry: {
 | 
					    entry: {
 | 
				
			||||||
 | 
					        setup: './src/public/app/setup.js',
 | 
				
			||||||
        mobile: './src/public/app/mobile.js',
 | 
					        mobile: './src/public/app/mobile.js',
 | 
				
			||||||
 | 
					        desktop: './src/public/app/desktop.js',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    output: {
 | 
					    output: {
 | 
				
			||||||
        publicPath: `${assetPath}/app-dist/`,
 | 
					        publicPath: `${assetPath}/app-dist/`,
 | 
				
			||||||
        path: path.resolve(__dirname, 'src/public/app-dist'),
 | 
					        path: path.resolve(__dirname, 'src/public/app-dist'),
 | 
				
			||||||
        filename: 'mobile.js'
 | 
					        filename: '[name].js',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    devtool: 'source-map',
 | 
					    devtool: 'source-map',
 | 
				
			||||||
    target: 'electron-renderer'
 | 
					    target: 'electron-renderer',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
		Reference in New Issue
	
	Block a user