diff --git a/.github/workflows_old/docker.yaml b/.github/workflows_old/docker.yaml index c2db4becf..39adce00a 100644 --- a/.github/workflows_old/docker.yaml +++ b/.github/workflows_old/docker.yaml @@ -1,53 +1,53 @@ name: Publish Docker image on: - push: - tags: [v*] + push: + tags: [v*] jobs: - push_to_registries: - name: Push Docker image to multiple registries - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - name: Docker meta - id: meta - uses: docker/metadata-action@v3 - with: - images: | - zadam/trilium - ghcr.io/zadam/trilium - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}}-latest - type=match,pattern=(\d+.\d+).\d+\-beta,enable=${{ endsWith(github.ref, 'beta') }},group=1,suffix=-latest - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - with: - install: true - - name: Log in to Docker Hub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Log in to GitHub Docker Registry - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Create server-package.json - run: cat package.json | grep -v electron > server-package.json - - name: Build and Push - uses: docker/build-push-action@v2.7.0 - with: - context: . - platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 - push: true - cache-from: type=registry,ref=zadam/trilium:buildcache - cache-to: type=registry,ref=zadam/trilium:buildcache,mode=max - tags: ${{ steps.meta.outputs.tags }} + push_to_registries: + name: Push Docker image to multiple registries + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + images: | + zadam/trilium + ghcr.io/zadam/trilium + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}}-latest + type=match,pattern=(\d+.\d+).\d+\-beta,enable=${{ endsWith(github.ref, 'beta') }},group=1,suffix=-latest + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + with: + install: true + - name: Log in to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Log in to GitHub Docker Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Create server-package.json + run: cat package.json | grep -v electron > server-package.json + - name: Build and Push + uses: docker/build-push-action@v2.7.0 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 + push: true + cache-from: type=registry,ref=zadam/trilium:buildcache + cache-to: type=registry,ref=zadam/trilium:buildcache,mode=max + tags: ${{ steps.meta.outputs.tags }} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index cf619af51..7fc3d9975 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,3 @@ { - "recommendations": [ - "lokalise.i18n-ally", - "editorconfig.editorconfig" - ] -} \ No newline at end of file + "recommendations": ["lokalise.i18n-ally", "editorconfig.editorconfig"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 07248c0fb..f8d4780a1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,24 +1,22 @@ { - "version": "0.2.0", - "configurations": [ - // nodemon should be installed globally, use npm i -g nodemon - { - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "name": "nodemon start-server", - "program": "${workspaceFolder}/src/www", - "request": "launch", - "restart": true, - "runtimeExecutable": "nodemon", - "env": { - "TRILIUM_ENV": "dev", - "TRILIUM_DATA_DIR": "./data" - }, - "skipFiles": [ - "/**" - ], - "type": "node", - "outputCapture": "std", - }, - ] -} \ No newline at end of file + "version": "0.2.0", + "configurations": [ + // nodemon should be installed globally, use npm i -g nodemon + { + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "name": "nodemon start-server", + "program": "${workspaceFolder}/src/www", + "request": "launch", + "restart": true, + "runtimeExecutable": "nodemon", + "env": { + "TRILIUM_ENV": "dev", + "TRILIUM_DATA_DIR": "./data" + }, + "skipFiles": ["/**"], + "type": "node", + "outputCapture": "std" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 1a141254a..a9c07eb28 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,27 +1,22 @@ { - "editor.formatOnSave": false, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "files.eol": "\n", - "typescript.tsdk": "node_modules/typescript/lib", - "i18n-ally.sourceLanguage": "en", - "i18n-ally.keystyle": "nested", - "i18n-ally.localesPaths": [ - "./src/public/translations", - "./translations" - ], - "[jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features" - }, - "[javascript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - }, - "[typescript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - }, - "github-actions.workflows.pinned.workflows": [ - ".github/workflows/nightly.yml" - ], - "[css]": { - "editor.defaultFormatter": "vscode.css-language-features" - }, + "editor.formatOnSave": false, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "files.eol": "\n", + "typescript.tsdk": "node_modules/typescript/lib", + "i18n-ally.sourceLanguage": "en", + "i18n-ally.keystyle": "nested", + "i18n-ally.localesPaths": ["./src/public/translations", "./translations"], + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "github-actions.workflows.pinned.workflows": [".github/workflows/nightly.yml"], + "[css]": { + "editor.defaultFormatter": "vscode.css-language-features" + } } diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets index 792d1501f..77b251a4a 100644 --- a/.vscode/snippets.code-snippets +++ b/.vscode/snippets.code-snippets @@ -1,26 +1,24 @@ { - // Place your Notes workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and - // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope - // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is - // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: - // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. - // Placeholders with the same ids are connected. - // Example: - // "Print to console": { - // "scope": "javascript,typescript", - // "prefix": "log", - // "body": [ - // "console.log('$1');", - // "$2" - // ], - // "description": "Log output to console" - // } + // Place your Notes workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } - "JQuery HTMLElement field": { - "scope": "typescript", - "prefix": "jqf", - "body": [ - "private $${1:name}!: JQuery;" - ] - } -} \ No newline at end of file + "JQuery HTMLElement field": { + "scope": "typescript", + "prefix": "jqf", + "body": ["private $${1:name}!: JQuery;"] + } +} diff --git a/bin/copy-dist.ts b/bin/copy-dist.ts index 69ca710b7..d594dcdb9 100644 --- a/bin/copy-dist.ts +++ b/bin/copy-dist.ts @@ -8,108 +8,108 @@ const DEST_DIR_NODE_MODULES = path.join(DEST_DIR, "node_modules"); const VERBOSE = process.env.VERBOSE; function log(...args) { - if (VERBOSE) { - console.log(args); - } + if (VERBOSE) { + console.log(args); + } } async function copyNodeModuleFileOrFolder(source: string) { - const adjustedSource = source.substring(13); - const destination = path.join(DEST_DIR_NODE_MODULES, adjustedSource); + const adjustedSource = source.substring(13); + const destination = path.join(DEST_DIR_NODE_MODULES, adjustedSource); - log(`Copying ${source} to ${destination}`); - await fs.ensureDir(path.dirname(destination)); - await fs.copy(source, destination); + log(`Copying ${source} to ${destination}`); + await fs.ensureDir(path.dirname(destination)); + await fs.copy(source, destination); } const copy = async () => { - for (const srcFile of fs.readdirSync("build")) { - const destFile = path.join(DEST_DIR, path.basename(srcFile)); - log(`Copying source ${srcFile} -> ${destFile}.`); - fs.copySync(path.join("build", srcFile), destFile, { recursive: true }); - } + for (const srcFile of fs.readdirSync("build")) { + const destFile = path.join(DEST_DIR, path.basename(srcFile)); + log(`Copying source ${srcFile} -> ${destFile}.`); + fs.copySync(path.join("build", srcFile), destFile, { recursive: true }); + } - const filesToCopy = ["config-sample.ini", "tsconfig.webpack.json"]; - for (const file of filesToCopy) { - log(`Copying ${file}`); - await fs.copy(file, path.join(DEST_DIR, file)); - } + const filesToCopy = ["config-sample.ini", "tsconfig.webpack.json"]; + for (const file of filesToCopy) { + log(`Copying ${file}`); + await fs.copy(file, path.join(DEST_DIR, file)); + } - const dirsToCopy = ["images", "libraries", "translations", "db"]; - for (const dir of dirsToCopy) { - log(`Copying ${dir}`); - await fs.copy(dir, path.join(DEST_DIR, dir)); - } + const dirsToCopy = ["images", "libraries", "translations", "db"]; + for (const dir of dirsToCopy) { + log(`Copying ${dir}`); + await fs.copy(dir, path.join(DEST_DIR, dir)); + } - const srcDirsToCopy = ["./src/public", "./src/views", "./build"]; - for (const dir of srcDirsToCopy) { - log(`Copying ${dir}`); - await fs.copy(dir, path.join(DEST_DIR_SRC, path.basename(dir))); - } + const srcDirsToCopy = ["./src/public", "./src/views", "./build"]; + for (const dir of srcDirsToCopy) { + log(`Copying ${dir}`); + await fs.copy(dir, path.join(DEST_DIR_SRC, path.basename(dir))); + } - /** - * Directories to be copied relative to the project root into /src/public/app-dist. - */ - const publicDirsToCopy = [ "./src/public/app/doc_notes" ]; - const PUBLIC_DIR = path.join(DEST_DIR, "src", "public", "app-dist"); - for (const dir of publicDirsToCopy) { - await fs.copy(dir, path.join(PUBLIC_DIR, path.basename(dir))); - } + /** + * Directories to be copied relative to the project root into /src/public/app-dist. + */ + const publicDirsToCopy = ["./src/public/app/doc_notes"]; + const PUBLIC_DIR = path.join(DEST_DIR, "src", "public", "app-dist"); + for (const dir of publicDirsToCopy) { + await fs.copy(dir, path.join(PUBLIC_DIR, path.basename(dir))); + } - const nodeModulesFile = [ - "node_modules/react/umd/react.production.min.js", - "node_modules/react/umd/react.development.js", - "node_modules/react-dom/umd/react-dom.production.min.js", - "node_modules/react-dom/umd/react-dom.development.js", - "node_modules/katex/dist/katex.min.js", - "node_modules/katex/dist/contrib/mhchem.min.js", - "node_modules/katex/dist/contrib/auto-render.min.js", - "node_modules/@highlightjs/cdn-assets/highlight.min.js", - "node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs", - ]; + const nodeModulesFile = [ + "node_modules/react/umd/react.production.min.js", + "node_modules/react/umd/react.development.js", + "node_modules/react-dom/umd/react-dom.production.min.js", + "node_modules/react-dom/umd/react-dom.development.js", + "node_modules/katex/dist/katex.min.js", + "node_modules/katex/dist/contrib/mhchem.min.js", + "node_modules/katex/dist/contrib/auto-render.min.js", + "node_modules/@highlightjs/cdn-assets/highlight.min.js", + "node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs" + ]; - for (const file of nodeModulesFile) { - await copyNodeModuleFileOrFolder(file); - } + for (const file of nodeModulesFile) { + await copyNodeModuleFileOrFolder(file); + } - const nodeModulesFolder = [ - "node_modules/@excalidraw/excalidraw/dist/", - "node_modules/katex/dist/", - "node_modules/dayjs/", - "node_modules/force-graph/dist/", - "node_modules/boxicons/css/", - "node_modules/boxicons/fonts/", - "node_modules/mermaid/dist/", - "node_modules/jquery/dist/", - "node_modules/jquery-hotkeys/", - "node_modules/print-this/", - "node_modules/split.js/dist/", - "node_modules/panzoom/dist/", - "node_modules/i18next/", - "node_modules/i18next-http-backend/", - "node_modules/eslint/bin/", - "node_modules/jsplumb/dist/", - "node_modules/vanilla-js-wheel-zoom/dist/", - "node_modules/mark.js/dist/", - "node_modules/knockout/build/output/", - "node_modules/normalize.css/", - "node_modules/jquery.fancytree/dist/", - "node_modules/bootstrap/dist/", - "node_modules/autocomplete.js/dist/", - "node_modules/codemirror/lib/", - "node_modules/codemirror/addon/", - "node_modules/codemirror/mode/", - "node_modules/codemirror/keymap/", - "node_modules/mind-elixir/dist/", - "node_modules/@highlightjs/cdn-assets/languages", - "node_modules/@highlightjs/cdn-assets/styles" - ]; + const nodeModulesFolder = [ + "node_modules/@excalidraw/excalidraw/dist/", + "node_modules/katex/dist/", + "node_modules/dayjs/", + "node_modules/force-graph/dist/", + "node_modules/boxicons/css/", + "node_modules/boxicons/fonts/", + "node_modules/mermaid/dist/", + "node_modules/jquery/dist/", + "node_modules/jquery-hotkeys/", + "node_modules/print-this/", + "node_modules/split.js/dist/", + "node_modules/panzoom/dist/", + "node_modules/i18next/", + "node_modules/i18next-http-backend/", + "node_modules/eslint/bin/", + "node_modules/jsplumb/dist/", + "node_modules/vanilla-js-wheel-zoom/dist/", + "node_modules/mark.js/dist/", + "node_modules/knockout/build/output/", + "node_modules/normalize.css/", + "node_modules/jquery.fancytree/dist/", + "node_modules/bootstrap/dist/", + "node_modules/autocomplete.js/dist/", + "node_modules/codemirror/lib/", + "node_modules/codemirror/addon/", + "node_modules/codemirror/mode/", + "node_modules/codemirror/keymap/", + "node_modules/mind-elixir/dist/", + "node_modules/@highlightjs/cdn-assets/languages", + "node_modules/@highlightjs/cdn-assets/styles" + ]; - for (const folder of nodeModulesFolder) { - await copyNodeModuleFileOrFolder(folder); - } + for (const folder of nodeModulesFolder) { + await copyNodeModuleFileOrFolder(folder); + } }; copy() - .then(() => console.log("Copying complete!")) - .catch((err) => console.error("Error during copy:", err)); + .then(() => console.log("Copying complete!")) + .catch((err) => console.error("Error during copy:", err)); diff --git a/bin/create-anonymization-script.ts b/bin/create-anonymization-script.ts index 5cf991b22..ff462ec5e 100755 --- a/bin/create-anonymization-script.ts +++ b/bin/create-anonymization-script.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import anonymizationService from '../src/services/anonymization.js'; -import fs from 'fs'; -import path from 'path'; +import anonymizationService from "../src/services/anonymization.js"; +import fs from "fs"; +import path from "path"; -fs.writeFileSync(path.resolve(__dirname, 'tpl', 'anonymize-database.sql'), anonymizationService.getFullAnonymizationScript()); +fs.writeFileSync(path.resolve(__dirname, "tpl", "anonymize-database.sql"), anonymizationService.getFullAnonymizationScript()); diff --git a/bin/docs/assets/v0.63.6/app-dist/share.js b/bin/docs/assets/v0.63.6/app-dist/share.js index 03ad92515..3c3282497 100644 --- a/bin/docs/assets/v0.63.6/app-dist/share.js +++ b/bin/docs/assets/v0.63.6/app-dist/share.js @@ -13,11 +13,15 @@ async function fetchNote(noteId = null) { return await resp.json(); } -document.addEventListener('DOMContentLoaded', () => { - const toggleMenuButton = document.getElementById('toggleMenuButton'); - const layout = document.getElementById('layout'); +document.addEventListener( + "DOMContentLoaded", + () => { + const toggleMenuButton = document.getElementById("toggleMenuButton"); + const layout = document.getElementById("layout"); - if (toggleMenuButton && layout) { - toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu')); - } -}, false); + if (toggleMenuButton && layout) { + toggleMenuButton.addEventListener("click", () => layout.classList.toggle("showMenu")); + } + }, + false +); diff --git a/bin/docs/assets/v0.63.6/libraries/ckeditor/ckeditor-content.css b/bin/docs/assets/v0.63.6/libraries/ckeditor/ckeditor-content.css index 47274e5f4..41d0ab466 100644 --- a/bin/docs/assets/v0.63.6/libraries/ckeditor/ckeditor-content.css +++ b/bin/docs/assets/v0.63.6/libraries/ckeditor/ckeditor-content.css @@ -1,6 +1,8 @@ /* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */ -.printed-content .ck-widget__selection-handle, .printed-content .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */ +.printed-content .ck-widget__selection-handle, +.printed-content .ck-widget__type-around { + /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */ display: none; } @@ -59,7 +61,7 @@ .ck-content .table table td, .ck-content .table table th { min-width: 2em; - padding: .4em; + padding: 0.4em; border: 1px solid hsl(0, 0%, 75%); } /* @ckeditor/ckeditor5-table/theme/table.css */ @@ -83,8 +85,8 @@ text-align: center; color: var(--ck-color-selector-caption-text); background-color: var(--ck-color-selector-caption-background); - padding: .6em; - font-size: .75em; + padding: 0.6em; + font-size: 0.75em; outline-offset: -1px; } /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ @@ -98,7 +100,7 @@ } /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ .ck-content .page-break::after { - content: ''; + content: ""; position: absolute; border-bottom: 2px dashed hsl(0, 0%, 77%); width: 100%; @@ -107,7 +109,7 @@ .ck-content .page-break__label { position: relative; z-index: 1; - padding: .3em .6em; + padding: 0.3em 0.6em; display: block; text-transform: uppercase; border: 1px solid hsl(0, 0%, 77%); @@ -158,7 +160,7 @@ margin-left: 0; } /* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content[dir=rtl] .todo-list .todo-list__label > input { +.ck-content[dir="rtl"] .todo-list .todo-list__label > input { left: 0; margin-right: 0; right: -25px; @@ -169,7 +171,7 @@ display: block; position: absolute; box-sizing: border-box; - content: ''; + content: ""; width: 100%; height: 100%; border: 1px solid hsl(0, 0%, 20%); @@ -182,14 +184,14 @@ position: absolute; box-sizing: content-box; pointer-events: none; - content: ''; - left: calc( var(--ck-todo-list-checkmark-size) / 3 ); - top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); + content: ""; + left: calc(var(--ck-todo-list-checkmark-size) / 3); + top: calc(var(--ck-todo-list-checkmark-size) / 5.3); + width: calc(var(--ck-todo-list-checkmark-size) / 5.3); + height: calc(var(--ck-todo-list-checkmark-size) / 2.6); border-style: solid; border-color: transparent; - border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; + border-width: 0 calc(var(--ck-todo-list-checkmark-size) / 8) calc(var(--ck-todo-list-checkmark-size) / 8) 0; transform: rotate(45deg); } /* @ckeditor/ckeditor5-list/theme/todolist.css */ @@ -206,20 +208,21 @@ vertical-align: middle; } /* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] { +.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type="checkbox"] { position: absolute; } /* @ckeditor/ckeditor5-list/theme/todolist.css */ .ck-editor__editable.ck-content .todo-list .todo-list__label > input, -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input { +.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input { cursor: pointer; } /* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before, .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input:hover::before { +.ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before, +.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input:hover::before { box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1); } /* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input { +.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input { -webkit-appearance: none; display: inline-block; position: relative; @@ -233,18 +236,18 @@ margin-left: 0; } /* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content[dir=rtl] .todo-list .todo-list__label > span[contenteditable=false] > input { +.ck-editor__editable.ck-content[dir="rtl"] .todo-list .todo-list__label > span[contenteditable="false"] > input { left: 0; margin-right: 0; right: -25px; margin-left: -15px; } /* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::before { +.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input::before { display: block; position: absolute; box-sizing: border-box; - content: ''; + content: ""; width: 100%; height: 100%; border: 1px solid hsl(0, 0%, 20%); @@ -252,32 +255,32 @@ transition: 250ms ease-in-out box-shadow; } /* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::after { +.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input::after { display: block; position: absolute; box-sizing: content-box; pointer-events: none; - content: ''; - left: calc( var(--ck-todo-list-checkmark-size) / 3 ); - top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); + content: ""; + left: calc(var(--ck-todo-list-checkmark-size) / 3); + top: calc(var(--ck-todo-list-checkmark-size) / 5.3); + width: calc(var(--ck-todo-list-checkmark-size) / 5.3); + height: calc(var(--ck-todo-list-checkmark-size) / 2.6); border-style: solid; border-color: transparent; - border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; + border-width: 0 calc(var(--ck-todo-list-checkmark-size) / 8) calc(var(--ck-todo-list-checkmark-size) / 8) 0; transform: rotate(45deg); } /* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::before { +.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input[checked]::before { background: hsl(126, 64%, 41%); border-color: hsl(126, 64%, 41%); } /* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::after { +.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input[checked]::after { border-color: hsl(0, 0%, 100%); } /* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] { +.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type="checkbox"] { position: absolute; } /* @ckeditor/ckeditor5-list/theme/list.css */ @@ -379,8 +382,8 @@ word-break: break-word; color: var(--ck-color-image-caption-text); background-color: var(--ck-color-image-caption-background); - padding: .6em; - font-size: .75em; + padding: 0.6em; + font-size: 0.75em; outline-offset: -1px; } /* @ckeditor/ckeditor5-image/theme/imagestyle.css */ @@ -488,16 +491,16 @@ /* @ckeditor/ckeditor5-basic-styles/theme/code.css */ .ck-content code { background-color: hsla(0, 0%, 78%, 0.3); - padding: .15em; + padding: 0.15em; border-radius: 2px; } /* @ckeditor/ckeditor5-font/theme/fontsize.css */ .ck-content .text-tiny { - font-size: .7em; + font-size: 0.7em; } /* @ckeditor/ckeditor5-font/theme/fontsize.css */ .ck-content .text-small { - font-size: .85em; + font-size: 0.85em; } /* @ckeditor/ckeditor5-font/theme/fontsize.css */ .ck-content .text-big { diff --git a/bin/docs/assets/v0.63.6/libraries/normalize.min.css b/bin/docs/assets/v0.63.6/libraries/normalize.min.css index 87aa24dc5..3d24d023d 100644 --- a/bin/docs/assets/v0.63.6/libraries/normalize.min.css +++ b/bin/docs/assets/v0.63.6/libraries/normalize.min.css @@ -1,2 +1,148 @@ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none} -/*# sourceMappingURL=normalize.min.css.map */ \ No newline at end of file +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ +html { + line-height: 1.15; + -webkit-text-size-adjust: 100%; +} +body { + margin: 0; +} +main { + display: block; +} +h1 { + font-size: 2em; + margin: 0.67em 0; +} +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} +pre { + font-family: monospace, monospace; + font-size: 1em; +} +a { + background-color: transparent; +} +abbr[title] { + border-bottom: none; + text-decoration: underline; + text-decoration: underline dotted; +} +b, +strong { + font-weight: bolder; +} +code, +kbd, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +img { + border-style: none; +} +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + font-size: 100%; + line-height: 1.15; + margin: 0; +} +button, +input { + overflow: visible; +} +button, +select { + text-transform: none; +} +[type="button"], +[type="reset"], +[type="submit"], +button { + -webkit-appearance: button; +} +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner, +button::-moz-focus-inner { + border-style: none; + padding: 0; +} +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring, +button:-moz-focusring { + outline: 1px dotted ButtonText; +} +fieldset { + padding: 0.35em 0.75em 0.625em; +} +legend { + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal; +} +progress { + vertical-align: baseline; +} +textarea { + overflow: auto; +} +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; + padding: 0; +} +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} +[type="search"] { + -webkit-appearance: textfield; + outline-offset: -2px; +} +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} +details { + display: block; +} +summary { + display: list-item; +} +template { + display: none; +} +[hidden] { + display: none; +} +/*# sourceMappingURL=normalize.min.css.map */ diff --git a/bin/docs/assets/v0.63.6/stylesheets/share.css b/bin/docs/assets/v0.63.6/stylesheets/share.css index 718f8594c..457898d8f 100644 --- a/bin/docs/assets/v0.63.6/stylesheets/share.css +++ b/bin/docs/assets/v0.63.6/stylesheets/share.css @@ -1,5 +1,5 @@ body { - font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; + font-family: "Lucida Grande", "Lucida Sans Unicode", arial, sans-serif; line-height: 1.5; } diff --git a/bin/update-build-info.ts b/bin/update-build-info.ts index 25356c468..4635bb8ea 100644 --- a/bin/update-build-info.ts +++ b/bin/update-build-info.ts @@ -4,15 +4,11 @@ import fs from "fs"; function getBuildDate() { const now = new Date(); now.setMilliseconds(0); - return now - .toISOString() - .replace(".000", ""); + return now.toISOString().replace(".000", ""); } function getGitRevision() { - return child_process.execSync('git log -1 --format="%H"') - .toString("utf-8") - .trimEnd(); + return child_process.execSync('git log -1 --format="%H"').toString("utf-8").trimEnd(); } const output = `\ diff --git a/bin/update-nightly-version.ts b/bin/update-nightly-version.ts index c700e3b22..b08231dda 100644 --- a/bin/update-nightly-version.ts +++ b/bin/update-nightly-version.ts @@ -20,11 +20,7 @@ function processVersion(version) { version = version.replace("-beta", ""); // Add the nightly suffix, plus the date. - const referenceDate = new Date() - .toISOString() - .substring(2, 19) - .replace(/[-:]*/g, "") - .replace("T", "-"); + const referenceDate = new Date().toISOString().substring(2, 19).replace(/[-:]*/g, "").replace("T", "-"); version = `${version}-test-${referenceDate}`; return version; diff --git a/bin/watch-dist.ts b/bin/watch-dist.ts index 9faa8d7a2..8c76034af 100644 --- a/bin/watch-dist.ts +++ b/bin/watch-dist.ts @@ -12,7 +12,5 @@ function onFileChanged(sourceFile: string) { const sourceDir = "src/public"; -chokidar - .watch(sourceDir) - .on("change", onFileChanged); +chokidar.watch(sourceDir).on("change", onFileChanged); console.log(`Watching for changes to ${sourceDir}...`); diff --git a/db/migrations/0216__move_content_into_blobs.js b/db/migrations/0216__move_content_into_blobs.js index eea287b21..53586d0e3 100644 --- a/db/migrations/0216__move_content_into_blobs.js +++ b/db/migrations/0216__move_content_into_blobs.js @@ -1,6 +1,6 @@ module.exports = () => { - const sql = require('../../src/services/sql'); - const utils = require('../../src/services/utils'); + const sql = require("../../src/services/sql"); + const utils = require("../../src/services/utils"); const existingBlobIds = new Set(); @@ -11,7 +11,7 @@ module.exports = () => { if (!existingBlobIds.has(blobId)) { existingBlobIds.add(blobId); - sql.insert('blobs', { + sql.insert("blobs", { blobId, content: row.content, dateModified: row.dateModified, @@ -24,7 +24,7 @@ module.exports = () => { sql.execute("DELETE FROM entity_changes WHERE entityName = 'note_contents' AND entityId = ?", [row.noteId]); } - sql.execute('UPDATE notes SET blobId = ? WHERE noteId = ?', [blobId, row.noteId]); + sql.execute("UPDATE notes SET blobId = ? WHERE noteId = ?", [blobId, row.noteId]); } for (const noteRevisionId of sql.getColumn(`SELECT noteRevisionId FROM note_revision_contents`)) { @@ -34,7 +34,7 @@ module.exports = () => { if (!existingBlobIds.has(blobId)) { existingBlobIds.add(blobId); - sql.insert('blobs', { + sql.insert("blobs", { blobId, content: row.content, dateModified: row.utcDateModified, @@ -47,7 +47,7 @@ module.exports = () => { sql.execute("DELETE FROM entity_changes WHERE entityName = 'note_revision_contents' AND entityId = ?", [row.noteId]); } - sql.execute('UPDATE note_revisions SET blobId = ? WHERE noteRevisionId = ?', [blobId, row.noteRevisionId]); + sql.execute("UPDATE note_revisions SET blobId = ? WHERE noteRevisionId = ?", [blobId, row.noteRevisionId]); } const notesWithoutBlobIds = sql.getColumn("SELECT noteId FROM notes WHERE blobId IS NULL"); diff --git a/db/migrations/0220__migrate_images_to_attachments.js b/db/migrations/0220__migrate_images_to_attachments.js index f88894820..a93e2028d 100644 --- a/db/migrations/0220__migrate_images_to_attachments.js +++ b/db/migrations/0220__migrate_images_to_attachments.js @@ -1,9 +1,9 @@ module.exports = () => { - const beccaLoader = require('../../src/becca/becca_loader'); - const becca = require('../../src/becca/becca'); - const cls = require('../../src/services/cls'); - const log = require('../../src/services/log'); - const sql = require('../../src/services/sql'); + const beccaLoader = require("../../src/becca/becca_loader"); + const becca = require("../../src/becca/becca"); + const cls = require("../../src/services/cls"); + const log = require("../../src/services/log"); + const sql = require("../../src/services/sql"); cls.init(() => { // emergency disabling of image compression since it appears to make problems in migration to 0.61 @@ -18,8 +18,7 @@ module.exports = () => { if (attachment) { log.info(`Auto-converted note '${note.noteId}' into attachment '${attachment.attachmentId}'.`); } - } - catch (e) { + } catch (e) { log.error(`Cannot convert note '${note.noteId}' to attachment: ${e.message} ${e.stack}`); } } diff --git a/docker_healthcheck.ts b/docker_healthcheck.ts index c11c853a4..2a2635db2 100755 --- a/docker_healthcheck.ts +++ b/docker_healthcheck.ts @@ -1,8 +1,8 @@ import http from "http"; import ini from "ini"; import fs from "fs"; -import dataDir from './src/services/data_dir.js'; -const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, 'utf-8')); +import dataDir from "./src/services/data_dir.js"; +const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8")); if (config.Network.https) { // built-in TLS (terminated by trilium) is not supported yet, PRs are welcome @@ -10,12 +10,12 @@ if (config.Network.https) { process.exit(0); } -import port from './src/services/port.js'; -import host from './src/services/host.js'; +import port from "./src/services/port.js"; +import host from "./src/services/host.js"; const options: http.RequestOptions = { timeout: 2000 }; -const callback: (res: http.IncomingMessage) => void = res => { +const callback: (res: http.IncomingMessage) => void = (res) => { console.log(`STATUS: ${res.statusCode}`); if (res.statusCode === 200) { process.exit(0); @@ -26,16 +26,18 @@ const callback: (res: http.IncomingMessage) => void = res => { let request; -if (port !== 0) { // TCP socket. +if (port !== 0) { + // TCP socket. const url = `http://${host}:${port}/api/health-check`; request = http.request(url, options, callback); -} else { // Unix socket. +} else { + // Unix socket. options.socketPath = host; - options.path = '/api/health-check'; + options.path = "/api/health-check"; request = http.request(options, callback); } -request.on("error", err => { +request.on("error", (err) => { console.log("ERROR"); process.exit(1); }); diff --git a/dump-db/dump-db.ts b/dump-db/dump-db.ts index 44273a726..e00f1c615 100755 --- a/dump-db/dump-db.ts +++ b/dump-db/dump-db.ts @@ -1,33 +1,37 @@ #!/usr/bin/env node -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; -import dumpService from './inc/dump.js'; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +import dumpService from "./inc/dump.js"; yargs(hideBin(process.argv)) - .command('$0 ', 'dump the contents of document.db into the target directory', (yargs) => { - return yargs - .option('path_to_document', { alias: 'p', describe: 'path to the document.db', type: 'string', demandOption: true }) - .option('target_directory', { alias: 't', describe: 'path of the directory into which the notes should be dumped', type: 'string', demandOption: true }); - }, (argv) => { - try { - dumpService.dumpDocument(argv.path_to_document, argv.target_directory, { - includeDeleted: argv.includeDeleted, - password: argv.password - }); - } - catch (e) { - console.error(`Unrecoverable error:`, e); - process.exit(1); + .command( + "$0 ", + "dump the contents of document.db into the target directory", + (yargs) => { + return yargs + .option("path_to_document", { alias: "p", describe: "path to the document.db", type: "string", demandOption: true }) + .option("target_directory", { alias: "t", describe: "path of the directory into which the notes should be dumped", type: "string", demandOption: true }); + }, + (argv) => { + try { + dumpService.dumpDocument(argv.path_to_document, argv.target_directory, { + includeDeleted: argv.includeDeleted, + password: argv.password + }); + } catch (e) { + console.error(`Unrecoverable error:`, e); + process.exit(1); + } } + ) + .option("password", { + type: "string", + description: "Set password to be able to decrypt protected notes." }) - .option('password', { - type: 'string', - description: 'Set password to be able to decrypt protected notes.' - }) - .option('include-deleted', { - type: 'boolean', + .option("include-deleted", { + type: "boolean", default: false, - description: 'If set to true, dump also deleted notes.' + description: "If set to true, dump also deleted notes." }) .parse(); diff --git a/dump-db/inc/data_key.ts b/dump-db/inc/data_key.ts index a2781c8e1..35594926a 100644 --- a/dump-db/inc/data_key.ts +++ b/dump-db/inc/data_key.ts @@ -1,6 +1,6 @@ -import crypto from 'crypto'; -import sql from './sql.js'; -import decryptService from './decrypt.js'; +import crypto from "crypto"; +import sql from "./sql.js"; +import decryptService from "./decrypt.js"; function getDataKey(password: any) { if (!password) { @@ -10,26 +10,24 @@ function getDataKey(password: any) { try { const passwordDerivedKey = getPasswordDerivedKey(password); - const encryptedDataKey = getOption('encryptedDataKey'); + const encryptedDataKey = getOption("encryptedDataKey"); const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey, 16); return decryptedDataKey; - } - catch (e: any) { + } catch (e: any) { throw new Error(`Cannot read data key, the entered password might be wrong. The underlying error: '${e.message}', stack:\n${e.stack}`); } } function getPasswordDerivedKey(password: any) { - const salt = getOption('passwordDerivedKeySalt'); + const salt = getOption("passwordDerivedKeySalt"); return getScryptHash(password, salt); } function getScryptHash(password: any, salt: any) { - const hashed = crypto.scryptSync(password, salt, 32, - { N: 16384, r: 8, p: 1 }); + const hashed = crypto.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 }); return hashed; } diff --git a/dump-db/inc/decrypt.ts b/dump-db/inc/decrypt.ts index e30c362bc..d7ae92614 100644 --- a/dump-db/inc/decrypt.ts +++ b/dump-db/inc/decrypt.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from "crypto"; function decryptString(dataKey: any, cipherText: any) { const buffer = decrypt(dataKey, cipherText); @@ -7,9 +7,9 @@ function decryptString(dataKey: any, cipherText: any) { return null; } - const str = buffer.toString('utf-8'); + const str = buffer.toString("utf-8"); - if (str === 'false') { + if (str === "false") { throw new Error("Could not decrypt string."); } @@ -26,12 +26,12 @@ function decrypt(key: any, cipherText: any, ivLength = 13) { } try { - const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), 'base64'); + const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64"); const iv = cipherTextBufferWithIv.slice(0, ivLength); const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength); - const decipher = crypto.createDecipheriv('aes-128-cbc', pad(key), pad(iv)); + const decipher = crypto.createDecipheriv("aes-128-cbc", pad(key), pad(iv)); const decryptedBytes = Buffer.concat([decipher.update(cipherTextBuffer), decipher.final()]); @@ -45,14 +45,12 @@ function decrypt(key: any, cipherText: any, ivLength = 13) { } return payload; - } - catch (e: any) { + } catch (e: any) { // recovery from https://github.com/zadam/trilium/issues/510 if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) { console.log("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead"); return cipherText; - } - else { + } else { throw e; } } @@ -61,8 +59,7 @@ function decrypt(key: any, cipherText: any, ivLength = 13) { function pad(data: any) { if (data.length > 16) { data = data.slice(0, 16); - } - else if (data.length < 16) { + } else if (data.length < 16) { const zeros = Array(16 - data.length).fill(0); data = Buffer.concat([data, Buffer.from(zeros)]); @@ -82,7 +79,7 @@ function arraysIdentical(a: any, b: any) { function shaArray(content: any) { // we use this as simple checksum and don't rely on its security so SHA-1 is good enough - return crypto.createHash('sha1').update(content).digest(); + return crypto.createHash("sha1").update(content).digest(); } export default { diff --git a/dump-db/inc/dump.ts b/dump-db/inc/dump.ts index 4bb51e181..cb4960f02 100644 --- a/dump-db/inc/dump.ts +++ b/dump-db/inc/dump.ts @@ -1,11 +1,11 @@ -import fs from 'fs'; -import sanitize from 'sanitize-filename'; -import sql from './sql.js'; -import decryptService from './decrypt.js'; -import dataKeyService from './data_key.js'; -import extensionService from './extension.js'; +import fs from "fs"; +import sanitize from "sanitize-filename"; +import sql from "./sql.js"; +import decryptService from "./decrypt.js"; +import dataKeyService from "./data_key.js"; +import extensionService from "./extension.js"; -function dumpDocument(documentPath: string, targetPath: string, options: { password: any; includeDeleted: any; }) { +function dumpDocument(documentPath: string, targetPath: string, options: { password: any; includeDeleted: any }) { const stats = { succeeded: 0, failed: 0, @@ -22,7 +22,7 @@ function dumpDocument(documentPath: string, targetPath: string, options: { passw const existingPaths: Record = {}; const noteIdToPath: Record = {}; - dumpNote(targetPath, 'root'); + dumpNote(targetPath, "root"); printDumpResults(stats, options); @@ -56,10 +56,10 @@ function dumpDocument(documentPath: string, targetPath: string, options: { passw safeTitle = safeTitle.substring(0, 20); } - childTargetPath = targetPath + '/' + safeTitle; + childTargetPath = targetPath + "/" + safeTitle; for (let i = 1; i < 100000 && childTargetPath in existingPaths; i++) { - childTargetPath = targetPath + '/' + safeTitle + '_' + i; + childTargetPath = targetPath + "/" + safeTitle + "_" + i; } existingPaths[childTargetPath] = true; @@ -93,8 +93,7 @@ function dumpDocument(documentPath: string, targetPath: string, options: { passw } noteIdToPath[noteId] = childTargetPath; - } - catch (e: any) { + } catch (e: any) { console.error(`DUMPERROR: Writing '${noteId}' failed with error '${e.message}':\n${e.stack}`); stats.failed++; @@ -104,13 +103,12 @@ function dumpDocument(documentPath: string, targetPath: string, options: { passw if (childNoteIds.length > 0) { if (childTargetPath === fileNameWithPath) { - childTargetPath += '_dir'; + childTargetPath += "_dir"; } try { fs.mkdirSync(childTargetPath as string, { recursive: true }); - } - catch (e: any) { + } catch (e: any) { console.error(`DUMPERROR: Creating directory ${childTargetPath} failed with error '${e.message}'`); } @@ -122,12 +120,12 @@ function dumpDocument(documentPath: string, targetPath: string, options: { passw } function printDumpResults(stats: any, options: any) { - console.log('\n----------------------- STATS -----------------------'); - console.log('Successfully dumpted notes: ', stats.succeeded.toString().padStart(5, ' ')); - console.log('Protected notes: ', stats.protected.toString().padStart(5, ' '), options.password ? '' : '(skipped)'); - console.log('Failed notes: ', stats.failed.toString().padStart(5, ' ')); - console.log('Deleted notes: ', stats.deleted.toString().padStart(5, ' '), options.includeDeleted ? "(dumped)" : "(at least, skipped)"); - console.log('-----------------------------------------------------'); + console.log("\n----------------------- STATS -----------------------"); + console.log("Successfully dumpted notes: ", stats.succeeded.toString().padStart(5, " ")); + console.log("Protected notes: ", stats.protected.toString().padStart(5, " "), options.password ? "" : "(skipped)"); + console.log("Failed notes: ", stats.failed.toString().padStart(5, " ")); + console.log("Deleted notes: ", stats.deleted.toString().padStart(5, " "), options.includeDeleted ? "(dumped)" : "(at least, skipped)"); + console.log("-----------------------------------------------------"); if (!options.password && stats.protected > 0) { console.log("\nWARNING: protected notes are present in the document but no password has been provided. Protected notes have not been dumped."); @@ -140,12 +138,10 @@ function isContentEmpty(content: any) { } if (typeof content === "string") { - return !content.trim() || content.trim() === '

'; - } - else if (Buffer.isBuffer(content)) { + return !content.trim() || content.trim() === "

"; + } else if (Buffer.isBuffer(content)) { return content.length === 0; - } - else { + } else { return false; } } diff --git a/dump-db/inc/extension.ts b/dump-db/inc/extension.ts index 612e1ce23..73c90a2b7 100644 --- a/dump-db/inc/extension.ts +++ b/dump-db/inc/extension.ts @@ -5,15 +5,17 @@ function getFileName(note: any, childTargetPath: string, safeTitle: string) { let existingExtension = path.extname(safeTitle).toLowerCase(); let newExtension; - if (note.type === 'text') { - newExtension = 'html'; - } else if (note.mime === 'application/x-javascript' || note.mime === 'text/javascript') { - newExtension = 'js'; - } else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it + if (note.type === "text") { + newExtension = "html"; + } else if (note.mime === "application/x-javascript" || note.mime === "text/javascript") { + newExtension = "js"; + } else if (existingExtension.length > 0) { + // if the page already has an extension, then we'll just keep it newExtension = null; } else { - if (note.mime?.toLowerCase()?.trim() === "image/jpg") { // image/jpg is invalid but pretty common - newExtension = 'jpg'; + if (note.mime?.toLowerCase()?.trim() === "image/jpg") { + // image/jpg is invalid but pretty common + newExtension = "jpg"; } else { newExtension = mimeTypes.extension(note.mime) || "dat"; } diff --git a/dump-db/inc/sql.ts b/dump-db/inc/sql.ts index 00d6296a2..c0f6ebc50 100644 --- a/dump-db/inc/sql.ts +++ b/dump-db/inc/sql.ts @@ -2,7 +2,9 @@ import Database, { Database as DatabaseType } from "better-sqlite3"; let dbConnection: DatabaseType; -const openDatabase = (documentPath: string) => { dbConnection = new Database(documentPath, { readonly: true }) }; +const openDatabase = (documentPath: string) => { + dbConnection = new Database(documentPath, { readonly: true }); +}; const getRow = (query: string, params: string[] = []): Record => dbConnection.prepare(query).get(params) as Record; const getRows = (query: string, params = []) => dbConnection.prepare(query).all(params); diff --git a/dump-db/tsconfig.json b/dump-db/tsconfig.json index eb70c28db..1865f5d1b 100644 --- a/dump-db/tsconfig.json +++ b/dump-db/tsconfig.json @@ -1,10 +1,10 @@ { - "compilerOptions": { - "module": "ESNext", - "moduleResolution": "node", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "target": "ES6", - "strict": true - } -} \ No newline at end of file + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "target": "ES6", + "strict": true + } +} diff --git a/electron-main.ts b/electron-main.ts index 58fae2dd9..424209012 100644 --- a/electron-main.ts +++ b/electron-main.ts @@ -1,4 +1,4 @@ import { initializeTranslations } from "./src/services/i18n.js"; await initializeTranslations(); -await import("./electron.js") +await import("./electron.js"); diff --git a/electron.ts b/electron.ts index 188240610..154ec38d4 100644 --- a/electron.ts +++ b/electron.ts @@ -12,8 +12,8 @@ import sourceMapSupport from "source-map-support"; sourceMapSupport.install(); // Prevent Trilium starting twice on first install and on uninstall for the Windows installer. -if ((await import('electron-squirrel-startup')).default) { - process.exit(0); +if ((await import("electron-squirrel-startup")).default) { + process.exit(0); } // Adds debug features like hotkeys for triggering dev tools and reload @@ -24,50 +24,48 @@ appIconService.installLocalAppIcon(); electronDl({ saveAs: true }); // needed for excalidraw export https://github.com/zadam/trilium/issues/4271 -electron.app.commandLine.appendSwitch( - "enable-experimental-web-platform-features" -); +electron.app.commandLine.appendSwitch("enable-experimental-web-platform-features"); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. electron.app.on("window-all-closed", () => { - if (process.platform !== "darwin") { - electron.app.quit(); - } + if (process.platform !== "darwin") { + electron.app.quit(); + } }); electron.app.on("ready", async () => { - // electron.app.setAppUserModelId('com.github.zadam.trilium'); + // electron.app.setAppUserModelId('com.github.zadam.trilium'); - // if db is not initialized -> setup process - // if db is initialized, then we need to wait until the migration process is finished - if (sqlInit.isDbInitialized()) { - await sqlInit.dbReady; + // if db is not initialized -> setup process + // if db is initialized, then we need to wait until the migration process is finished + if (sqlInit.isDbInitialized()) { + await sqlInit.dbReady; - await windowService.createMainWindow(electron.app); - - if (process.platform === "darwin") { - electron.app.on("activate", async () => { - if (electron.BrowserWindow.getAllWindows().length === 0) { await windowService.createMainWindow(electron.app); + + if (process.platform === "darwin") { + electron.app.on("activate", async () => { + if (electron.BrowserWindow.getAllWindows().length === 0) { + await windowService.createMainWindow(electron.app); + } + }); } - }); + + tray.createTray(); + } else { + await windowService.createSetupWindow(); } - tray.createTray(); - } else { - await windowService.createSetupWindow(); - } - - await windowService.registerGlobalShortcuts(); + await windowService.registerGlobalShortcuts(); }); electron.app.on("will-quit", () => { - electron.globalShortcut.unregisterAll(); + electron.globalShortcut.unregisterAll(); }); // this is to disable electron warning spam in the dev console (local development only) process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true"; -await import('./src/main.js'); +await import("./src/main.js"); diff --git a/forge.config.cjs b/forge.config.cjs index 3d29c84f0..64ef74b16 100644 --- a/forge.config.cjs +++ b/forge.config.cjs @@ -1,117 +1,115 @@ -const path = require('path'); -const fs = require('fs-extra'); +const path = require("path"); +const fs = require("fs-extra"); const APP_NAME = "TriliumNext Notes"; module.exports = { - packagerConfig: { - executableName: "trilium", - name: APP_NAME, - overwrite: true, - asar: true, - icon: "./images/app-icons/icon", - extraResource: [ - // Moved to root - ...getExtraResourcesForPlatform(), + packagerConfig: { + executableName: "trilium", + name: APP_NAME, + overwrite: true, + asar: true, + icon: "./images/app-icons/icon", + extraResource: [ + // Moved to root + ...getExtraResourcesForPlatform(), - // Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS) - "translations/", - "node_modules/@highlightjs/cdn-assets/styles" + // Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS) + "translations/", + "node_modules/@highlightjs/cdn-assets/styles" + ], + afterComplete: [ + (buildPath, _electronVersion, platform, _arch, callback) => { + const extraResources = getExtraResourcesForPlatform(); + for (const resource of extraResources) { + const baseName = path.basename(resource); + let sourcePath; + if (platform === "darwin") { + sourcePath = path.join(buildPath, `${APP_NAME}.app`, "Contents", "Resources", baseName); + } else { + sourcePath = path.join(buildPath, "resources", baseName); + } + let destPath; + + if (baseName !== "256x256.png") { + destPath = path.join(buildPath, baseName); + } else { + destPath = path.join(buildPath, "icon.png"); + } + + // Copy files from resources folder to root + fs.move(sourcePath, destPath) + .then(() => callback()) + .catch((err) => callback(err)); + } + } + ] + }, + rebuildConfig: { + force: true + }, + makers: [ + { + name: "@electron-forge/maker-deb", + config: { + options: { + icon: "./images/app-icons/png/128x128.png", + desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs") + } + } + }, + { + name: "@electron-forge/maker-squirrel", + config: { + iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico", + setupIcon: "./images/app-icons/icon.ico", + loadingGif: "./images/app-icons/win/setup-banner.gif" + } + }, + { + name: "@electron-forge/maker-dmg", + config: { + icon: "./images/app-icons/icon.icns" + } + }, + { + name: "@electron-forge/maker-zip", + config: { + options: { + iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico", + icon: "./images/app-icons/icon.ico" + } + } + } ], - afterComplete: [(buildPath, _electronVersion, platform, _arch, callback) => { - const extraResources = getExtraResourcesForPlatform(); - for (const resource of extraResources) { - const baseName = path.basename(resource); - let sourcePath; - if (platform === 'darwin') { - sourcePath = path.join(buildPath, `${APP_NAME}.app`, 'Contents', 'Resources', baseName); - } else { - sourcePath = path.join(buildPath, 'resources', baseName); + plugins: [ + { + name: "@electron-forge/plugin-auto-unpack-natives", + config: {} } - let destPath; - - if (baseName !== "256x256.png") { - destPath = path.join(buildPath, baseName); - } else { - destPath = path.join(buildPath, "icon.png"); - } - - // Copy files from resources folder to root - fs.move(sourcePath, destPath) - .then(() => callback()) - .catch(err => callback(err)); - } - }] - }, - rebuildConfig: { - force: true - }, - makers: [ - { - name: '@electron-forge/maker-deb', - config: { - options: { - icon: "./images/app-icons/png/128x128.png", - desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs") - } - } - }, - { - name: '@electron-forge/maker-squirrel', - config: { - iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico", - setupIcon: "./images/app-icons/icon.ico", - loadingGif: "./images/app-icons/win/setup-banner.gif" - } - }, - { - name: '@electron-forge/maker-dmg', - config: { - icon: "./images/app-icons/icon.icns", - } - }, - { - name: '@electron-forge/maker-zip', - config: { - options: { - iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico", - icon: "./images/app-icons/icon.ico", - } - } - } - ], - plugins: [ - { - name: '@electron-forge/plugin-auto-unpack-natives', - config: {}, - }, - ], + ] }; - function getExtraResourcesForPlatform() { - let resources = [ - 'dump-db/', - './bin/tpl/anonymize-database.sql' - ]; - const scripts = ['trilium-portable', 'trilium-safe-mode', 'trilium-no-cert-check'] - switch (process.platform) { - case 'win32': - for (const script of scripts) { - resources.push(`./bin/tpl/${script}.bat`) - } - break; - case 'darwin': - break; - case 'linux': - resources.push("images/app-icons/png/256x256.png") - for (const script of scripts) { - resources.push(`./bin/tpl/${script}.sh`) - } - break; - default: - break; - } + let resources = ["dump-db/", "./bin/tpl/anonymize-database.sql"]; + const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"]; + switch (process.platform) { + case "win32": + for (const script of scripts) { + resources.push(`./bin/tpl/${script}.bat`); + } + break; + case "darwin": + break; + case "linux": + resources.push("images/app-icons/png/256x256.png"); + for (const script of scripts) { + resources.push(`./bin/tpl/${script}.sh`); + } + break; + default: + break; + } - return resources; -} \ No newline at end of file + return resources; +} diff --git a/integration-tests/auth.setup.ts b/integration-tests/auth.setup.ts index 8a1d4e4d1..9b31eec49 100644 --- a/integration-tests/auth.setup.ts +++ b/integration-tests/auth.setup.ts @@ -1,6 +1,6 @@ -import { test as setup, expect } from '@playwright/test'; +import { test as setup, expect } from "@playwright/test"; -const authFile = 'playwright/.auth/user.json'; +const authFile = "playwright/.auth/user.json"; const ROOT_URL = "http://localhost:8082"; const LOGIN_PASSWORD = "demo1234"; @@ -12,6 +12,6 @@ setup("authenticate", async ({ page }) => { await expect(page).toHaveURL(`${ROOT_URL}/login`); await page.getByRole("textbox", { name: "Password" }).fill(LOGIN_PASSWORD); - await page.getByRole("button", { name: "Login"}).click(); + await page.getByRole("button", { name: "Login" }).click(); await page.context().storageState({ path: authFile }); }); diff --git a/integration-tests/duplicate.spec.ts b/integration-tests/duplicate.spec.ts index e984c2f98..fe079952b 100644 --- a/integration-tests/duplicate.spec.ts +++ b/integration-tests/duplicate.spec.ts @@ -1,9 +1,9 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; test("Can duplicate note with broken links", async ({ page }) => { await page.goto(`http://localhost:8082/#2VammGGdG6Ie`); - await page.locator('.tree-wrapper .fancytree-active').getByText('Note map').click({ button: 'right' }); - await page.getByText('Duplicate subtree').click(); + await page.locator(".tree-wrapper .fancytree-active").getByText("Note map").click({ button: "right" }); + await page.getByText("Duplicate subtree").click(); await expect(page.locator(".toast-body")).toBeHidden(); - await expect(page.locator('.tree-wrapper').getByText('Note map (dup)')).toBeVisible(); + await expect(page.locator(".tree-wrapper").getByText("Note map (dup)")).toBeVisible(); }); diff --git a/integration-tests/example.disabled.ts b/integration-tests/example.disabled.ts index 54a906a4e..a149fe328 100644 --- a/integration-tests/example.disabled.ts +++ b/integration-tests/example.disabled.ts @@ -1,18 +1,18 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); +test("has title", async ({ page }) => { + await page.goto("https://playwright.dev/"); - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); }); -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); +test("get started link", async ({ page }) => { + await page.goto("https://playwright.dev/"); - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); + // Click the get started link. + await page.getByRole("link", { name: "Get started" }).click(); - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible(); }); diff --git a/integration-tests/help.spec.ts b/integration-tests/help.spec.ts index cd7722222..bf12d9978 100644 --- a/integration-tests/help.spec.ts +++ b/integration-tests/help.spec.ts @@ -1,23 +1,23 @@ import test, { expect } from "@playwright/test"; -test('Help popup', async ({ page }) => { - await page.goto('http://localhost:8082'); - await page.getByText('Trilium Integration Test DB').click(); +test("Help popup", async ({ page }) => { + await page.goto("http://localhost:8082"); + await page.getByText("Trilium Integration Test DB").click(); - await page.locator('body').press('F1'); - await page.getByRole('link', { name: 'online↗' }).click(); - expect((await page.waitForEvent('popup')).url()).toBe("https://triliumnext.github.io/Docs/") + await page.locator("body").press("F1"); + await page.getByRole("link", { name: "online↗" }).click(); + expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/"); }); -test('Complete help in search', async ({ page }) => { - await page.goto('http://localhost:8082'); +test("Complete help in search", async ({ page }) => { + await page.goto("http://localhost:8082"); // Clear all tabs - await page.locator('.note-tab:first-of-type').locator("div").nth(1).click({ button: 'right' }); - await page.getByText('Close all tabs').click(); + await page.locator(".note-tab:first-of-type").locator("div").nth(1).click({ button: "right" }); + await page.getByText("Close all tabs").click(); - await page.locator('#launcher-container').getByRole('button', { name: '' }).first().click(); - await page.getByRole('cell', { name: ' ' }).locator('span').first().click(); - await page.getByRole('button', { name: 'complete help on search syntax' }).click(); - expect((await page.waitForEvent('popup')).url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html"); + await page.locator("#launcher-container").getByRole("button", { name: "" }).first().click(); + await page.getByRole("cell", { name: " " }).locator("span").first().click(); + await page.getByRole("button", { name: "complete help on search syntax" }).click(); + expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html"); }); diff --git a/integration-tests/i18n.spec.ts b/integration-tests/i18n.spec.ts index bd935ac68..1c3fc1d36 100644 --- a/integration-tests/i18n.spec.ts +++ b/integration-tests/i18n.spec.ts @@ -1,33 +1,33 @@ import test, { expect } from "@playwright/test"; test("User can change language from settings", async ({ page }) => { - await page.goto('http://localhost:8082'); + await page.goto("http://localhost:8082"); // Clear all tabs - await page.locator('.note-tab:first-of-type').locator("div").nth(1).click({ button: 'right' }); - await page.getByText('Close all tabs').click(); + await page.locator(".note-tab:first-of-type").locator("div").nth(1).click({ button: "right" }); + await page.getByText("Close all tabs").click(); // Go to options -> Appearance - await page.locator('#launcher-pane div').filter({ hasText: 'Options Open New Window' }).getByRole('button').click(); - await page.locator('#launcher-pane').getByText('Options').click(); - await page.locator('#center-pane').getByText('Appearance').click(); + await page.locator("#launcher-pane div").filter({ hasText: "Options Open New Window" }).getByRole("button").click(); + await page.locator("#launcher-pane").getByText("Options").click(); + await page.locator("#center-pane").getByText("Appearance").click(); // Check that the default value (English) is set. - await expect(page.locator('#center-pane')).toContainText('Theme'); - const languageCombobox = await page.getByRole('combobox').first(); + await expect(page.locator("#center-pane")).toContainText("Theme"); + const languageCombobox = await page.getByRole("combobox").first(); await expect(languageCombobox).toHaveValue("en"); // Select Chinese and ensure the translation is set. languageCombobox.selectOption("cn"); - await expect(page.locator('#center-pane')).toContainText('主题'); + await expect(page.locator("#center-pane")).toContainText("主题"); // Select English again. languageCombobox.selectOption("en"); }); test("Restores language on start-up on desktop", async ({ page, context }) => { - await page.goto('http://localhost:8082'); - await expect(page.locator('#launcher-pane').first()).toContainText("Open New Window"); + await page.goto("http://localhost:8082"); + await expect(page.locator("#launcher-pane").first()).toContainText("Open New Window"); }); test("Restores language on start-up on mobile", async ({ page, context }) => { @@ -38,6 +38,6 @@ test("Restores language on start-up on mobile", async ({ page, context }) => { value: "mobile" } ]); - await page.goto('http://localhost:8082'); - await expect(page.locator('#launcher-pane div').first()).toContainText("Open New Window"); + await page.goto("http://localhost:8082"); + await expect(page.locator("#launcher-pane div").first()).toContainText("Open New Window"); }); diff --git a/integration-tests/katex.disabled.ts b/integration-tests/katex.disabled.ts index 0e99a3fbe..457c53d7c 100644 --- a/integration-tests/katex.disabled.ts +++ b/integration-tests/katex.disabled.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; const ROOT_URL = "http://localhost:8080"; const LOGIN_PASSWORD = "eliandoran"; @@ -12,7 +12,6 @@ test("Can insert equations", async ({ page }) => { // .click(); const activeNote = page.locator(".component.note-split:visible"); - const noteContent = activeNote - .locator(".note-detail-editable-text-editor") + const noteContent = activeNote.locator(".note-detail-editable-text-editor"); await noteContent.press("Ctrl+M"); }); diff --git a/integration-tests/settings.spec.ts b/integration-tests/settings.spec.ts index 835e8e341..b3fe16fda 100644 --- a/integration-tests/settings.spec.ts +++ b/integration-tests/settings.spec.ts @@ -1,21 +1,21 @@ import test, { expect } from "@playwright/test"; test("Native Title Bar not displayed on web", async ({ page }) => { - await page.goto('http://localhost:8082/#root/_hidden/_options/_optionsAppearance'); - await expect(page.getByRole('heading', { name: 'Theme' })).toBeVisible(); - await expect(page.getByRole('heading', { name: 'Native Title Bar (requires' })).toBeHidden(); + await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsAppearance"); + await expect(page.getByRole("heading", { name: "Theme" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Native Title Bar (requires" })).toBeHidden(); }); test("Tray settings not displayed on web", async ({ page }) => { - await page.goto('http://localhost:8082/#root/_hidden/_options/_optionsOther'); - await expect(page.getByRole('heading', { name: 'Note Erasure Timeout' })).toBeVisible(); - await expect(page.getByRole('heading', { name: 'Tray' })).toBeHidden(); + await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsOther"); + await expect(page.getByRole("heading", { name: "Note Erasure Timeout" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden(); }); test("Spellcheck settings not displayed on web", async ({ page }) => { - await page.goto('http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck'); - await expect(page.getByRole('heading', { name: 'Spell Check' })).toBeVisible(); - await expect(page.getByRole('heading', { name: 'Tray' })).toBeHidden(); - await expect(page.getByText('These options apply only for desktop builds')).toBeVisible(); - await expect(page.getByText('Enable spellcheck')).toBeHidden(); + await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck"); + await expect(page.getByRole("heading", { name: "Spell Check" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden(); + await expect(page.getByText("These options apply only for desktop builds")).toBeVisible(); + await expect(page.getByText("Enable spellcheck")).toBeHidden(); }); diff --git a/integration-tests/tree.spec.ts b/integration-tests/tree.spec.ts index 0bc9caf22..257375fa8 100644 --- a/integration-tests/tree.spec.ts +++ b/integration-tests/tree.spec.ts @@ -1,8 +1,8 @@ import test, { expect } from "@playwright/test"; test("Renders on desktop", async ({ page, context }) => { - await page.goto('http://localhost:8082'); - await expect(page.locator('.tree')).toContainText('Trilium Integration Test'); + await page.goto("http://localhost:8082"); + await expect(page.locator(".tree")).toContainText("Trilium Integration Test"); }); test("Renders on mobile", async ({ page, context }) => { @@ -13,6 +13,6 @@ test("Renders on mobile", async ({ page, context }) => { value: "mobile" } ]); - await page.goto('http://localhost:8082'); - await expect(page.locator('.tree')).toContainText('Trilium Integration Test'); + await page.goto("http://localhost:8082"); + await expect(page.locator(".tree")).toContainText("Trilium Integration Test"); }); diff --git a/integration-tests/update_check.spec.ts b/integration-tests/update_check.spec.ts index 732cd501a..8cb7f4bf2 100644 --- a/integration-tests/update_check.spec.ts +++ b/integration-tests/update_check.spec.ts @@ -1,12 +1,12 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; const expectedVersion = "0.90.3"; test("Displays update badge when there is a version available", async ({ page }) => { await page.goto("http://localhost:8080"); - await page.getByRole('button', { name: '' }).click(); + await page.getByRole("button", { name: "" }).click(); await page.getByText(`Version ${expectedVersion} is available,`).click(); - const page1 = await page.waitForEvent('popup'); + const page1 = await page.waitForEvent("popup"); expect(page1.url()).toBe(`https://github.com/TriliumNext/Notes/releases/tag/v${expectedVersion}`); }); diff --git a/jsdoc-conf.json b/jsdoc-conf.json index c0451f5b4..b61bbacb6 100644 --- a/jsdoc-conf.json +++ b/jsdoc-conf.json @@ -4,4 +4,4 @@ "includeDate": false } } -} \ No newline at end of file +} diff --git a/loader-register.js b/loader-register.js index b35f8bd59..ff86e6832 100644 --- a/loader-register.js +++ b/loader-register.js @@ -5,6 +5,6 @@ // Then probably can change webpack comand to // "webpack": "cross-env NODE_OPTIONS=--import=ts-node/esm webpack -c webpack.config.ts", -import { register } from 'node:module'; -import { pathToFileURL } from 'node:url'; -register('ts-node/esm', pathToFileURL('./')); +import { register } from "node:module"; +import { pathToFileURL } from "node:url"; +register("ts-node/esm", pathToFileURL("./")); diff --git a/nodemon.json b/nodemon.json index efaf5f759..e860ebccc 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,15 +1,12 @@ { - "restartable": "rs", - "ignore": [".git", "node_modules/**/node_modules", "src/public/"], - "verbose": false, - "exec": "tsx", - "watch": [ - "src/", - "translations/" - ], - "signal": "SIGTERM", - "env": { - "NODE_ENV": "development" - }, - "ext": "ts,js,json" + "restartable": "rs", + "ignore": [".git", "node_modules/**/node_modules", "src/public/"], + "verbose": false, + "exec": "tsx", + "watch": ["src/", "translations/"], + "signal": "SIGTERM", + "env": { + "NODE_ENV": "development" + }, + "ext": "ts,js,json" } diff --git a/playwright.config.ts b/playwright.config.ts index ca8b95889..93103178b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * Read environment variables from file. @@ -11,75 +11,75 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './integration-tests', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, - - webServer: { - command: "npm run integration-mem-db", - url: "http://127.0.0.1:8082", - reuseExistingServer: true, - stdout: "ignore", - stderr: "pipe" - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: "setup", - testMatch: /.*\.setup\.ts/ - }, - - { - name: "firefox", + testDir: "./integration-tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { - ...devices[ "Desktop Firefox" ], - storageState: "playwright/.auth/user.json" - }, - dependencies: [ "setup" ] + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry" }, - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, + webServer: { + command: "npm run integration-mem-db", + url: "http://127.0.0.1:8082", + reuseExistingServer: true, + stdout: "ignore", + stderr: "pipe" + }, - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], + /* Configure projects for major browsers */ + projects: [ + { + name: "setup", + testMatch: /.*\.setup\.ts/ + }, - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, + { + name: "firefox", + use: { + ...devices["Desktop Firefox"], + storageState: "playwright/.auth/user.json" + }, + dependencies: ["setup"] + } + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ] + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, }); diff --git a/spec-es6/attribute_parser.spec.ts b/spec-es6/attribute_parser.spec.ts index 60f510d8d..c7932b378 100644 --- a/spec-es6/attribute_parser.spec.ts +++ b/spec-es6/attribute_parser.spec.ts @@ -1,29 +1,24 @@ -import * as attributeParser from '../src/public/app/services/attribute_parser.js'; +import * as attributeParser from "../src/public/app/services/attribute_parser.js"; -import {describe, it, expect, execute} from './mini_test.js'; +import { describe, it, expect, execute } from "./mini_test.js"; describe("Lexing", () => { it("simple label", () => { - expect(attributeParser.lex("#label").map((t: any) => t.text)) - .toEqual(["#label"]); + expect(attributeParser.lex("#label").map((t: any) => t.text)).toEqual(["#label"]); }); it("simple label with trailing spaces", () => { - expect(attributeParser.lex(" #label ").map((t: any) => t.text)) - .toEqual(["#label"]); + expect(attributeParser.lex(" #label ").map((t: any) => t.text)).toEqual(["#label"]); }); it("inherited label", () => { - expect(attributeParser.lex("#label(inheritable)").map((t: any) => t.text)) - .toEqual(["#label", "(", "inheritable", ")"]); + expect(attributeParser.lex("#label(inheritable)").map((t: any) => t.text)).toEqual(["#label", "(", "inheritable", ")"]); - expect(attributeParser.lex("#label ( inheritable ) ").map((t: any) => t.text)) - .toEqual(["#label", "(", "inheritable", ")"]); + expect(attributeParser.lex("#label ( inheritable ) ").map((t: any) => t.text)).toEqual(["#label", "(", "inheritable", ")"]); }); it("label with value", () => { - expect(attributeParser.lex("#label=Hallo").map((t: any) => t.text)) - .toEqual(["#label", "=", "Hallo"]); + expect(attributeParser.lex("#label=Hallo").map((t: any) => t.text)).toEqual(["#label", "=", "Hallo"]); }); it("label with value", () => { @@ -33,79 +28,72 @@ describe("Lexing", () => { }); it("relation with value", () => { - expect(attributeParser.lex('~relation=#root/RclIpMauTOKS/NFi2gL4xtPxM').map((t: any) => t.text)) - .toEqual(["~relation", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"]); + expect(attributeParser.lex("~relation=#root/RclIpMauTOKS/NFi2gL4xtPxM").map((t: any) => t.text)).toEqual(["~relation", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"]); }); it("use quotes to define value", () => { - expect(attributeParser.lex("#'label a'='hello\"` world'").map((t: any) => t.text)) - .toEqual(["#label a", "=", 'hello"` world']); + expect(attributeParser.lex("#'label a'='hello\"` world'").map((t: any) => t.text)).toEqual(["#label a", "=", 'hello"` world']); - expect(attributeParser.lex('#"label a" = "hello\'` world"').map((t: any) => t.text)) - .toEqual(["#label a", "=", "hello'` world"]); + expect(attributeParser.lex('#"label a" = "hello\'` world"').map((t: any) => t.text)).toEqual(["#label a", "=", "hello'` world"]); - expect(attributeParser.lex('#`label a` = `hello\'" world`').map((t: any) => t.text)) - .toEqual(["#label a", "=", "hello'\" world"]); + expect(attributeParser.lex("#`label a` = `hello'\" world`").map((t: any) => t.text)).toEqual(["#label a", "=", "hello'\" world"]); }); }); describe("Parser", () => { it("simple label", () => { - const attrs = attributeParser.parse(["#token"].map((t: any) => ({text: t}))); + const attrs = attributeParser.parse(["#token"].map((t: any) => ({ text: t }))); expect(attrs.length).toEqual(1); - expect(attrs[0].type).toEqual('label'); - expect(attrs[0].name).toEqual('token'); + expect(attrs[0].type).toEqual("label"); + expect(attrs[0].name).toEqual("token"); expect(attrs[0].isInheritable).toBeFalsy(); expect(attrs[0].value).toBeFalsy(); }); it("inherited label", () => { - const attrs = attributeParser.parse(["#token", "(", "inheritable", ")"].map((t: any) => ({text: t}))); + const attrs = attributeParser.parse(["#token", "(", "inheritable", ")"].map((t: any) => ({ text: t }))); expect(attrs.length).toEqual(1); - expect(attrs[0].type).toEqual('label'); - expect(attrs[0].name).toEqual('token'); + expect(attrs[0].type).toEqual("label"); + expect(attrs[0].name).toEqual("token"); expect(attrs[0].isInheritable).toBeTruthy(); expect(attrs[0].value).toBeFalsy(); }); it("label with value", () => { - const attrs = attributeParser.parse(["#token", "=", "val"].map((t: any) => ({text: t}))); + const attrs = attributeParser.parse(["#token", "=", "val"].map((t: any) => ({ text: t }))); expect(attrs.length).toEqual(1); - expect(attrs[0].type).toEqual('label'); - expect(attrs[0].name).toEqual('token'); + expect(attrs[0].type).toEqual("label"); + expect(attrs[0].name).toEqual("token"); expect(attrs[0].value).toEqual("val"); }); it("relation", () => { - let attrs = attributeParser.parse(["~token", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"].map((t: any) => ({text: t}))); + let attrs = attributeParser.parse(["~token", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"].map((t: any) => ({ text: t }))); expect(attrs.length).toEqual(1); - expect(attrs[0].type).toEqual('relation'); + expect(attrs[0].type).toEqual("relation"); expect(attrs[0].name).toEqual("token"); - expect(attrs[0].value).toEqual('NFi2gL4xtPxM'); + expect(attrs[0].value).toEqual("NFi2gL4xtPxM"); - attrs = attributeParser.parse(["~token", "=", "#NFi2gL4xtPxM"].map((t: any) => ({text: t}))); + attrs = attributeParser.parse(["~token", "=", "#NFi2gL4xtPxM"].map((t: any) => ({ text: t }))); expect(attrs.length).toEqual(1); - expect(attrs[0].type).toEqual('relation'); + expect(attrs[0].type).toEqual("relation"); expect(attrs[0].name).toEqual("token"); - expect(attrs[0].value).toEqual('NFi2gL4xtPxM'); + expect(attrs[0].value).toEqual("NFi2gL4xtPxM"); }); }); describe("error cases", () => { it("error cases", () => { - expect(() => attributeParser.lexAndParse('~token')) - .toThrow('Relation "~token" in "~token" should point to a note.'); + expect(() => attributeParser.lexAndParse("~token")).toThrow('Relation "~token" in "~token" should point to a note.'); - expect(() => attributeParser.lexAndParse("#a&b/s")) - .toThrow(`Attribute name "a&b/s" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`); + expect(() => attributeParser.lexAndParse("#a&b/s")).toThrow(`Attribute name "a&b/s" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`); - expect(() => attributeParser.lexAndParse("#")) - .toThrow(`Attribute name is empty, please fill the name.`); + expect(() => attributeParser.lexAndParse("#")).toThrow(`Attribute name is empty, please fill the name.`); }); }); diff --git a/spec-es6/mini_test.ts b/spec-es6/mini_test.ts index 1105981c0..0657b8a89 100644 --- a/spec-es6/mini_test.ts +++ b/spec-es6/mini_test.ts @@ -47,8 +47,7 @@ export function expect(val: any) { toThrow: (errorMessage: any) => { try { val(); - } - catch (e: any) { + } catch (e: any) { if (e.message !== errorMessage) { console.trace("toThrow caught exception, but messages differ"); console.error(`expected: ${errorMessage}`); @@ -66,7 +65,7 @@ export function expect(val: any) { console.error(`got: [none]`); errorCount++; } - } + }; } export function execute() { @@ -74,8 +73,7 @@ export function execute() { if (errorCount) { console.log(`!!!${errorCount} tests failed!!!`); - } - else { + } else { console.log("All tests passed!"); } } diff --git a/spec-es6/sanitize_attribute_name.spec.ts b/spec-es6/sanitize_attribute_name.spec.ts index fa01a4690..9399d1edb 100644 --- a/spec-es6/sanitize_attribute_name.spec.ts +++ b/spec-es6/sanitize_attribute_name.spec.ts @@ -1,43 +1,39 @@ -import sanitizeAttributeName from "../src/services/sanitize_attribute_name" +import sanitizeAttributeName from "../src/services/sanitize_attribute_name"; import { describe, it, execute, expect } from "./mini_test"; // fn value, expected value const testCases: [fnValue: string, expectedValue: string][] = [ - ["testName", "testName"], - ["test_name", "test_name"], - ["test with space", "test_with_space"], - ["test:with:colon", "test:with:colon"], + ["testName", "testName"], + ["test_name", "test_name"], + ["test with space", "test_with_space"], + ["test:with:colon", "test:with:colon"], - // numbers - ["123456", "123456"], - ["123:456", "123:456"], - ["123456 abc", "123456_abc"], + // numbers + ["123456", "123456"], + ["123:456", "123:456"], + ["123456 abc", "123456_abc"], - // non-latin characters - ["ε", "ε"], - ["attribute ε", "attribute_ε"], - - - // special characters - ["test/name", "test_name"], - ["test%name", "test_name"], - ["\/", "_"], - - // empty string - ["", "unnamed"], -] + // non-latin characters + ["ε", "ε"], + ["attribute ε", "attribute_ε"], + // special characters + ["test/name", "test_name"], + ["test%name", "test_name"], + ["\/", "_"], + // empty string + ["", "unnamed"] +]; describe("sanitizeAttributeName unit tests", () => { + testCases.forEach((testCase) => { + return it(`'${testCase[0]}' should return '${testCase[1]}'`, () => { + const [value, expected] = testCase; + const actual = sanitizeAttributeName(value); + expect(actual).toEqual(expected); + }); + }); +}); - testCases.forEach(testCase => { - return it(`'${testCase[0]}' should return '${testCase[1]}'`, () => { - const [value, expected] = testCase; - const actual = sanitizeAttributeName(value); - expect(actual).toEqual(expected); - }) - }) -}) - -execute() \ No newline at end of file +execute(); diff --git a/spec-es6/utils/formatDownloadTitle.spec.ts b/spec-es6/utils/formatDownloadTitle.spec.ts index 923786915..053f79fb7 100644 --- a/spec-es6/utils/formatDownloadTitle.spec.ts +++ b/spec-es6/utils/formatDownloadTitle.spec.ts @@ -2,128 +2,62 @@ import { formatDownloadTitle } from "../../src/services/utils.ts"; import { describe, it, execute, expect } from "../mini_test.ts"; const testCases: [fnValue: Parameters, expectedValue: ReturnType][] = [ - // empty fileName tests - [ - ["", "text", ""], - "untitled.html" - ], + // empty fileName tests + [["", "text", ""], "untitled.html"], - [ - ["", "canvas", ""], - "untitled.json" - ], + [["", "canvas", ""], "untitled.json"], - [ - ["", null, ""], - "untitled" - ], + [["", null, ""], "untitled"], - // json extension from type tests - [ - ["test_file", "canvas", ""], - "test_file.json" - ], + // json extension from type tests + [["test_file", "canvas", ""], "test_file.json"], - [ - ["test_file", "relationMap", ""], - "test_file.json" - ], + [["test_file", "relationMap", ""], "test_file.json"], - [ - ["test_file", "search", ""], - "test_file.json" - ], + [["test_file", "search", ""], "test_file.json"], - // extension based on mime type - [ - ["test_file", null, "text/csv"], - "test_file.csv" - ], + // extension based on mime type + [["test_file", null, "text/csv"], "test_file.csv"], - [ - ["test_file_wo_ext", "image", "image/svg+xml"], - "test_file_wo_ext.svg" - ], + [["test_file_wo_ext", "image", "image/svg+xml"], "test_file_wo_ext.svg"], - [ - ["test_file_wo_ext", "file", "application/json"], - "test_file_wo_ext.json" - ], + [["test_file_wo_ext", "file", "application/json"], "test_file_wo_ext.json"], - [ - ["test_file_w_fake_ext.ext", "image", "image/svg+xml"], - "test_file_w_fake_ext.ext.svg" - ], + [["test_file_w_fake_ext.ext", "image", "image/svg+xml"], "test_file_w_fake_ext.ext.svg"], - [ - ["test_file_w_correct_ext.svg", "image", "image/svg+xml"], - "test_file_w_correct_ext.svg" - ], + [["test_file_w_correct_ext.svg", "image", "image/svg+xml"], "test_file_w_correct_ext.svg"], - [ - ["test_file_w_correct_ext.svgz", "image", "image/svg+xml"], - "test_file_w_correct_ext.svgz" - ], + [["test_file_w_correct_ext.svgz", "image", "image/svg+xml"], "test_file_w_correct_ext.svgz"], - [ - ["test_file.zip", "file", "application/zip"], - "test_file.zip" - ], + [["test_file.zip", "file", "application/zip"], "test_file.zip"], - [ - ["test_file", "file", "application/zip"], - "test_file.zip" - ], + [["test_file", "file", "application/zip"], "test_file.zip"], - // application/octet-stream tests - [ - ["test_file", "file", "application/octet-stream"], - "test_file" - ], + // application/octet-stream tests + [["test_file", "file", "application/octet-stream"], "test_file"], - [ - ["test_file.zip", "file", "application/octet-stream"], - "test_file.zip" - ], + [["test_file.zip", "file", "application/octet-stream"], "test_file.zip"], - [ - ["test_file.unknown", null, "application/octet-stream"], - "test_file.unknown" - ], + [["test_file.unknown", null, "application/octet-stream"], "test_file.unknown"], - // sanitized filename tests - [ - ["test/file", null, "application/octet-stream"], - "testfile" - ], + // sanitized filename tests + [["test/file", null, "application/octet-stream"], "testfile"], - [ - ["test:file.zip", "file", "application/zip"], - "testfile.zip" - ], + [["test:file.zip", "file", "application/zip"], "testfile.zip"], - [ - [":::", "file", "application/zip"], - ".zip" - ], - - [ - [":::a", "file", "application/zip"], - "a.zip" - ], -] + [[":::", "file", "application/zip"], ".zip"], + [[":::a", "file", "application/zip"], "a.zip"] +]; describe("utils/formatDownloadTitle unit tests", () => { + testCases.forEach((testCase) => { + return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => { + const [value, expected] = testCase; + const actual = formatDownloadTitle(...value); + expect(actual).toEqual(expected); + }); + }); +}); - testCases.forEach(testCase => { - return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => { - const [value, expected] = testCase; - const actual = formatDownloadTitle(...value); - expect(actual).toEqual(expected); - }) - }) - -}) - -execute() \ No newline at end of file +execute(); diff --git a/spec/etapi/app_info.ts b/spec/etapi/app_info.ts index 6ba049357..4bb8c4064 100644 --- a/spec/etapi/app_info.ts +++ b/spec/etapi/app_info.ts @@ -1,8 +1,8 @@ import etapi from "../support/etapi.js"; etapi.describeEtapi("app_info", () => { - it("get", async () => { - const appInfo = await etapi.getEtapi("app-info"); - expect(appInfo.clipperProtocolVersion).toEqual("1.0"); - }); + it("get", async () => { + const appInfo = await etapi.getEtapi("app-info"); + expect(appInfo.clipperProtocolVersion).toEqual("1.0"); + }); }); diff --git a/spec/etapi/backup.ts b/spec/etapi/backup.ts index 2f9532f71..78aa01d7f 100644 --- a/spec/etapi/backup.ts +++ b/spec/etapi/backup.ts @@ -1,8 +1,8 @@ import etapi from "../support/etapi.js"; etapi.describeEtapi("backup", () => { - it("create", async () => { - const response = await etapi.putEtapiContent("backup/etapi_test"); - expect(response.status).toEqual(204); - }); + it("create", async () => { + const response = await etapi.putEtapiContent("backup/etapi_test"); + expect(response.status).toEqual(204); + }); }); diff --git a/spec/etapi/import.ts b/spec/etapi/import.ts index d800075d4..d36a01c58 100644 --- a/spec/etapi/import.ts +++ b/spec/etapi/import.ts @@ -4,28 +4,21 @@ import path from "path"; import { fileURLToPath } from "url"; etapi.describeEtapi("import", () => { - // temporarily skip this test since test-export.zip is missing - xit("import", async () => { - const scriptDir = path.dirname(fileURLToPath(import.meta.url)); + // temporarily skip this test since test-export.zip is missing + xit("import", async () => { + const scriptDir = path.dirname(fileURLToPath(import.meta.url)); - const zipFileBuffer = fs.readFileSync( - path.resolve(scriptDir, "test-export.zip") - ); + const zipFileBuffer = fs.readFileSync(path.resolve(scriptDir, "test-export.zip")); - const response = await etapi.postEtapiContent( - "notes/root/import", - zipFileBuffer - ); - expect(response.status).toEqual(201); + const response = await etapi.postEtapiContent("notes/root/import", zipFileBuffer); + expect(response.status).toEqual(201); - const { note, branch } = await response.json(); + const { note, branch } = await response.json(); - expect(note.title).toEqual("test-export"); - expect(branch.parentNoteId).toEqual("root"); + expect(note.title).toEqual("test-export"); + expect(branch.parentNoteId).toEqual("root"); - const content = await ( - await etapi.getEtapiContent(`notes/${note.noteId}/content`) - ).text(); - expect(content).toContain("test export content"); - }); + const content = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text(); + expect(content).toContain("test export content"); + }); }); diff --git a/spec/etapi/notes.spec.ts b/spec/etapi/notes.spec.ts index ce0df713d..bf7e0a282 100644 --- a/spec/etapi/notes.spec.ts +++ b/spec/etapi/notes.spec.ts @@ -1,5 +1,3 @@ describe("Notes", () => { - it("zzz", () => { - - }); + it("zzz", () => {}); }); diff --git a/spec/etapi/notes.ts b/spec/etapi/notes.ts index 3db54ab1b..6efcd88a1 100644 --- a/spec/etapi/notes.ts +++ b/spec/etapi/notes.ts @@ -2,106 +2,100 @@ import crypto from "crypto"; import etapi from "../support/etapi.js"; etapi.describeEtapi("notes", () => { - it("create", async () => { - const { note, branch } = await etapi.postEtapi("create-note", { - parentNoteId: "root", - type: "text", - title: "Hello World!", - content: "Content", - prefix: "Custom prefix", + it("create", async () => { + const { note, branch } = await etapi.postEtapi("create-note", { + parentNoteId: "root", + type: "text", + title: "Hello World!", + content: "Content", + prefix: "Custom prefix" + }); + + expect(note.title).toEqual("Hello World!"); + expect(branch.parentNoteId).toEqual("root"); + expect(branch.prefix).toEqual("Custom prefix"); + + const rNote = await etapi.getEtapi(`notes/${note.noteId}`); + expect(rNote.title).toEqual("Hello World!"); + + const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text(); + expect(rContent).toEqual("Content"); + + const rBranch = await etapi.getEtapi(`branches/${branch.branchId}`); + expect(rBranch.parentNoteId).toEqual("root"); + expect(rBranch.prefix).toEqual("Custom prefix"); }); - expect(note.title).toEqual("Hello World!"); - expect(branch.parentNoteId).toEqual("root"); - expect(branch.prefix).toEqual("Custom prefix"); + it("patch", async () => { + const { note } = await etapi.postEtapi("create-note", { + parentNoteId: "root", + type: "text", + title: "Hello World!", + content: "Content" + }); - const rNote = await etapi.getEtapi(`notes/${note.noteId}`); - expect(rNote.title).toEqual("Hello World!"); + await etapi.patchEtapi(`notes/${note.noteId}`, { + title: "new title", + type: "code", + mime: "text/apl", + dateCreated: "2000-01-01 12:34:56.999+0200", + utcDateCreated: "2000-01-01 10:34:56.999Z" + }); - const rContent = await ( - await etapi.getEtapiContent(`notes/${note.noteId}/content`) - ).text(); - expect(rContent).toEqual("Content"); - - const rBranch = await etapi.getEtapi(`branches/${branch.branchId}`); - expect(rBranch.parentNoteId).toEqual("root"); - expect(rBranch.prefix).toEqual("Custom prefix"); - }); - - it("patch", async () => { - const { note } = await etapi.postEtapi("create-note", { - parentNoteId: "root", - type: "text", - title: "Hello World!", - content: "Content", + const rNote = await etapi.getEtapi(`notes/${note.noteId}`); + expect(rNote.title).toEqual("new title"); + expect(rNote.type).toEqual("code"); + expect(rNote.mime).toEqual("text/apl"); + expect(rNote.dateCreated).toEqual("2000-01-01 12:34:56.999+0200"); + expect(rNote.utcDateCreated).toEqual("2000-01-01 10:34:56.999Z"); }); - await etapi.patchEtapi(`notes/${note.noteId}`, { - title: "new title", - type: "code", - mime: "text/apl", - dateCreated: "2000-01-01 12:34:56.999+0200", - utcDateCreated: "2000-01-01 10:34:56.999Z", + it("update content", async () => { + const { note } = await etapi.postEtapi("create-note", { + parentNoteId: "root", + type: "text", + title: "Hello World!", + content: "Content" + }); + + await etapi.putEtapiContent(`notes/${note.noteId}/content`, "new content"); + + const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text(); + expect(rContent).toEqual("new content"); }); - const rNote = await etapi.getEtapi(`notes/${note.noteId}`); - expect(rNote.title).toEqual("new title"); - expect(rNote.type).toEqual("code"); - expect(rNote.mime).toEqual("text/apl"); - expect(rNote.dateCreated).toEqual("2000-01-01 12:34:56.999+0200"); - expect(rNote.utcDateCreated).toEqual("2000-01-01 10:34:56.999Z"); - }); + it("create / update binary content", async () => { + const { note } = await etapi.postEtapi("create-note", { + parentNoteId: "root", + type: "file", + title: "Hello World!", + content: "ZZZ" + }); - it("update content", async () => { - const { note } = await etapi.postEtapi("create-note", { - parentNoteId: "root", - type: "text", - title: "Hello World!", - content: "Content", + const updatedContent = crypto.randomBytes(16); + + await etapi.putEtapiContent(`notes/${note.noteId}/content`, updatedContent); + + const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).arrayBuffer(); + expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent); }); - await etapi.putEtapiContent(`notes/${note.noteId}/content`, "new content"); + it("delete note", async () => { + const { note } = await etapi.postEtapi("create-note", { + parentNoteId: "root", + type: "text", + title: "Hello World!", + content: "Content" + }); - const rContent = await ( - await etapi.getEtapiContent(`notes/${note.noteId}/content`) - ).text(); - expect(rContent).toEqual("new content"); - }); + await etapi.deleteEtapi(`notes/${note.noteId}`); - it("create / update binary content", async () => { - const { note } = await etapi.postEtapi("create-note", { - parentNoteId: "root", - type: "file", - title: "Hello World!", - content: "ZZZ", + const resp = await etapi.getEtapiResponse(`notes/${note.noteId}`); + expect(resp.status).toEqual(404); + + const error = await resp.json(); + expect(error.status).toEqual(404); + expect(error.code).toEqual("NOTE_NOT_FOUND"); + expect(error.message).toEqual(`Note '${note.noteId}' not found.`); }); - - const updatedContent = crypto.randomBytes(16); - - await etapi.putEtapiContent(`notes/${note.noteId}/content`, updatedContent); - - const rContent = await ( - await etapi.getEtapiContent(`notes/${note.noteId}/content`) - ).arrayBuffer(); - expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent); - }); - - it("delete note", async () => { - const { note } = await etapi.postEtapi("create-note", { - parentNoteId: "root", - type: "text", - title: "Hello World!", - content: "Content", - }); - - await etapi.deleteEtapi(`notes/${note.noteId}`); - - const resp = await etapi.getEtapiResponse(`notes/${note.noteId}`); - expect(resp.status).toEqual(404); - - const error = await resp.json(); - expect(error.status).toEqual(404); - expect(error.code).toEqual("NOTE_NOT_FOUND"); - expect(error.message).toEqual(`Note '${note.noteId}' not found.`); - }); }); diff --git a/spec/search/becca_mocking.ts b/spec/search/becca_mocking.ts index 33bbb63bb..0bc7116cb 100644 --- a/spec/search/becca_mocking.ts +++ b/spec/search/becca_mocking.ts @@ -7,81 +7,76 @@ import SearchResult from "../../src/services/search/search_result.js"; import { NoteType } from "../../src/becca/entities/rows.js"; randtoken.generator({ source: "crypto" }); -function findNoteByTitle( - searchResults: Array, - title: string -): BNote | undefined { - return searchResults - .map((sr) => becca.notes[sr.noteId]) - .find((note) => note.title === title); +function findNoteByTitle(searchResults: Array, title: string): BNote | undefined { + return searchResults.map((sr) => becca.notes[sr.noteId]).find((note) => note.title === title); } class NoteBuilder { - note: BNote; - constructor(note: BNote) { - this.note = note; - } + note: BNote; + constructor(note: BNote) { + this.note = note; + } - label(name: string, value = "", isInheritable = false) { - new BAttribute({ - attributeId: id(), - noteId: this.note.noteId, - type: "label", - isInheritable, - name, - value, - }); + label(name: string, value = "", isInheritable = false) { + new BAttribute({ + attributeId: id(), + noteId: this.note.noteId, + type: "label", + isInheritable, + name, + value + }); - return this; - } + return this; + } - relation(name: string, targetNote: BNote) { - new BAttribute({ - attributeId: id(), - noteId: this.note.noteId, - type: "relation", - name, - value: targetNote.noteId, - }); + relation(name: string, targetNote: BNote) { + new BAttribute({ + attributeId: id(), + noteId: this.note.noteId, + type: "relation", + name, + value: targetNote.noteId + }); - return this; - } + return this; + } - child(childNoteBuilder: NoteBuilder, prefix = "") { - new BBranch({ - branchId: id(), - noteId: childNoteBuilder.note.noteId, - parentNoteId: this.note.noteId, - prefix, - notePosition: 10, - }); + child(childNoteBuilder: NoteBuilder, prefix = "") { + new BBranch({ + branchId: id(), + noteId: childNoteBuilder.note.noteId, + parentNoteId: this.note.noteId, + prefix, + notePosition: 10 + }); - return this; - } + return this; + } } function id() { - return randtoken.generate(10); + return randtoken.generate(10); } function note(title: string, extraParams = {}) { - const row = Object.assign( - { - noteId: id(), - title: title, - type: "text" as NoteType, - mime: "text/html", - }, - extraParams - ); + const row = Object.assign( + { + noteId: id(), + title: title, + type: "text" as NoteType, + mime: "text/html" + }, + extraParams + ); - const note = new BNote(row); + const note = new BNote(row); - return new NoteBuilder(note); + return new NoteBuilder(note); } export default { - NoteBuilder, - findNoteByTitle, - note, + NoteBuilder, + findNoteByTitle, + note }; diff --git a/spec/search/lexer.spec.ts b/spec/search/lexer.spec.ts index 2e3179fa3..c489b03e6 100644 --- a/spec/search/lexer.spec.ts +++ b/spec/search/lexer.spec.ts @@ -1,256 +1,162 @@ import lex from "../../src/services/search/services/lex.js"; describe("Lexer fulltext", () => { - it("simple lexing", () => { - expect(lex("hello world").fulltextTokens.map((t) => t.token)).toEqual([ - "hello", - "world", - ]); + it("simple lexing", () => { + expect(lex("hello world").fulltextTokens.map((t) => t.token)).toEqual(["hello", "world"]); - expect(lex("hello, world").fulltextTokens.map((t) => t.token)).toEqual([ - "hello", - "world", - ]); - }); + expect(lex("hello, world").fulltextTokens.map((t) => t.token)).toEqual(["hello", "world"]); + }); - it("use quotes to keep words together", () => { - expect( - lex("'hello world' my friend").fulltextTokens.map((t) => t.token) - ).toEqual(["hello world", "my", "friend"]); + it("use quotes to keep words together", () => { + expect(lex("'hello world' my friend").fulltextTokens.map((t) => t.token)).toEqual(["hello world", "my", "friend"]); - expect( - lex('"hello world" my friend').fulltextTokens.map((t) => t.token) - ).toEqual(["hello world", "my", "friend"]); + expect(lex('"hello world" my friend').fulltextTokens.map((t) => t.token)).toEqual(["hello world", "my", "friend"]); - expect( - lex("`hello world` my friend").fulltextTokens.map((t) => t.token) - ).toEqual(["hello world", "my", "friend"]); - }); + expect(lex("`hello world` my friend").fulltextTokens.map((t) => t.token)).toEqual(["hello world", "my", "friend"]); + }); - it("you can use different quotes and other special characters inside quotes", () => { - expect( - lex("'i can use \" or ` or #~=*' without problem").fulltextTokens.map( - (t) => t.token - ) - ).toEqual(['i can use " or ` or #~=*', "without", "problem"]); - }); + it("you can use different quotes and other special characters inside quotes", () => { + expect(lex("'i can use \" or ` or #~=*' without problem").fulltextTokens.map((t) => t.token)).toEqual(['i can use " or ` or #~=*', "without", "problem"]); + }); - it("I can use backslash to escape quotes", () => { - expect(lex('hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual( - ["hello", '"world"'] - ); + it("I can use backslash to escape quotes", () => { + expect(lex('hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual(["hello", '"world"']); - expect(lex("hello \\'world\\'").fulltextTokens.map((t) => t.token)).toEqual( - ["hello", "'world'"] - ); + expect(lex("hello \\'world\\'").fulltextTokens.map((t) => t.token)).toEqual(["hello", "'world'"]); - expect(lex("hello \\`world\\`").fulltextTokens.map((t) => t.token)).toEqual( - ["hello", "`world`"] - ); + expect(lex("hello \\`world\\`").fulltextTokens.map((t) => t.token)).toEqual(["hello", "`world`"]); - expect( - lex('"hello \\"world\\"').fulltextTokens.map((t) => t.token) - ).toEqual(['hello "world"']); + expect(lex('"hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual(['hello "world"']); - expect( - lex("'hello \\'world\\''").fulltextTokens.map((t) => t.token) - ).toEqual(["hello 'world'"]); + expect(lex("'hello \\'world\\''").fulltextTokens.map((t) => t.token)).toEqual(["hello 'world'"]); - expect( - lex("`hello \\`world\\``").fulltextTokens.map((t) => t.token) - ).toEqual(["hello `world`"]); + expect(lex("`hello \\`world\\``").fulltextTokens.map((t) => t.token)).toEqual(["hello `world`"]); - expect(lex("\\#token").fulltextTokens.map((t) => t.token)).toEqual([ - "#token", - ]); - }); + expect(lex("\\#token").fulltextTokens.map((t) => t.token)).toEqual(["#token"]); + }); - it("quote inside a word does not have a special meaning", () => { - const lexResult = lex("d'Artagnan is dead #hero = d'Artagnan"); + it("quote inside a word does not have a special meaning", () => { + const lexResult = lex("d'Artagnan is dead #hero = d'Artagnan"); - expect(lexResult.fulltextTokens.map((t) => t.token)).toEqual([ - "d'artagnan", - "is", - "dead", - ]); + expect(lexResult.fulltextTokens.map((t) => t.token)).toEqual(["d'artagnan", "is", "dead"]); - expect(lexResult.expressionTokens.map((t) => t.token)).toEqual([ - "#hero", - "=", - "d'artagnan", - ]); - }); + expect(lexResult.expressionTokens.map((t) => t.token)).toEqual(["#hero", "=", "d'artagnan"]); + }); - it("if quote is not ended then it's just one long token", () => { - expect(lex("'unfinished quote").fulltextTokens.map((t) => t.token)).toEqual( - ["unfinished quote"] - ); - }); + it("if quote is not ended then it's just one long token", () => { + expect(lex("'unfinished quote").fulltextTokens.map((t) => t.token)).toEqual(["unfinished quote"]); + }); - it("parenthesis and symbols in fulltext section are just normal characters", () => { - expect( - lex("what's u=p ").fulltextTokens.map((t) => t.token) - ).toEqual(["what's", "u=p", ""]); - }); + it("parenthesis and symbols in fulltext section are just normal characters", () => { + expect(lex("what's u=p ").fulltextTokens.map((t) => t.token)).toEqual(["what's", "u=p", ""]); + }); - it("operator characters in expressions are separate tokens", () => { - expect( - lex("# abc+=-def**-+d").expressionTokens.map((t) => t.token) - ).toEqual(["#", "abc", "+=-", "def", "**-+", "d"]); - }); + it("operator characters in expressions are separate tokens", () => { + expect(lex("# abc+=-def**-+d").expressionTokens.map((t) => t.token)).toEqual(["#", "abc", "+=-", "def", "**-+", "d"]); + }); - it("escaping special characters", () => { - expect(lex("hello \\#\\~\\'").fulltextTokens.map((t) => t.token)).toEqual([ - "hello", - "#~'", - ]); - }); + it("escaping special characters", () => { + expect(lex("hello \\#\\~\\'").fulltextTokens.map((t) => t.token)).toEqual(["hello", "#~'"]); + }); }); describe("Lexer expression", () => { - it("simple attribute existence", () => { - expect( - lex("#label ~relation").expressionTokens.map((t) => t.token) - ).toEqual(["#label", "~relation"]); - }); + it("simple attribute existence", () => { + expect(lex("#label ~relation").expressionTokens.map((t) => t.token)).toEqual(["#label", "~relation"]); + }); - it("simple label operators", () => { - expect(lex("#label*=*text").expressionTokens.map((t) => t.token)).toEqual([ - "#label", - "*=*", - "text", - ]); - }); + it("simple label operators", () => { + expect(lex("#label*=*text").expressionTokens.map((t) => t.token)).toEqual(["#label", "*=*", "text"]); + }); - it("simple label operator with in quotes", () => { - expect(lex("#label*=*'text'").expressionTokens).toEqual([ - { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, - { token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 }, - { token: "text", inQuotes: true, startIndex: 10, endIndex: 13 }, - ]); - }); + it("simple label operator with in quotes", () => { + expect(lex("#label*=*'text'").expressionTokens).toEqual([ + { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, + { token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 }, + { token: "text", inQuotes: true, startIndex: 10, endIndex: 13 } + ]); + }); - it("simple label operator with param without quotes", () => { - expect(lex("#label*=*text").expressionTokens).toEqual([ - { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, - { token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 }, - { token: "text", inQuotes: false, startIndex: 9, endIndex: 12 }, - ]); - }); + it("simple label operator with param without quotes", () => { + expect(lex("#label*=*text").expressionTokens).toEqual([ + { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, + { token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 }, + { token: "text", inQuotes: false, startIndex: 9, endIndex: 12 } + ]); + }); - it("simple label operator with empty string param", () => { - expect(lex("#label = ''").expressionTokens).toEqual([ - { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, - { token: "=", inQuotes: false, startIndex: 7, endIndex: 7 }, - // weird case for empty strings which ends up with endIndex < startIndex :-( - { token: "", inQuotes: true, startIndex: 10, endIndex: 9 }, - ]); - }); + it("simple label operator with empty string param", () => { + expect(lex("#label = ''").expressionTokens).toEqual([ + { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, + { token: "=", inQuotes: false, startIndex: 7, endIndex: 7 }, + // weird case for empty strings which ends up with endIndex < startIndex :-( + { token: "", inQuotes: true, startIndex: 10, endIndex: 9 } + ]); + }); - it("note. prefix also separates fulltext from expression", () => { - expect( - lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map( - (t) => t.token - ) - ).toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]); - }); + it("note. prefix also separates fulltext from expression", () => { + expect(lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map((t) => t.token)).toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]); + }); - it("note. prefix in quotes will note start expression", () => { - expect( - lex(`hello fulltext "note.txt"`).expressionTokens.map((t) => t.token) - ).toEqual([]); + it("note. prefix in quotes will note start expression", () => { + expect(lex(`hello fulltext "note.txt"`).expressionTokens.map((t) => t.token)).toEqual([]); - expect( - lex(`hello fulltext "note.txt"`).fulltextTokens.map((t) => t.token) - ).toEqual(["hello", "fulltext", "note.txt"]); - }); + expect(lex(`hello fulltext "note.txt"`).fulltextTokens.map((t) => t.token)).toEqual(["hello", "fulltext", "note.txt"]); + }); - it("complex expressions with and, or and parenthesis", () => { - expect( - lex(`# (#label=text OR #second=text) AND ~relation`).expressionTokens.map( - (t) => t.token - ) - ).toEqual([ - "#", - "(", - "#label", - "=", - "text", - "or", - "#second", - "=", - "text", - ")", - "and", - "~relation", - ]); - }); + it("complex expressions with and, or and parenthesis", () => { + expect(lex(`# (#label=text OR #second=text) AND ~relation`).expressionTokens.map((t) => t.token)).toEqual([ + "#", + "(", + "#label", + "=", + "text", + "or", + "#second", + "=", + "text", + ")", + "and", + "~relation" + ]); + }); - it("dot separated properties", () => { - expect( - lex( - `# ~author.title = 'Hugh Howey' AND note.'book title' = 'Silo'` - ).expressionTokens.map((t) => t.token) - ).toEqual([ - "#", - "~author", - ".", - "title", - "=", - "hugh howey", - "and", - "note", - ".", - "book title", - "=", - "silo", - ]); - }); + it("dot separated properties", () => { + expect(lex(`# ~author.title = 'Hugh Howey' AND note.'book title' = 'Silo'`).expressionTokens.map((t) => t.token)).toEqual([ + "#", + "~author", + ".", + "title", + "=", + "hugh howey", + "and", + "note", + ".", + "book title", + "=", + "silo" + ]); + }); - it("negation of label and relation", () => { - expect( - lex(`#!capital ~!neighbor`).expressionTokens.map((t) => t.token) - ).toEqual(["#!capital", "~!neighbor"]); - }); + it("negation of label and relation", () => { + expect(lex(`#!capital ~!neighbor`).expressionTokens.map((t) => t.token)).toEqual(["#!capital", "~!neighbor"]); + }); - it("negation of sub-expression", () => { - expect( - lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map( - (t) => t.token - ) - ).toEqual([ - "#", - "not", - "(", - "#capital", - ")", - "and", - "note", - ".", - "noteid", - "!=", - "root", - ]); - }); + it("negation of sub-expression", () => { + expect(lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map((t) => t.token)).toEqual(["#", "not", "(", "#capital", ")", "and", "note", ".", "noteid", "!=", "root"]); + }); - it("order by multiple labels", () => { - expect(lex(`# orderby #a,#b`).expressionTokens.map((t) => t.token)).toEqual( - ["#", "orderby", "#a", ",", "#b"] - ); - }); + it("order by multiple labels", () => { + expect(lex(`# orderby #a,#b`).expressionTokens.map((t) => t.token)).toEqual(["#", "orderby", "#a", ",", "#b"]); + }); }); describe("Lexer invalid queries and edge cases", () => { - it("concatenated attributes", () => { - expect(lex("#label~relation").expressionTokens.map((t) => t.token)).toEqual( - ["#label", "~relation"] - ); - }); + it("concatenated attributes", () => { + expect(lex("#label~relation").expressionTokens.map((t) => t.token)).toEqual(["#label", "~relation"]); + }); - it("trailing escape \\", () => { - expect(lex("abc \\").fulltextTokens.map((t) => t.token)).toEqual([ - "abc", - "\\", - ]); - }); + it("trailing escape \\", () => { + expect(lex("abc \\").fulltextTokens.map((t) => t.token)).toEqual(["abc", "\\"]); + }); }); diff --git a/spec/search/parens.spec.ts b/spec/search/parens.spec.ts index 3bc900b8b..cea7fc5de 100644 --- a/spec/search/parens.spec.ts +++ b/spec/search/parens.spec.ts @@ -3,23 +3,9 @@ import { TokenStructure } from "../../src/services/search/services/types.js"; describe("Parens handler", () => { it("handles parens", () => { - const input = ["(", "hello", ")", "and", "(", "(", "pick", "one", ")", "and", "another", ")"] - .map(token => ({token})); + const input = ["(", "hello", ")", "and", "(", "(", "pick", "one", ")", "and", "another", ")"].map((token) => ({ token })); - const actual: TokenStructure = [ - [ - {token: "hello"} - ], - {token: "and"}, - [ - [ - {token: "pick"}, - {token: "one"} - ], - {token: "and"}, - {token: "another"} - ] - ]; + const actual: TokenStructure = [[{ token: "hello" }], { token: "and" }, [[{ token: "pick" }, { token: "one" }], { token: "and" }, { token: "another" }]]; expect(handleParens(input)).toEqual(actual); }); diff --git a/spec/search/parser.spec.ts b/spec/search/parser.spec.ts index 003036664..cf5d3315d 100644 --- a/spec/search/parser.spec.ts +++ b/spec/search/parser.spec.ts @@ -17,301 +17,297 @@ function tokens(toks: Array, cur = 0): Array { token: arg, inQuotes: false, startIndex: cur - arg.length, - endIndex: cur - 1, + endIndex: cur - 1 }; } }); } function assertIsArchived(exp: Expression) { - expect(exp.constructor.name).toEqual('PropertyComparisonExp'); - expect(exp.propertyName).toEqual('isArchived'); - expect(exp.operator).toEqual('='); - expect(exp.comparedValue).toEqual('false'); + expect(exp.constructor.name).toEqual("PropertyComparisonExp"); + expect(exp.propertyName).toEqual("isArchived"); + expect(exp.operator).toEqual("="); + expect(exp.comparedValue).toEqual("false"); } -describe('Parser', () => { - it('fulltext parser without content', () => { +describe("Parser", () => { + it("fulltext parser without content", () => { const rootExp = parse({ - fulltextTokens: tokens(['hello', 'hi']), + fulltextTokens: tokens(["hello", "hi"]), expressionTokens: [], - searchContext: new SearchContext({ excludeArchived: true }), + searchContext: new SearchContext({ excludeArchived: true }) }); - expect(rootExp.constructor.name).toEqual('AndExp'); - expect(rootExp.subExpressions[0].constructor.name).toEqual('PropertyComparisonExp'); - expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp'); - expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual('NoteFlatTextExp'); - expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(['hello', 'hi']); + expect(rootExp.constructor.name).toEqual("AndExp"); + expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp"); + expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); + expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); + expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(["hello", "hi"]); }); - it('fulltext parser with content', () => { + it("fulltext parser with content", () => { const rootExp = parse({ - fulltextTokens: tokens(['hello', 'hi']), + fulltextTokens: tokens(["hello", "hi"]), expressionTokens: [], - searchContext: new SearchContext(), + searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.constructor.name).toEqual("AndExp"); assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp'); + expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); const subs = rootExp.subExpressions[2].subExpressions; - expect(subs[0].constructor.name).toEqual('NoteFlatTextExp'); - expect(subs[0].tokens).toEqual(['hello', 'hi']); + expect(subs[0].constructor.name).toEqual("NoteFlatTextExp"); + expect(subs[0].tokens).toEqual(["hello", "hi"]); - expect(subs[1].constructor.name).toEqual('NoteContentFulltextExp'); - expect(subs[1].tokens).toEqual(['hello', 'hi']); + expect(subs[1].constructor.name).toEqual("NoteContentFulltextExp"); + expect(subs[1].tokens).toEqual(["hello", "hi"]); }); - it('simple label comparison', () => { + it("simple label comparison", () => { const rootExp = parse({ fulltextTokens: [], - expressionTokens: tokens(['#mylabel', '=', 'text']), - searchContext: new SearchContext(), + expressionTokens: tokens(["#mylabel", "=", "text"]), + searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.constructor.name).toEqual("AndExp"); assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual('LabelComparisonExp'); - expect(rootExp.subExpressions[2].attributeType).toEqual('label'); - expect(rootExp.subExpressions[2].attributeName).toEqual('mylabel'); + expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp"); + expect(rootExp.subExpressions[2].attributeType).toEqual("label"); + expect(rootExp.subExpressions[2].attributeName).toEqual("mylabel"); expect(rootExp.subExpressions[2].comparator).toBeTruthy(); }); - it('simple attribute negation', () => { + it("simple attribute negation", () => { let rootExp = parse({ fulltextTokens: [], - expressionTokens: tokens(['#!mylabel']), - searchContext: new SearchContext(), + expressionTokens: tokens(["#!mylabel"]), + searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.constructor.name).toEqual("AndExp"); assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual('NotExp'); - expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual('AttributeExistsExp'); - expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual('label'); - expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual('mylabel'); + expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp"); + expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp"); + expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("label"); + expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("mylabel"); rootExp = parse({ fulltextTokens: [], - expressionTokens: tokens(['~!myrelation']), - searchContext: new SearchContext(), + expressionTokens: tokens(["~!myrelation"]), + searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.constructor.name).toEqual("AndExp"); assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual('NotExp'); - expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual('AttributeExistsExp'); - expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual('relation'); - expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual('myrelation'); + expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp"); + expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp"); + expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("relation"); + expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("myrelation"); }); - it('simple label AND', () => { + it("simple label AND", () => { const rootExp = parse({ fulltextTokens: [], - expressionTokens: tokens(['#first', '=', 'text', 'and', '#second', '=', 'text']), - searchContext: new SearchContext(true), + expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]), + searchContext: new SearchContext(true) }); - expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.constructor.name).toEqual("AndExp"); assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp'); + expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - expect(firstSub.constructor.name).toEqual('LabelComparisonExp'); - expect(firstSub.attributeName).toEqual('first'); + expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); + expect(firstSub.attributeName).toEqual("first"); - expect(secondSub.constructor.name).toEqual('LabelComparisonExp'); - expect(secondSub.attributeName).toEqual('second'); + expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); + expect(secondSub.attributeName).toEqual("second"); }); - it('simple label AND without explicit AND', () => { + it("simple label AND without explicit AND", () => { const rootExp = parse({ fulltextTokens: [], - expressionTokens: tokens(['#first', '=', 'text', '#second', '=', 'text']), - searchContext: new SearchContext(), + expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]), + searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.constructor.name).toEqual("AndExp"); assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp'); + expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - expect(firstSub.constructor.name).toEqual('LabelComparisonExp'); - expect(firstSub.attributeName).toEqual('first'); + expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); + expect(firstSub.attributeName).toEqual("first"); - expect(secondSub.constructor.name).toEqual('LabelComparisonExp'); - expect(secondSub.attributeName).toEqual('second'); + expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); + expect(secondSub.attributeName).toEqual("second"); }); - it('simple label OR', () => { + it("simple label OR", () => { const rootExp = parse({ fulltextTokens: [], - expressionTokens: tokens(['#first', '=', 'text', 'or', '#second', '=', 'text']), - searchContext: new SearchContext(), + expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]), + searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.constructor.name).toEqual("AndExp"); assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp'); + expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - expect(firstSub.constructor.name).toEqual('LabelComparisonExp'); - expect(firstSub.attributeName).toEqual('first'); + expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); + expect(firstSub.attributeName).toEqual("first"); - expect(secondSub.constructor.name).toEqual('LabelComparisonExp'); - expect(secondSub.attributeName).toEqual('second'); + expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); + expect(secondSub.attributeName).toEqual("second"); }); - it('fulltext and simple label', () => { + it("fulltext and simple label", () => { const rootExp = parse({ - fulltextTokens: tokens(['hello']), - expressionTokens: tokens(['#mylabel', '=', 'text']), - searchContext: new SearchContext({ excludeArchived: true }), + fulltextTokens: tokens(["hello"]), + expressionTokens: tokens(["#mylabel", "=", "text"]), + searchContext: new SearchContext({ excludeArchived: true }) }); - expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.constructor.name).toEqual("AndExp"); const [firstSub, secondSub, thirdSub, fourth] = rootExp.subExpressions; - expect(firstSub.constructor.name).toEqual('PropertyComparisonExp'); - expect(firstSub.propertyName).toEqual('isArchived'); + expect(firstSub.constructor.name).toEqual("PropertyComparisonExp"); + expect(firstSub.propertyName).toEqual("isArchived"); - expect(thirdSub.constructor.name).toEqual('OrExp'); - expect(thirdSub.subExpressions[0].constructor.name).toEqual('NoteFlatTextExp'); - expect(thirdSub.subExpressions[0].tokens).toEqual(['hello']); + expect(thirdSub.constructor.name).toEqual("OrExp"); + expect(thirdSub.subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); + expect(thirdSub.subExpressions[0].tokens).toEqual(["hello"]); - expect(fourth.constructor.name).toEqual('LabelComparisonExp'); - expect(fourth.attributeName).toEqual('mylabel'); + expect(fourth.constructor.name).toEqual("LabelComparisonExp"); + expect(fourth.attributeName).toEqual("mylabel"); }); - it('label sub-expression', () => { + it("label sub-expression", () => { const rootExp = parse({ fulltextTokens: [], - expressionTokens: tokens(['#first', '=', 'text', 'or', ['#second', '=', 'text', 'and', '#third', '=', 'text']]), - searchContext: new SearchContext(), + expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]), + searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.constructor.name).toEqual("AndExp"); assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp'); + expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - expect(firstSub.constructor.name).toEqual('LabelComparisonExp'); - expect(firstSub.attributeName).toEqual('first'); + expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); + expect(firstSub.attributeName).toEqual("first"); - expect(secondSub.constructor.name).toEqual('AndExp'); + expect(secondSub.constructor.name).toEqual("AndExp"); const [firstSubSub, secondSubSub] = secondSub.subExpressions; - expect(firstSubSub.constructor.name).toEqual('LabelComparisonExp'); - expect(firstSubSub.attributeName).toEqual('second'); + expect(firstSubSub.constructor.name).toEqual("LabelComparisonExp"); + expect(firstSubSub.attributeName).toEqual("second"); - expect(secondSubSub.constructor.name).toEqual('LabelComparisonExp'); - expect(secondSubSub.attributeName).toEqual('third'); + expect(secondSubSub.constructor.name).toEqual("LabelComparisonExp"); + expect(secondSubSub.attributeName).toEqual("third"); }); - it('label sub-expression without explicit operator', () => { + it("label sub-expression without explicit operator", () => { const rootExp = parse({ fulltextTokens: [], - expressionTokens: tokens(['#first', ['#second', 'or', '#third'], '#fourth']), - searchContext: new SearchContext(), + expressionTokens: tokens(["#first", ["#second", "or", "#third"], "#fourth"]), + searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.constructor.name).toEqual("AndExp"); assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp'); + expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); const [firstSub, secondSub, thirdSub] = rootExp.subExpressions[2].subExpressions; - expect(firstSub.constructor.name).toEqual('AttributeExistsExp'); - expect(firstSub.attributeName).toEqual('first'); + expect(firstSub.constructor.name).toEqual("AttributeExistsExp"); + expect(firstSub.attributeName).toEqual("first"); - expect(secondSub.constructor.name).toEqual('OrExp'); + expect(secondSub.constructor.name).toEqual("OrExp"); const [firstSubSub, secondSubSub] = secondSub.subExpressions; - expect(firstSubSub.constructor.name).toEqual('AttributeExistsExp'); - expect(firstSubSub.attributeName).toEqual('second'); + expect(firstSubSub.constructor.name).toEqual("AttributeExistsExp"); + expect(firstSubSub.attributeName).toEqual("second"); - expect(secondSubSub.constructor.name).toEqual('AttributeExistsExp'); - expect(secondSubSub.attributeName).toEqual('third'); + expect(secondSubSub.constructor.name).toEqual("AttributeExistsExp"); + expect(secondSubSub.attributeName).toEqual("third"); - expect(thirdSub.constructor.name).toEqual('AttributeExistsExp'); - expect(thirdSub.attributeName).toEqual('fourth'); + expect(thirdSub.constructor.name).toEqual("AttributeExistsExp"); + expect(thirdSub.attributeName).toEqual("fourth"); }); }); -describe('Invalid expressions', () => { - it('incomplete comparison', () => { +describe("Invalid expressions", () => { + it("incomplete comparison", () => { const searchContext = new SearchContext(); parse({ fulltextTokens: [], - expressionTokens: tokens(['#first', '=']), - searchContext, + expressionTokens: tokens(["#first", "="]), + searchContext }); expect(searchContext.error).toEqual('Misplaced or incomplete expression "="'); }); - it('comparison between labels is impossible', () => { + it("comparison between labels is impossible", () => { let searchContext = new SearchContext(); - searchContext.originalQuery = '#first = #second'; + searchContext.originalQuery = "#first = #second"; parse({ fulltextTokens: [], - expressionTokens: tokens(['#first', '=', '#second']), - searchContext, + expressionTokens: tokens(["#first", "=", "#second"]), + searchContext }); - expect(searchContext.error).toEqual( - `Error near token "#second" in "#first = #second", it's possible to compare with constant only.` - ); + expect(searchContext.error).toEqual(`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`); searchContext = new SearchContext(); - searchContext.originalQuery = '#first = note.relations.second'; + searchContext.originalQuery = "#first = note.relations.second"; parse({ fulltextTokens: [], - expressionTokens: tokens(['#first', '=', 'note', '.', 'relations', 'second']), - searchContext, + expressionTokens: tokens(["#first", "=", "note", ".", "relations", "second"]), + searchContext }); - expect(searchContext.error).toEqual( - `Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.` - ); + expect(searchContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`); const rootExp = parse({ fulltextTokens: [], expressionTokens: [ - { token: '#first', inQuotes: false }, - { token: '=', inQuotes: false }, - { token: '#second', inQuotes: true }, + { token: "#first", inQuotes: false }, + { token: "=", inQuotes: false }, + { token: "#second", inQuotes: true } ], - searchContext: new SearchContext(), + searchContext: new SearchContext() }); - expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.constructor.name).toEqual("AndExp"); assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual('LabelComparisonExp'); - expect(rootExp.subExpressions[2].attributeType).toEqual('label'); - expect(rootExp.subExpressions[2].attributeName).toEqual('first'); + expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp"); + expect(rootExp.subExpressions[2].attributeType).toEqual("label"); + expect(rootExp.subExpressions[2].attributeName).toEqual("first"); expect(rootExp.subExpressions[2].comparator).toBeTruthy(); }); - it('searching by relation without note property', () => { + it("searching by relation without note property", () => { const searchContext = new SearchContext(); parse({ fulltextTokens: [], - expressionTokens: tokens(['~first', '=', 'text', '-', 'abc']), - searchContext, + expressionTokens: tokens(["~first", "=", "text", "-", "abc"]), + searchContext }); expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""'); diff --git a/spec/search/search.spec.ts b/spec/search/search.spec.ts index a058ae895..0099546ad 100644 --- a/spec/search/search.spec.ts +++ b/spec/search/search.spec.ts @@ -6,195 +6,177 @@ import dateUtils from "../../src/services/date_utils.js"; import becca from "../../src/becca/becca.js"; import becca_mocking from "./becca_mocking.js"; -describe('Search', () => { +describe("Search", () => { let rootNote: any; beforeEach(() => { becca.reset(); - rootNote = new becca_mocking.NoteBuilder(new BNote({ noteId: 'root', title: 'root', type: 'text' })); + rootNote = new becca_mocking.NoteBuilder(new BNote({ noteId: "root", title: "root", type: "text" })); new BBranch({ - branchId: 'none_root', - noteId: 'root', - parentNoteId: 'none', - notePosition: 10, + branchId: "none_root", + noteId: "root", + parentNoteId: "none", + notePosition: 10 }); }); - xit('simple path match', () => { - rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria'))); + xit("simple path match", () => { + rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria"))); const searchContext = new SearchContext(); - const searchResults = searchService.findResultsWithQuery('europe austria', searchContext); + const searchResults = searchService.findResultsWithQuery("europe austria", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); - xit('normal search looks also at attributes', () => { - const austria = becca_mocking.note('Austria'); - const vienna = becca_mocking.note('Vienna'); + xit("normal search looks also at attributes", () => { + const austria = becca_mocking.note("Austria"); + const vienna = becca_mocking.note("Vienna"); - rootNote.child(austria.relation('capital', vienna.note)).child(vienna.label('inhabitants', '1888776')); + rootNote.child(austria.relation("capital", vienna.note)).child(vienna.label("inhabitants", "1888776")); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('capital', searchContext); + let searchResults = searchService.findResultsWithQuery("capital", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - searchResults = searchService.findResultsWithQuery('inhabitants', searchContext); + searchResults = searchService.findResultsWithQuery("inhabitants", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Vienna')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Vienna")).toBeTruthy(); }); - xit('normal search looks also at type and mime', () => { - rootNote - .child(becca_mocking.note('Effective Java', { type: 'book', mime: '' })) - .child(becca_mocking.note('Hello World.java', { type: 'code', mime: 'text/x-java' })); + xit("normal search looks also at type and mime", () => { + rootNote.child(becca_mocking.note("Effective Java", { type: "book", mime: "" })).child(becca_mocking.note("Hello World.java", { type: "code", mime: "text/x-java" })); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('book', searchContext); + let searchResults = searchService.findResultsWithQuery("book", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Effective Java')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Effective Java")).toBeTruthy(); - searchResults = searchService.findResultsWithQuery('text', searchContext); // should match mime + searchResults = searchService.findResultsWithQuery("text", searchContext); // should match mime expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Hello World.java')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Hello World.java")).toBeTruthy(); - searchResults = searchService.findResultsWithQuery('java', searchContext); + searchResults = searchService.findResultsWithQuery("java", searchContext); expect(searchResults.length).toEqual(2); }); - xit('only end leafs are results', () => { - rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria'))); + xit("only end leafs are results", () => { + rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria"))); const searchContext = new SearchContext(); - const searchResults = searchService.findResultsWithQuery('europe', searchContext); + const searchResults = searchService.findResultsWithQuery("europe", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy(); }); - xit('only end leafs are results', () => { - rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria').label('capital', 'Vienna'))); + xit("only end leafs are results", () => { + rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria").label("capital", "Vienna"))); const searchContext = new SearchContext(); - const searchResults = searchService.findResultsWithQuery('Vienna', searchContext); + const searchResults = searchService.findResultsWithQuery("Vienna", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); - it('label comparison with short syntax', () => { - rootNote.child( - becca_mocking - .note('Europe') - .child(becca_mocking.note('Austria').label('capital', 'Vienna')) - .child(becca_mocking.note('Czech Republic').label('capital', 'Prague')) - ); + it("label comparison with short syntax", () => { + rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria").label("capital", "Vienna")).child(becca_mocking.note("Czech Republic").label("capital", "Prague"))); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('#capital=Vienna', searchContext); + let searchResults = searchService.findResultsWithQuery("#capital=Vienna", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); // case sensitivity: - searchResults = searchService.findResultsWithQuery('#CAPITAL=VIENNA', searchContext); + searchResults = searchService.findResultsWithQuery("#CAPITAL=VIENNA", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - searchResults = searchService.findResultsWithQuery('#caPItal=vienNa', searchContext); + searchResults = searchService.findResultsWithQuery("#caPItal=vienNa", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); - it('label comparison with full syntax', () => { + it("label comparison with full syntax", () => { + rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria").label("capital", "Vienna")).child(becca_mocking.note("Czech Republic").label("capital", "Prague"))); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery("# note.labels.capital=Prague", searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); + }); + + it("numeric label comparison", () => { rootNote.child( becca_mocking - .note('Europe') - .child(becca_mocking.note('Austria').label('capital', 'Vienna')) - .child(becca_mocking.note('Czech Republic').label('capital', 'Prague')) + .note("Europe") + .label("country", "", true) + .child(becca_mocking.note("Austria").label("population", "8859000")) + .child(becca_mocking.note("Czech Republic").label("population", "10650000")) ); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('# note.labels.capital=Prague', searchContext); + const searchResults = searchService.findResultsWithQuery("#country #population >= 10000000", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); }); - it('numeric label comparison', () => { - rootNote.child( - becca_mocking - .note('Europe') - .label('country', '', true) - .child(becca_mocking.note('Austria').label('population', '8859000')) - .child(becca_mocking.note('Czech Republic').label('population', '10650000')) - ); + xit("inherited label comparison", () => { + rootNote.child(becca_mocking.note("Europe").label("country", "", true).child(becca_mocking.note("Austria")).child(becca_mocking.note("Czech Republic"))); const searchContext = new SearchContext(); - const searchResults = searchService.findResultsWithQuery('#country #population >= 10000000', searchContext); + const searchResults = searchService.findResultsWithQuery("austria #country", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); - xit('inherited label comparison', () => { - rootNote.child( - becca_mocking - .note('Europe') - .label('country', '', true) - .child(becca_mocking.note('Austria')) - .child(becca_mocking.note('Czech Republic')) - ); - - const searchContext = new SearchContext(); - - const searchResults = searchService.findResultsWithQuery('austria #country', searchContext); - expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); - }); - - it('numeric label comparison fallback to string comparison', () => { + it("numeric label comparison fallback to string comparison", () => { // dates should not be coerced into numbers which would then give wrong numbers rootNote.child( becca_mocking - .note('Europe') - .label('country', '', true) - .child(becca_mocking.note('Austria').label('established', '1955-07-27')) - .child(becca_mocking.note('Czech Republic').label('established', '1993-01-01')) - .child(becca_mocking.note('Hungary').label('established', '1920-06-04')) + .note("Europe") + .label("country", "", true) + .child(becca_mocking.note("Austria").label("established", "1955-07-27")) + .child(becca_mocking.note("Czech Republic").label("established", "1993-01-01")) + .child(becca_mocking.note("Hungary").label("established", "1920-06-04")) ); const searchContext = new SearchContext(); let searchResults = searchService.findResultsWithQuery('#established <= "1955-01-01"', searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Hungary')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Hungary")).toBeTruthy(); searchResults = searchService.findResultsWithQuery('#established > "1955-01-01"', searchContext); expect(searchResults.length).toEqual(2); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); - expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); }); - it('smart date comparisons', () => { + it("smart date comparisons", () => { // dates should not be coerced into numbers which would then give wrong numbers rootNote.child( becca_mocking - .note('My note', { dateCreated: dateUtils.localNowDateTime() }) - .label('year', new Date().getFullYear().toString()) - .label('month', dateUtils.localNowDate().substr(0, 7)) - .label('date', dateUtils.localNowDate()) - .label('dateTime', dateUtils.localNowDateTime()) + .note("My note", { dateCreated: dateUtils.localNowDateTime() }) + .label("year", new Date().getFullYear().toString()) + .label("month", dateUtils.localNowDate().substr(0, 7)) + .label("date", dateUtils.localNowDate()) + .label("dateTime", dateUtils.localNowDateTime()) ); const searchContext = new SearchContext(); @@ -206,263 +188,258 @@ describe('Search', () => { .toEqual(expectedResultCount); if (expectedResultCount === 1) { - expect(becca_mocking.findNoteByTitle(searchResults, 'My note')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "My note")).toBeTruthy(); } } - test('#year = YEAR', 1); + test("#year = YEAR", 1); test("#year = 'YEAR'", 0); - test('#year >= YEAR', 1); - test('#year <= YEAR', 1); - test('#year < YEAR+1', 1); - test('#year < YEAR + 1', 1); - test('#year < year + 1', 1); - test('#year > YEAR+1', 0); + test("#year >= YEAR", 1); + test("#year <= YEAR", 1); + test("#year < YEAR+1", 1); + test("#year < YEAR + 1", 1); + test("#year < year + 1", 1); + test("#year > YEAR+1", 0); - test('#month = MONTH', 1); - test('#month = month', 1); + test("#month = MONTH", 1); + test("#month = month", 1); test("#month = 'MONTH'", 0); - test('note.dateCreated =* month', 2); + test("note.dateCreated =* month", 2); - test('#date = TODAY', 1); - test('#date = today', 1); + test("#date = TODAY", 1); + test("#date = today", 1); test("#date = 'today'", 0); - test('#date > TODAY', 0); - test('#date > TODAY-1', 1); - test('#date > TODAY - 1', 1); - test('#date < TODAY+1', 1); - test('#date < TODAY + 1', 1); + test("#date > TODAY", 0); + test("#date > TODAY-1", 1); + test("#date > TODAY - 1", 1); + test("#date < TODAY+1", 1); + test("#date < TODAY + 1", 1); test("#date < 'TODAY + 1'", 1); - test('#dateTime <= NOW+10', 1); - test('#dateTime <= NOW + 10', 1); - test('#dateTime < NOW-10', 0); - test('#dateTime >= NOW-10', 1); - test('#dateTime < NOW-10', 0); + test("#dateTime <= NOW+10", 1); + test("#dateTime <= NOW + 10", 1); + test("#dateTime < NOW-10", 0); + test("#dateTime >= NOW-10", 1); + test("#dateTime < NOW-10", 0); }); - it('logical or', () => { + it("logical or", () => { rootNote.child( becca_mocking - .note('Europe') - .label('country', '', true) - .child(becca_mocking.note('Austria').label('languageFamily', 'germanic')) - .child(becca_mocking.note('Czech Republic').label('languageFamily', 'slavic')) - .child(becca_mocking.note('Hungary').label('languageFamily', 'finnougric')) + .note("Europe") + .label("country", "", true) + .child(becca_mocking.note("Austria").label("languageFamily", "germanic")) + .child(becca_mocking.note("Czech Republic").label("languageFamily", "slavic")) + .child(becca_mocking.note("Hungary").label("languageFamily", "finnougric")) ); const searchContext = new SearchContext(); - const searchResults = searchService.findResultsWithQuery('#languageFamily = slavic OR #languageFamily = germanic', searchContext); + const searchResults = searchService.findResultsWithQuery("#languageFamily = slavic OR #languageFamily = germanic", searchContext); expect(searchResults.length).toEqual(2); - expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); - it('fuzzy attribute search', () => { + it("fuzzy attribute search", () => { rootNote.child( becca_mocking - .note('Europe') - .label('country', '', true) - .child(becca_mocking.note('Austria').label('languageFamily', 'germanic')) - .child(becca_mocking.note('Czech Republic').label('languageFamily', 'slavic')) + .note("Europe") + .label("country", "", true) + .child(becca_mocking.note("Austria").label("languageFamily", "germanic")) + .child(becca_mocking.note("Czech Republic").label("languageFamily", "slavic")) ); let searchContext = new SearchContext({ fuzzyAttributeSearch: false }); - let searchResults = searchService.findResultsWithQuery('#language', searchContext); + let searchResults = searchService.findResultsWithQuery("#language", searchContext); expect(searchResults.length).toEqual(0); - searchResults = searchService.findResultsWithQuery('#languageFamily=ger', searchContext); + searchResults = searchService.findResultsWithQuery("#languageFamily=ger", searchContext); expect(searchResults.length).toEqual(0); searchContext = new SearchContext({ fuzzyAttributeSearch: true }); - searchResults = searchService.findResultsWithQuery('#language', searchContext); + searchResults = searchService.findResultsWithQuery("#language", searchContext); expect(searchResults.length).toEqual(2); - searchResults = searchService.findResultsWithQuery('#languageFamily=ger', searchContext); + searchResults = searchService.findResultsWithQuery("#languageFamily=ger", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); - it('filter by note property', () => { - rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria')).child(becca_mocking.note('Czech Republic'))); + it("filter by note property", () => { + rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")).child(becca_mocking.note("Czech Republic"))); const searchContext = new SearchContext(); - const searchResults = searchService.findResultsWithQuery('# note.title =* czech', searchContext); + const searchResults = searchService.findResultsWithQuery("# note.title =* czech", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); }); it("filter by note's parent", () => { rootNote .child( becca_mocking - .note('Europe') - .child(becca_mocking.note('Austria')) - .child(becca_mocking.note('Czech Republic').child(becca_mocking.note('Prague'))) + .note("Europe") + .child(becca_mocking.note("Austria")) + .child(becca_mocking.note("Czech Republic").child(becca_mocking.note("Prague"))) ) - .child(becca_mocking.note('Asia').child(becca_mocking.note('Taiwan'))); + .child(becca_mocking.note("Asia").child(becca_mocking.note("Taiwan"))); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe', searchContext); + let searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe", searchContext); expect(searchResults.length).toEqual(2); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); - expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - searchResults = searchService.findResultsWithQuery('# note.parents.title = Asia', searchContext); + searchResults = searchService.findResultsWithQuery("# note.parents.title = Asia", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Taiwan')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Taiwan")).toBeTruthy(); - searchResults = searchService.findResultsWithQuery('# note.parents.parents.title = Europe', searchContext); + searchResults = searchService.findResultsWithQuery("# note.parents.parents.title = Europe", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Prague')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Prague")).toBeTruthy(); }); it("filter by note's ancestor", () => { rootNote .child( becca_mocking - .note('Europe') - .child(becca_mocking.note('Austria')) - .child(becca_mocking.note('Czech Republic').child(becca_mocking.note('Prague').label('city'))) + .note("Europe") + .child(becca_mocking.note("Austria")) + .child(becca_mocking.note("Czech Republic").child(becca_mocking.note("Prague").label("city"))) ) - .child(becca_mocking.note('Asia').child(becca_mocking.note('Taiwan').child(becca_mocking.note('Taipei').label('city')))); + .child(becca_mocking.note("Asia").child(becca_mocking.note("Taiwan").child(becca_mocking.note("Taipei").label("city")))); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Europe', searchContext); + let searchResults = searchService.findResultsWithQuery("#city AND note.ancestors.title = Europe", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Prague')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Prague")).toBeTruthy(); - searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Asia', searchContext); + searchResults = searchService.findResultsWithQuery("#city AND note.ancestors.title = Asia", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Taipei')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Taipei")).toBeTruthy(); }); it("filter by note's child", () => { rootNote .child( becca_mocking - .note('Europe') - .child(becca_mocking.note('Austria').child(becca_mocking.note('Vienna'))) - .child(becca_mocking.note('Czech Republic').child(becca_mocking.note('Prague'))) + .note("Europe") + .child(becca_mocking.note("Austria").child(becca_mocking.note("Vienna"))) + .child(becca_mocking.note("Czech Republic").child(becca_mocking.note("Prague"))) ) - .child(becca_mocking.note('Oceania').child(becca_mocking.note('Australia'))); + .child(becca_mocking.note("Oceania").child(becca_mocking.note("Australia"))); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('# note.children.title =* Aust', searchContext); + let searchResults = searchService.findResultsWithQuery("# note.children.title =* Aust", searchContext); expect(searchResults.length).toEqual(2); - expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); - expect(becca_mocking.findNoteByTitle(searchResults, 'Oceania')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Oceania")).toBeTruthy(); - searchResults = searchService.findResultsWithQuery( - '# note.children.title =* Aust AND note.children.title *= republic', - searchContext - ); + searchResults = searchService.findResultsWithQuery("# note.children.title =* Aust AND note.children.title *= republic", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy(); - searchResults = searchService.findResultsWithQuery('# note.children.children.title = Prague', searchContext); + searchResults = searchService.findResultsWithQuery("# note.children.children.title = Prague", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy(); }); it("filter by relation's note properties using short syntax", () => { - const austria = becca_mocking.note('Austria'); - const portugal = becca_mocking.note('Portugal'); + const austria = becca_mocking.note("Austria"); + const portugal = becca_mocking.note("Portugal"); rootNote.child( becca_mocking - .note('Europe') + .note("Europe") .child(austria) - .child(becca_mocking.note('Czech Republic').relation('neighbor', austria.note)) + .child(becca_mocking.note("Czech Republic").relation("neighbor", austria.note)) .child(portugal) - .child(becca_mocking.note('Spain').relation('neighbor', portugal.note)) + .child(becca_mocking.note("Spain").relation("neighbor", portugal.note)) ); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('# ~neighbor.title = Austria', searchContext); + let searchResults = searchService.findResultsWithQuery("# ~neighbor.title = Austria", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - searchResults = searchService.findResultsWithQuery('# ~neighbor.title = Portugal', searchContext); + searchResults = searchService.findResultsWithQuery("# ~neighbor.title = Portugal", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Spain')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Spain")).toBeTruthy(); }); it("filter by relation's note properties using long syntax", () => { - const austria = becca_mocking.note('Austria'); - const portugal = becca_mocking.note('Portugal'); + const austria = becca_mocking.note("Austria"); + const portugal = becca_mocking.note("Portugal"); rootNote.child( becca_mocking - .note('Europe') + .note("Europe") .child(austria) - .child(becca_mocking.note('Czech Republic').relation('neighbor', austria.note)) + .child(becca_mocking.note("Czech Republic").relation("neighbor", austria.note)) .child(portugal) - .child(becca_mocking.note('Spain').relation('neighbor', portugal.note)) + .child(becca_mocking.note("Spain").relation("neighbor", portugal.note)) ); const searchContext = new SearchContext(); - const searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.title = Austria', searchContext); + const searchResults = searchService.findResultsWithQuery("# note.relations.neighbor.title = Austria", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); }); - it('filter by multiple level relation', () => { - const austria = becca_mocking.note('Austria'); - const slovakia = becca_mocking.note('Slovakia'); - const italy = becca_mocking.note('Italy'); - const ukraine = becca_mocking.note('Ukraine'); + it("filter by multiple level relation", () => { + const austria = becca_mocking.note("Austria"); + const slovakia = becca_mocking.note("Slovakia"); + const italy = becca_mocking.note("Italy"); + const ukraine = becca_mocking.note("Ukraine"); rootNote.child( becca_mocking - .note('Europe') - .child(austria.relation('neighbor', italy.note).relation('neighbor', slovakia.note)) - .child(becca_mocking.note('Czech Republic').relation('neighbor', austria.note).relation('neighbor', slovakia.note)) - .child(slovakia.relation('neighbor', ukraine.note)) + .note("Europe") + .child(austria.relation("neighbor", italy.note).relation("neighbor", slovakia.note)) + .child(becca_mocking.note("Czech Republic").relation("neighbor", austria.note).relation("neighbor", slovakia.note)) + .child(slovakia.relation("neighbor", ukraine.note)) .child(ukraine) ); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.relations.neighbor.title = Italy', searchContext); + let searchResults = searchService.findResultsWithQuery("# note.relations.neighbor.relations.neighbor.title = Italy", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.relations.neighbor.title = Ukraine', searchContext); + searchResults = searchService.findResultsWithQuery("# note.relations.neighbor.relations.neighbor.title = Ukraine", searchContext); expect(searchResults.length).toEqual(2); - expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); - expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); - it('test note properties', () => { - const austria = becca_mocking.note('Austria'); + it("test note properties", () => { + const austria = becca_mocking.note("Austria"); - austria.relation('myself', austria.note); - austria.label('capital', 'Vienna'); - austria.label('population', '8859000'); + austria.relation("myself", austria.note); + austria.label("capital", "Vienna"); + austria.label("population", "8859000"); rootNote - .child(becca_mocking.note('Asia')) - .child( - becca_mocking.note('Europe').child(austria.child(becca_mocking.note('Vienna')).child(becca_mocking.note('Sebastian Kurz'))) - ) - .child(becca_mocking.note('Mozart').child(austria)); + .child(becca_mocking.note("Asia")) + .child(becca_mocking.note("Europe").child(austria.child(becca_mocking.note("Vienna")).child(becca_mocking.note("Sebastian Kurz")))) + .child(becca_mocking.note("Mozart").child(austria)); austria.note.isProtected = false; - austria.note.dateCreated = '2020-05-14 12:11:42.001+0200'; - austria.note.dateModified = '2020-05-14 13:11:42.001+0200'; - austria.note.utcDateCreated = '2020-05-14 10:11:42.001Z'; - austria.note.utcDateModified = '2020-05-14 11:11:42.001Z'; + austria.note.dateCreated = "2020-05-14 12:11:42.001+0200"; + austria.note.dateModified = "2020-05-14 13:11:42.001+0200"; + austria.note.utcDateCreated = "2020-05-14 10:11:42.001Z"; + austria.note.utcDateModified = "2020-05-14 11:11:42.001Z"; // austria.note.contentLength = 1001; const searchContext = new SearchContext(); @@ -472,141 +449,130 @@ describe('Search', () => { expect(searchResults.length).toEqual(expectedResultCount); } - test('type', 'text', 7); - test('TYPE', 'TEXT', 7); - test('type', 'code', 0); + test("type", "text", 7); + test("TYPE", "TEXT", 7); + test("type", "code", 0); - test('mime', 'text/html', 6); - test('mime', 'application/json', 0); + test("mime", "text/html", 6); + test("mime", "application/json", 0); - test('isProtected', 'false', 7); - test('isProtected', 'FALSE', 7); - test('isProtected', 'true', 0); - test('isProtected', 'TRUE', 0); + test("isProtected", "false", 7); + test("isProtected", "FALSE", 7); + test("isProtected", "true", 0); + test("isProtected", "TRUE", 0); - test('dateCreated', "'2020-05-14 12:11:42.001+0200'", 1); - test('dateCreated', 'wrong', 0); + test("dateCreated", "'2020-05-14 12:11:42.001+0200'", 1); + test("dateCreated", "wrong", 0); - test('dateModified', "'2020-05-14 13:11:42.001+0200'", 1); - test('dateModified', 'wrong', 0); + test("dateModified", "'2020-05-14 13:11:42.001+0200'", 1); + test("dateModified", "wrong", 0); - test('utcDateCreated', "'2020-05-14 10:11:42.001Z'", 1); - test('utcDateCreated', 'wrong', 0); + test("utcDateCreated", "'2020-05-14 10:11:42.001Z'", 1); + test("utcDateCreated", "wrong", 0); - test('utcDateModified', "'2020-05-14 11:11:42.001Z'", 1); - test('utcDateModified', 'wrong', 0); + test("utcDateModified", "'2020-05-14 11:11:42.001Z'", 1); + test("utcDateModified", "wrong", 0); - test('parentCount', '2', 1); - test('parentCount', '3', 0); + test("parentCount", "2", 1); + test("parentCount", "3", 0); - test('childrenCount', '2', 1); - test('childrenCount', '10', 0); + test("childrenCount", "2", 1); + test("childrenCount", "10", 0); - test('attributeCount', '3', 1); - test('attributeCount', '4', 0); + test("attributeCount", "3", 1); + test("attributeCount", "4", 0); - test('labelCount', '2', 1); - test('labelCount', '3', 0); + test("labelCount", "2", 1); + test("labelCount", "3", 0); - test('relationCount', '1', 1); - test('relationCount', '2', 0); + test("relationCount", "1", 1); + test("relationCount", "2", 0); }); - it('test order by', () => { - const italy = becca_mocking.note('Italy').label('capital', 'Rome'); - const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); - const austria = becca_mocking.note('Austria').label('capital', 'Vienna'); - const ukraine = becca_mocking.note('Ukraine').label('capital', 'Kiev'); + it("test order by", () => { + const italy = becca_mocking.note("Italy").label("capital", "Rome"); + const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava"); + const austria = becca_mocking.note("Austria").label("capital", "Vienna"); + const ukraine = becca_mocking.note("Ukraine").label("capital", "Kiev"); - rootNote.child(becca_mocking.note('Europe').child(ukraine).child(slovakia).child(austria).child(italy)); + rootNote.child(becca_mocking.note("Europe").child(ukraine).child(slovakia).child(austria).child(italy)); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.title', searchContext); + let searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy note.title", searchContext); expect(searchResults.length).toEqual(4); - expect(becca.notes[searchResults[0].noteId].title).toEqual('Austria'); - expect(becca.notes[searchResults[1].noteId].title).toEqual('Italy'); - expect(becca.notes[searchResults[2].noteId].title).toEqual('Slovakia'); - expect(becca.notes[searchResults[3].noteId].title).toEqual('Ukraine'); + expect(becca.notes[searchResults[0].noteId].title).toEqual("Austria"); + expect(becca.notes[searchResults[1].noteId].title).toEqual("Italy"); + expect(becca.notes[searchResults[2].noteId].title).toEqual("Slovakia"); + expect(becca.notes[searchResults[3].noteId].title).toEqual("Ukraine"); - searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.labels.capital', searchContext); + searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy note.labels.capital", searchContext); expect(searchResults.length).toEqual(4); - expect(becca.notes[searchResults[0].noteId].title).toEqual('Slovakia'); - expect(becca.notes[searchResults[1].noteId].title).toEqual('Ukraine'); - expect(becca.notes[searchResults[2].noteId].title).toEqual('Italy'); - expect(becca.notes[searchResults[3].noteId].title).toEqual('Austria'); + expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia"); + expect(becca.notes[searchResults[1].noteId].title).toEqual("Ukraine"); + expect(becca.notes[searchResults[2].noteId].title).toEqual("Italy"); + expect(becca.notes[searchResults[3].noteId].title).toEqual("Austria"); - searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.labels.capital DESC', searchContext); + searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy note.labels.capital DESC", searchContext); expect(searchResults.length).toEqual(4); - expect(becca.notes[searchResults[0].noteId].title).toEqual('Austria'); - expect(becca.notes[searchResults[1].noteId].title).toEqual('Italy'); - expect(becca.notes[searchResults[2].noteId].title).toEqual('Ukraine'); - expect(becca.notes[searchResults[3].noteId].title).toEqual('Slovakia'); + expect(becca.notes[searchResults[0].noteId].title).toEqual("Austria"); + expect(becca.notes[searchResults[1].noteId].title).toEqual("Italy"); + expect(becca.notes[searchResults[2].noteId].title).toEqual("Ukraine"); + expect(becca.notes[searchResults[3].noteId].title).toEqual("Slovakia"); - searchResults = searchService.findResultsWithQuery( - '# note.parents.title = Europe orderBy note.labels.capital DESC limit 2', - searchContext - ); + searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy note.labels.capital DESC limit 2", searchContext); expect(searchResults.length).toEqual(2); - expect(becca.notes[searchResults[0].noteId].title).toEqual('Austria'); - expect(becca.notes[searchResults[1].noteId].title).toEqual('Italy'); + expect(becca.notes[searchResults[0].noteId].title).toEqual("Austria"); + expect(becca.notes[searchResults[1].noteId].title).toEqual("Italy"); - searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1', searchContext); + searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy #capital DESC limit 1", searchContext); expect(searchResults.length).toEqual(1); - searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', searchContext); + searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy #capital DESC limit 1000", searchContext); expect(searchResults.length).toEqual(4); }); - it('test not(...)', () => { - const italy = becca_mocking.note('Italy').label('capital', 'Rome'); - const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); + it("test not(...)", () => { + const italy = becca_mocking.note("Italy").label("capital", "Rome"); + const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava"); - rootNote.child(becca_mocking.note('Europe').child(slovakia).child(italy)); + rootNote.child(becca_mocking.note("Europe").child(slovakia).child(italy)); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('# not(#capital) and note.noteId != root', searchContext); + let searchResults = searchService.findResultsWithQuery("# not(#capital) and note.noteId != root", searchContext); expect(searchResults.length).toEqual(1); - expect(becca.notes[searchResults[0].noteId].title).toEqual('Europe'); + expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe"); - searchResults = searchService.findResultsWithQuery('#!capital and note.noteId != root', searchContext); + searchResults = searchService.findResultsWithQuery("#!capital and note.noteId != root", searchContext); expect(searchResults.length).toEqual(1); - expect(becca.notes[searchResults[0].noteId].title).toEqual('Europe'); + expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe"); }); - xit('test note.text *=* something', () => { - const italy = becca_mocking.note('Italy').label('capital', 'Rome'); - const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); + xit("test note.text *=* something", () => { + const italy = becca_mocking.note("Italy").label("capital", "Rome"); + const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava"); - rootNote.child(becca_mocking.note('Europe').child(slovakia).child(italy)); + rootNote.child(becca_mocking.note("Europe").child(slovakia).child(italy)); const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('# note.text *=* vaki and note.noteId != root', searchContext); + let searchResults = searchService.findResultsWithQuery("# note.text *=* vaki and note.noteId != root", searchContext); expect(searchResults.length).toEqual(1); - expect(becca.notes[searchResults[0].noteId].title).toEqual('Slovakia'); + expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia"); }); - xit('test that fulltext does not match archived notes', () => { - const italy = becca_mocking.note('Italy').label('capital', 'Rome'); - const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); + xit("test that fulltext does not match archived notes", () => { + const italy = becca_mocking.note("Italy").label("capital", "Rome"); + const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava"); - rootNote - .child( - becca_mocking - .note('Reddit') - .label('archived', '', true) - .child(becca_mocking.note('Post X')) - .child(becca_mocking.note('Post Y')) - ) - .child(becca_mocking.note('Reddit is bad')); + rootNote.child(becca_mocking.note("Reddit").label("archived", "", true).child(becca_mocking.note("Post X")).child(becca_mocking.note("Post Y"))).child(becca_mocking.note("Reddit is bad")); const searchContext = new SearchContext({ includeArchivedNotes: false }); - let searchResults = searchService.findResultsWithQuery('reddit', searchContext); + let searchResults = searchService.findResultsWithQuery("reddit", searchContext); expect(searchResults.length).toEqual(1); - expect(becca.notes[searchResults[0].noteId].title).toEqual('Reddit is bad'); + expect(becca.notes[searchResults[0].noteId].title).toEqual("Reddit is bad"); }); // FIXME: test what happens when we order without any filter criteria diff --git a/spec/search/value_extractor.spec.ts b/spec/search/value_extractor.spec.ts index 1672ddbb6..4155a7e51 100644 --- a/spec/search/value_extractor.spec.ts +++ b/spec/search/value_extractor.spec.ts @@ -5,77 +5,74 @@ import SearchContext from "../../src/services/search/search_context.js"; const dsc = new SearchContext(); -describe('Value extractor', () => { +describe("Value extractor", () => { beforeEach(() => { becca.reset(); }); - it('simple title extraction', async () => { - const europe = becca_mocking.note('Europe').note; + it("simple title extraction", async () => { + const europe = becca_mocking.note("Europe").note; - const valueExtractor = new ValueExtractor(dsc, ['note', 'title']); + const valueExtractor = new ValueExtractor(dsc, ["note", "title"]); expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(europe)).toEqual('Europe'); + expect(valueExtractor.extract(europe)).toEqual("Europe"); }); - it('label extraction', async () => { - const austria = becca_mocking.note('Austria').label('Capital', 'Vienna').note; + it("label extraction", async () => { + const austria = becca_mocking.note("Austria").label("Capital", "Vienna").note; - let valueExtractor = new ValueExtractor(dsc, ['note', 'labels', 'capital']); + let valueExtractor = new ValueExtractor(dsc, ["note", "labels", "capital"]); expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(austria)).toEqual('Vienna'); + expect(valueExtractor.extract(austria)).toEqual("Vienna"); - valueExtractor = new ValueExtractor(dsc, ['#capital']); + valueExtractor = new ValueExtractor(dsc, ["#capital"]); expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(austria)).toEqual('Vienna'); + expect(valueExtractor.extract(austria)).toEqual("Vienna"); }); - it('parent/child property extraction', async () => { - const vienna = becca_mocking.note('Vienna'); - const europe = becca_mocking.note('Europe').child(becca_mocking.note('Austria').child(vienna)); + it("parent/child property extraction", async () => { + const vienna = becca_mocking.note("Vienna"); + const europe = becca_mocking.note("Europe").child(becca_mocking.note("Austria").child(vienna)); - let valueExtractor = new ValueExtractor(dsc, ['note', 'children', 'children', 'title']); + let valueExtractor = new ValueExtractor(dsc, ["note", "children", "children", "title"]); expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(europe.note)).toEqual('Vienna'); + expect(valueExtractor.extract(europe.note)).toEqual("Vienna"); - valueExtractor = new ValueExtractor(dsc, ['note', 'parents', 'parents', 'title']); + valueExtractor = new ValueExtractor(dsc, ["note", "parents", "parents", "title"]); expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(vienna.note)).toEqual('Europe'); + expect(valueExtractor.extract(vienna.note)).toEqual("Europe"); }); - it('extract through relation', async () => { - const czechRepublic = becca_mocking.note('Czech Republic').label('capital', 'Prague'); - const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); - const austria = becca_mocking.note('Austria').relation('neighbor', czechRepublic.note).relation('neighbor', slovakia.note); + it("extract through relation", async () => { + const czechRepublic = becca_mocking.note("Czech Republic").label("capital", "Prague"); + const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava"); + const austria = becca_mocking.note("Austria").relation("neighbor", czechRepublic.note).relation("neighbor", slovakia.note); - let valueExtractor = new ValueExtractor(dsc, ['note', 'relations', 'neighbor', 'labels', 'capital']); + let valueExtractor = new ValueExtractor(dsc, ["note", "relations", "neighbor", "labels", "capital"]); expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(austria.note)).toEqual('Prague'); + expect(valueExtractor.extract(austria.note)).toEqual("Prague"); - valueExtractor = new ValueExtractor(dsc, ['~neighbor', 'labels', 'capital']); + valueExtractor = new ValueExtractor(dsc, ["~neighbor", "labels", "capital"]); expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(austria.note)).toEqual('Prague'); + expect(valueExtractor.extract(austria.note)).toEqual("Prague"); }); }); -describe('Invalid value extractor property path', () => { - it('each path must start with "note" (or label/relation)', () => expect(new ValueExtractor(dsc, ['neighbor']).validate()).toBeTruthy()); +describe("Invalid value extractor property path", () => { + it('each path must start with "note" (or label/relation)', () => expect(new ValueExtractor(dsc, ["neighbor"]).validate()).toBeTruthy()); - it('extra path element after terminal label', () => - expect(new ValueExtractor(dsc, ['~neighbor', 'labels', 'capital', 'noteId']).validate()).toBeTruthy()); + it("extra path element after terminal label", () => expect(new ValueExtractor(dsc, ["~neighbor", "labels", "capital", "noteId"]).validate()).toBeTruthy()); - it('extra path element after terminal title', () => - expect(new ValueExtractor(dsc, ['note', 'title', 'isProtected']).validate()).toBeTruthy()); + it("extra path element after terminal title", () => expect(new ValueExtractor(dsc, ["note", "title", "isProtected"]).validate()).toBeTruthy()); - it('relation name and note property is missing', () => expect(new ValueExtractor(dsc, ['note', 'relations']).validate()).toBeTruthy()); + it("relation name and note property is missing", () => expect(new ValueExtractor(dsc, ["note", "relations"]).validate()).toBeTruthy()); - it('relation is specified but target note property is not specified', () => - expect(new ValueExtractor(dsc, ['note', 'relations', 'myrel']).validate()).toBeTruthy()); + it("relation is specified but target note property is not specified", () => expect(new ValueExtractor(dsc, ["note", "relations", "myrel"]).validate()).toBeTruthy()); }); diff --git a/spec/support/etapi.ts b/spec/support/etapi.ts index 2738a34a2..3e6e1ac98 100644 --- a/spec/support/etapi.ts +++ b/spec/support/etapi.ts @@ -2,176 +2,153 @@ import child_process from "child_process"; let etapiAuthToken: string | undefined; -const getEtapiAuthorizationHeader = (): string => - "Basic " + Buffer.from(`etapi:${etapiAuthToken}`).toString("base64"); +const getEtapiAuthorizationHeader = (): string => "Basic " + Buffer.from(`etapi:${etapiAuthToken}`).toString("base64"); const PORT: string = "9999"; const HOST: string = "http://localhost:" + PORT; type SpecDefinitionsFunc = () => void; -function describeEtapi( - description: string, - specDefinitions: SpecDefinitionsFunc -): void { - describe(description, () => { - let appProcess: ReturnType; +function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void { + describe(description, () => { + let appProcess: ReturnType; - beforeAll(async () => { + beforeAll(async () => {}); + afterAll(() => {}); + + specDefinitions(); }); - - afterAll(() => { - - }); - - specDefinitions(); - }); } async function getEtapiResponse(url: string): Promise { - return await fetch(`${HOST}/etapi/${url}`, { - method: "GET", - headers: { - Authorization: getEtapiAuthorizationHeader(), - }, - }); + return await fetch(`${HOST}/etapi/${url}`, { + method: "GET", + headers: { + Authorization: getEtapiAuthorizationHeader() + } + }); } async function getEtapi(url: string): Promise { - const response = await getEtapiResponse(url); - return await processEtapiResponse(response); + const response = await getEtapiResponse(url); + return await processEtapiResponse(response); } async function getEtapiContent(url: string): Promise { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: "GET", - headers: { - Authorization: getEtapiAuthorizationHeader(), - }, - }); + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "GET", + headers: { + Authorization: getEtapiAuthorizationHeader() + } + }); - checkStatus(response); + checkStatus(response); - return response; + return response; } -async function postEtapi( - url: string, - data: Record = {} -): Promise { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: getEtapiAuthorizationHeader(), - }, - body: JSON.stringify(data), - }); - return await processEtapiResponse(response); +async function postEtapi(url: string, data: Record = {}): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: getEtapiAuthorizationHeader() + }, + body: JSON.stringify(data) + }); + return await processEtapiResponse(response); } -async function postEtapiContent( - url: string, - data: BodyInit -): Promise { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: "POST", - headers: { - "Content-Type": "application/octet-stream", - Authorization: getEtapiAuthorizationHeader(), - }, - body: data, - }); +async function postEtapiContent(url: string, data: BodyInit): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "POST", + headers: { + "Content-Type": "application/octet-stream", + Authorization: getEtapiAuthorizationHeader() + }, + body: data + }); - checkStatus(response); + checkStatus(response); - return response; + return response; } -async function putEtapi( - url: string, - data: Record = {} -): Promise { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: getEtapiAuthorizationHeader(), - }, - body: JSON.stringify(data), - }); - return await processEtapiResponse(response); +async function putEtapi(url: string, data: Record = {}): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: getEtapiAuthorizationHeader() + }, + body: JSON.stringify(data) + }); + return await processEtapiResponse(response); } -async function putEtapiContent( - url: string, - data?: BodyInit -): Promise { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: "PUT", - headers: { - "Content-Type": "application/octet-stream", - Authorization: getEtapiAuthorizationHeader(), - }, - body: data, - }); +async function putEtapiContent(url: string, data?: BodyInit): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "PUT", + headers: { + "Content-Type": "application/octet-stream", + Authorization: getEtapiAuthorizationHeader() + }, + body: data + }); - checkStatus(response); + checkStatus(response); - return response; + return response; } -async function patchEtapi( - url: string, - data: Record = {} -): Promise { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - Authorization: getEtapiAuthorizationHeader(), - }, - body: JSON.stringify(data), - }); - return await processEtapiResponse(response); +async function patchEtapi(url: string, data: Record = {}): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: getEtapiAuthorizationHeader() + }, + body: JSON.stringify(data) + }); + return await processEtapiResponse(response); } async function deleteEtapi(url: string): Promise { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: "DELETE", - headers: { - Authorization: getEtapiAuthorizationHeader(), - }, - }); - return await processEtapiResponse(response); + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "DELETE", + headers: { + Authorization: getEtapiAuthorizationHeader() + } + }); + return await processEtapiResponse(response); } async function processEtapiResponse(response: Response): Promise { - const text = await response.text(); + const text = await response.text(); - if (response.status < 200 || response.status >= 300) { - throw new Error(`ETAPI error ${response.status}: ${text}`); - } + if (response.status < 200 || response.status >= 300) { + throw new Error(`ETAPI error ${response.status}: ${text}`); + } - return text?.trim() ? JSON.parse(text) : null; + return text?.trim() ? JSON.parse(text) : null; } function checkStatus(response: Response): void { - if (response.status < 200 || response.status >= 300) { - throw new Error(`ETAPI error ${response.status}`); - } + if (response.status < 200 || response.status >= 300) { + throw new Error(`ETAPI error ${response.status}`); + } } export default { - describeEtapi, - getEtapi, - getEtapiResponse, - getEtapiContent, - postEtapi, - postEtapiContent, - putEtapi, - putEtapiContent, - patchEtapi, - deleteEtapi, + describeEtapi, + getEtapi, + getEtapiResponse, + getEtapiContent, + postEtapi, + postEtapiContent, + putEtapi, + putEtapiContent, + patchEtapi, + deleteEtapi }; diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index 1a1e6b68e..5043a9a03 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -1,7 +1,7 @@ { - "spec_dir": "spec", - "spec_files": ["./**/*.spec.ts"], - "helpers": ["helpers/**/*.js"], - "stopSpecOnExpectationFailure": false, - "random": true + "spec_dir": "spec", + "spec_files": ["./**/*.spec.ts"], + "helpers": ["helpers/**/*.js"], + "stopSpecOnExpectationFailure": false, + "random": true } diff --git a/spec/support/utils.spec.ts b/spec/support/utils.spec.ts index c385d0729..566e225d8 100644 --- a/spec/support/utils.spec.ts +++ b/spec/support/utils.spec.ts @@ -5,8 +5,7 @@ describe("Utils", () => { expect(trimIndentation`\ Hello world - 123` - ).toBe(`\ + 123`).toBe(`\ Hello world 123`); diff --git a/spec/support/utils.ts b/spec/support/utils.ts index 1e38fbe84..8735c9651 100644 --- a/spec/support/utils.ts +++ b/spec/support/utils.ts @@ -3,16 +3,16 @@ export function trimIndentation(strings: TemplateStringsArray) { // Count the number of spaces on the first line. let numSpaces = 0; - while (str.charAt(numSpaces) == ' ' && numSpaces < str.length) { + while (str.charAt(numSpaces) == " " && numSpaces < str.length) { numSpaces++; } // Trim the indentation of the first line in all the lines. const lines = str.split("\n"); const output = []; - for (let i=0; i { try { console.log("Starting anonymization..."); - const resp = await anonymizationService.createAnonymizedCopy('full'); + const resp = await anonymizationService.createAnonymizedCopy("full"); if (resp.success) { console.log(`Anonymized file has been saved to: ${resp.anonymizedFilePath}`); @@ -15,8 +15,7 @@ sqlInit.dbReady.then(async () => { } else { console.log("Anonymization failed."); } - } - catch (e: any) { + } catch (e: any) { console.error(e.message, e.stack); } diff --git a/src/app.ts b/src/app.ts index a3943b713..3977daf2e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,8 +16,8 @@ import { startScheduledCleanup } from "./services/erase.js"; import sql_init from "./services/sql_init.js"; import { t } from "i18next"; -await import('./services/handlers.js'); -await import('./becca/becca_loader.js'); +await import("./services/handlers.js"); +await import("./becca/becca_loader.js"); const app = express(); @@ -27,8 +27,8 @@ const scriptDir = dirname(fileURLToPath(import.meta.url)); sql_init.initializeDb(); // view engine setup -app.set('views', path.join(scriptDir, 'views')); -app.set('view engine', 'ejs'); +app.set("views", path.join(scriptDir, "views")); +app.set("view engine", "ejs"); app.use((req, res, next) => { res.locals.t = t; @@ -39,21 +39,23 @@ if (!utils.isElectron()) { app.use(compression()); // HTTP compression } -app.use(helmet({ - hidePoweredBy: false, // errors out in electron - contentSecurityPolicy: false, - crossOriginEmbedderPolicy: false -})); +app.use( + helmet({ + hidePoweredBy: false, // errors out in electron + contentSecurityPolicy: false, + crossOriginEmbedderPolicy: false + }) +); -app.use(express.text({ limit: '500mb' })); -app.use(express.json({ limit: '500mb' })); -app.use(express.raw({ limit: '500mb' })); +app.use(express.text({ limit: "500mb" })); +app.use(express.json({ limit: "500mb" })); +app.use(express.raw({ limit: "500mb" })); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); -app.use(express.static(path.join(scriptDir, 'public/root'))); -app.use(`/manifest.webmanifest`, express.static(path.join(scriptDir, 'public/manifest.webmanifest'))); -app.use(`/robots.txt`, express.static(path.join(scriptDir, 'public/robots.txt'))); -app.use(`/icon.png`, express.static(path.join(scriptDir, 'public/icon.png'))); +app.use(express.static(path.join(scriptDir, "public/root"))); +app.use(`/manifest.webmanifest`, express.static(path.join(scriptDir, "public/manifest.webmanifest"))); +app.use(`/robots.txt`, express.static(path.join(scriptDir, "public/robots.txt"))); +app.use(`/icon.png`, express.static(path.join(scriptDir, "public/icon.png"))); app.use(sessionParser); app.use(favicon(`${scriptDir}/../images/app-icons/icon.ico`)); @@ -66,17 +68,17 @@ error_handlers.register(app); await import("./services/sync.js"); // triggers backup timer -await import('./services/backup.js'); +await import("./services/backup.js"); // trigger consistency checks timer -await import('./services/consistency_checks.js'); +await import("./services/consistency_checks.js"); -await import('./services/scheduler.js'); +await import("./services/scheduler.js"); startScheduledCleanup(); if (utils.isElectron()) { - (await import('@electron/remote/main/index.js')).initialize(); + (await import("@electron/remote/main/index.js")).initialize(); } export default app; diff --git a/src/becca/becca-interface.ts b/src/becca/becca-interface.ts index d4c0bf1b7..0a89899b0 100644 --- a/src/becca/becca-interface.ts +++ b/src/becca/becca-interface.ts @@ -8,7 +8,7 @@ import BAttribute from "./entities/battribute.js"; import BBranch from "./entities/bbranch.js"; import BRevision from "./entities/brevision.js"; import BAttachment from "./entities/battachment.js"; -import { AttachmentRow, BlobRow, RevisionRow } from './entities/rows.js'; +import { AttachmentRow, BlobRow, RevisionRow } from "./entities/rows.js"; import BBlob from "./entities/bblob.js"; import BRecentNote from "./entities/brecent_note.js"; import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; @@ -55,13 +55,13 @@ export default class Becca { } getRoot() { - return this.getNote('root'); + return this.getNote("root"); } findAttributes(type: string, name: string): BAttribute[] { name = name.trim().toLowerCase(); - if (name.startsWith('#') || name.startsWith('~')) { + if (name.startsWith("#") || name.startsWith("~")) { name = name.substr(1); } @@ -177,8 +177,7 @@ export default class Becca { WHERE attachmentId = ? AND isDeleted = 0` : `SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0`; - return sql.getRows(query, [attachmentId]) - .map(row => new BAttachment(row))[0]; + return sql.getRows(query, [attachmentId]).map((row) => new BAttachment(row))[0]; } getAttachmentOrThrow(attachmentId: string, opts: AttachmentOpts = {}): BAttachment { @@ -190,8 +189,7 @@ export default class Becca { } getAttachments(attachmentIds: string[]): BAttachment[] { - return sql.getManyRows("SELECT * FROM attachments WHERE attachmentId IN (???) AND isDeleted = 0", attachmentIds) - .map(row => new BAttachment(row)); + return sql.getManyRows("SELECT * FROM attachments WHERE attachmentId IN (???) AND isDeleted = 0", attachmentIds).map((row) => new BAttachment(row)); } getBlob(entity: { blobId?: string }): BBlob | null { @@ -220,18 +218,13 @@ export default class Becca { return null; } - if (entityName === 'revisions') { + if (entityName === "revisions") { return this.getRevision(entityId); - } else if (entityName === 'attachments') { + } else if (entityName === "attachments") { return this.getAttachment(entityId); } - const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g, - group => - group - .toUpperCase() - .replace('_', '') - ); + const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g, (group) => group.toUpperCase().replace("_", "")); if (!(camelCaseEntityName in this)) { throw new Error(`Unknown entity name '${camelCaseEntityName}' (original argument '${entityName}')`); @@ -242,12 +235,12 @@ export default class Becca { getRecentNotesFromQuery(query: string, params: string[] = []): BRecentNote[] { const rows = sql.getRows(query, params); - return rows.map(row => new BRecentNote(row)); + return rows.map((row) => new BRecentNote(row)); } getRevisionsFromQuery(query: string, params: string[] = []): BRevision[] { const rows = sql.getRows(query, params); - return rows.map(row => new BRevision(row)); + return rows.map((row) => new BRevision(row)); } /** Should be called when the set of all non-skeleton notes changes (added/removed) */ diff --git a/src/becca/becca_loader.ts b/src/becca/becca_loader.ts index 8bdfd475e..e48c4ed7b 100644 --- a/src/becca/becca_loader.ts +++ b/src/becca/becca_loader.ts @@ -11,7 +11,7 @@ import BOption from "./entities/boption.js"; import BEtapiToken from "./entities/betapi_token.js"; import cls from "../services/cls.js"; import entityConstructor from "../becca/entity_constructor.js"; -import { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from './entities/rows.js'; +import { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from "./entities/rows.js"; import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import ws from "../services/ws.js"; @@ -119,13 +119,13 @@ eventService.subscribeBeccaLoader(eventService.ENTITY_CHANGED, ({ entityName, en * It should be therefore treated as a row. */ function postProcessEntityUpdate(entityName: string, entityRow: any) { - if (entityName === 'notes') { + if (entityName === "notes") { noteUpdated(entityRow); - } else if (entityName === 'branches') { + } else if (entityName === "branches") { branchUpdated(entityRow); - } else if (entityName === 'attributes') { + } else if (entityName === "attributes") { attributeUpdated(entityRow); - } else if (entityName === 'note_reordering') { + } else if (entityName === "note_reordering") { noteReorderingUpdated(entityRow); } } @@ -135,13 +135,13 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENT return; } - if (entityName === 'notes') { + if (entityName === "notes") { noteDeleted(entityId); - } else if (entityName === 'branches') { + } else if (entityName === "branches") { branchDeleted(entityId); - } else if (entityName === 'attributes') { + } else if (entityName === "attributes") { attributeDeleted(entityId); - } else if (entityName === 'etapi_tokens') { + } else if (entityName === "etapi_tokens") { etapiTokenDeleted(entityId); } }); @@ -162,9 +162,8 @@ function branchDeleted(branchId: string) { const childNote = becca.notes[branch.noteId]; if (childNote) { - childNote.parents = childNote.parents.filter(parent => parent.noteId !== branch.parentNoteId); - childNote.parentBranches = childNote.parentBranches - .filter(parentBranch => parentBranch.branchId !== branch.branchId); + childNote.parents = childNote.parents.filter((parent) => parent.noteId !== branch.parentNoteId); + childNote.parentBranches = childNote.parentBranches.filter((parentBranch) => parentBranch.branchId !== branch.branchId); if (childNote.parents.length > 0) { // subtree notes might lose some inherited attributes @@ -175,7 +174,7 @@ function branchDeleted(branchId: string) { const parentNote = becca.notes[branch.parentNoteId]; if (parentNote) { - parentNote.children = parentNote.children.filter(child => child.noteId !== branch.noteId); + parentNote.children = parentNote.children.filter((child) => child.noteId !== branch.noteId); } delete becca.childParentToBranch[`${branch.noteId}-${branch.parentNoteId}`]; @@ -230,12 +229,12 @@ function attributeDeleted(attributeId: string) { note.invalidateThisCache(); } - note.ownedAttributes = note.ownedAttributes.filter(attr => attr.attributeId !== attribute.attributeId); + note.ownedAttributes = note.ownedAttributes.filter((attr) => attr.attributeId !== attribute.attributeId); const targetNote = attribute.targetNote; if (targetNote) { - targetNote.targetRelations = targetNote.targetRelations.filter(rel => rel.attributeId !== attribute.attributeId); + targetNote.targetRelations = targetNote.targetRelations.filter((rel) => rel.attributeId !== attribute.attributeId); } } @@ -244,7 +243,7 @@ function attributeDeleted(attributeId: string) { const key = `${attribute.type}-${attribute.name.toLowerCase()}`; if (key in becca.attributeIndex) { - becca.attributeIndex[key] = becca.attributeIndex[key].filter(attr => attr.attributeId !== attribute.attributeId); + becca.attributeIndex[key] = becca.attributeIndex[key].filter((attr) => attr.attributeId !== attribute.attributeId); } } @@ -282,8 +281,7 @@ function etapiTokenDeleted(etapiTokenId: string) { eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => { try { becca.decryptProtectedNotes(); - } - catch (e: any) { + } catch (e: any) { log.error(`Could not decrypt protected notes: ${e.message} ${e.stack}`); } }); diff --git a/src/becca/becca_service.ts b/src/becca/becca_service.ts index 46d41fdbf..ab6838a78 100644 --- a/src/becca/becca_service.ts +++ b/src/becca/becca_service.ts @@ -37,7 +37,7 @@ function getNoteTitle(childNoteId: string, parentNoteId?: string) { const branch = parentNote ? becca.getBranchFromChildAndParent(childNote.noteId, parentNote.noteId) : null; - return `${(branch && branch.prefix) ? `${branch.prefix} - ` : ''}${title}`; + return `${branch && branch.prefix ? `${branch.prefix} - ` : ""}${title}`; } function getNoteTitleArrayForPath(notePathArray: string[]) { @@ -51,7 +51,7 @@ function getNoteTitleArrayForPath(notePathArray: string[]) { const titles = []; - let parentNoteId = 'root'; + let parentNoteId = "root"; let hoistedNotePassed = false; // this is a notePath from outside of hoisted subtree, so the full title path needs to be returned @@ -79,7 +79,7 @@ function getNoteTitleArrayForPath(notePathArray: string[]) { function getNoteTitleForPath(notePathArray: string[]) { const titles = getNoteTitleArrayForPath(notePathArray); - return titles.join(' / '); + return titles.join(" / "); } export default { diff --git a/src/becca/entities/abstract_becca_entity.ts b/src/becca/entities/abstract_becca_entity.ts index f86eced67..f0e7b6edc 100644 --- a/src/becca/entities/abstract_becca_entity.ts +++ b/src/becca/entities/abstract_becca_entity.ts @@ -9,7 +9,7 @@ import cls from "../../services/cls.js"; import log from "../../services/log.js"; import protectedSessionService from "../../services/protected_session.js"; import blobService from "../../services/blob.js"; -import Becca, { ConstructorData } from '../becca-interface.js'; +import Becca, { ConstructorData } from "../becca-interface.js"; import becca from "../becca.js"; interface ContentOpts { @@ -23,7 +23,6 @@ interface ContentOpts { * @type T the same entity type needed for self-reference in {@link ConstructorData}. */ abstract class AbstractBeccaEntity> { - utcDateModified?: string; dateCreated?: string; dateModified?: string; @@ -35,7 +34,7 @@ abstract class AbstractBeccaEntity> { blobId?: string; protected beforeSaving(opts?: {}) { - const constructorData = (this.constructor as unknown as ConstructorData); + const constructorData = this.constructor as unknown as ConstructorData; if (!(this as any)[constructorData.primaryKeyName]) { (this as any)[constructorData.primaryKeyName] = utils.newEntityId(); } @@ -50,19 +49,19 @@ abstract class AbstractBeccaEntity> { } protected putEntityChange(isDeleted: boolean) { - const constructorData = (this.constructor as unknown as ConstructorData); + const constructorData = this.constructor as unknown as ConstructorData; entityChangesService.putEntityChange({ entityName: constructorData.entityName, entityId: (this as any)[constructorData.primaryKeyName], hash: this.generateHash(isDeleted), isErased: false, utcDateChanged: this.getUtcDateChanged(), - isSynced: constructorData.entityName !== 'options' || !!this.isSynced + isSynced: constructorData.entityName !== "options" || !!this.isSynced }); } generateHash(isDeleted?: boolean): string { - const constructorData = (this.constructor as unknown as ConstructorData); + const constructorData = this.constructor as unknown as ConstructorData; let contentToHash = ""; for (const propertyName of constructorData.hashedProperties) { @@ -99,10 +98,10 @@ abstract class AbstractBeccaEntity> { } /** - * Saves entity - executes SQL, but doesn't commit the transaction on its own - */ + * Saves entity - executes SQL, but doesn't commit the transaction on its own + */ save(opts?: {}): this { - const constructorData = (this.constructor as unknown as ConstructorData); + const constructorData = this.constructor as unknown as ConstructorData; const entityName = constructorData.entityName; const primaryKeyName = constructorData.primaryKeyName; @@ -115,7 +114,7 @@ abstract class AbstractBeccaEntity> { sql.transactional(() => { sql.upsert(entityName, primaryKeyName, pojo); - if (entityName === 'recent_notes') { + if (entityName === "recent_notes") { return; } @@ -144,7 +143,7 @@ abstract class AbstractBeccaEntity> { opts.forceFrontendReload = !!opts.forceFrontendReload; if (content === null || content === undefined) { - const constructorData = (this.constructor as unknown as ConstructorData); + const constructorData = this.constructor as unknown as ConstructorData; throw new Error(`Cannot set null content to ${constructorData.primaryKeyName} '${(this as any)[constructorData.primaryKeyName]}'`); } @@ -206,9 +205,7 @@ abstract class AbstractBeccaEntity> { if (this.isProtected) { // a "random" prefix makes sure that the calculated hash/blobId is different for a decrypted/encrypted content const encryptedPrefixSuffix = "t$[nvQg7q)&_ENCRYPTED_?M:Bf&j3jr_"; - return Buffer.isBuffer(unencryptedContent) - ? Buffer.concat([Buffer.from(encryptedPrefixSuffix), unencryptedContent]) - : `${encryptedPrefixSuffix}${unencryptedContent}`; + return Buffer.isBuffer(unencryptedContent) ? Buffer.concat([Buffer.from(encryptedPrefixSuffix), unencryptedContent]) : `${encryptedPrefixSuffix}${unencryptedContent}`; } else { return unencryptedContent; } @@ -216,13 +213,13 @@ abstract class AbstractBeccaEntity> { private saveBlob(content: string | Buffer, unencryptedContentForHashCalculation: string | Buffer, opts: ContentOpts = {}) { /* - * We're using the unencrypted blob for the hash calculation, because otherwise the random IV would - * cause every content blob to be unique which would balloon the database size (esp. with revisioning). - * This has minor security implications (it's easy to infer that given content is shared between different - * notes/attachments), but the trade-off comes out clearly positive. - */ + * We're using the unencrypted blob for the hash calculation, because otherwise the random IV would + * cause every content blob to be unique which would balloon the database size (esp. with revisioning). + * This has minor security implications (it's easy to infer that given content is shared between different + * notes/attachments), but the trade-off comes out clearly positive. + */ const newBlobId = utils.hashedBlobId(unencryptedContentForHashCalculation); - const blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]); + const blobNeedsInsert = !sql.getValue("SELECT 1 FROM blobs WHERE blobId = ?", [newBlobId]); if (!blobNeedsInsert) { return newBlobId; @@ -242,7 +239,7 @@ abstract class AbstractBeccaEntity> { const hash = blobService.calculateContentHash(pojo); entityChangesService.putEntityChange({ - entityName: 'blobs', + entityName: "blobs", entityId: newBlobId, hash: hash, isErased: false, @@ -254,7 +251,7 @@ abstract class AbstractBeccaEntity> { }); eventService.emit(eventService.ENTITY_CHANGED, { - entityName: 'blobs', + entityName: "blobs", entity: this }); @@ -265,7 +262,7 @@ abstract class AbstractBeccaEntity> { const row = sql.getRow<{ content: string | Buffer }>(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]); if (!row) { - const constructorData = (this.constructor as unknown as ConstructorData); + const constructorData = this.constructor as unknown as ConstructorData; throw new Error(`Cannot find content for ${constructorData.primaryKeyName} '${(this as any)[constructorData.primaryKeyName]}', blobId '${this.blobId}'`); } @@ -273,26 +270,27 @@ abstract class AbstractBeccaEntity> { } /** - * Mark the entity as (soft) deleted. It will be completely erased later. - * - * This is a low-level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead. - */ + * Mark the entity as (soft) deleted. It will be completely erased later. + * + * This is a low-level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead. + */ markAsDeleted(deleteId: string | null = null) { - const constructorData = (this.constructor as unknown as ConstructorData); + const constructorData = this.constructor as unknown as ConstructorData; const entityId = (this as any)[constructorData.primaryKeyName]; const entityName = constructorData.entityName; this.utcDateModified = dateUtils.utcNowDateTime(); - sql.execute(`UPDATE ${entityName} SET isDeleted = 1, deleteId = ?, utcDateModified = ? + sql.execute( + `UPDATE ${entityName} SET isDeleted = 1, deleteId = ?, utcDateModified = ? WHERE ${constructorData.primaryKeyName} = ?`, - [deleteId, this.utcDateModified, entityId]); + [deleteId, this.utcDateModified, entityId] + ); if (this.dateModified) { this.dateModified = dateUtils.localNowDateTime(); - sql.execute(`UPDATE ${entityName} SET dateModified = ? WHERE ${constructorData.primaryKeyName} = ?`, - [this.dateModified, entityId]); + sql.execute(`UPDATE ${entityName} SET dateModified = ? WHERE ${constructorData.primaryKeyName} = ?`, [this.dateModified, entityId]); } log.info(`Marking ${entityName} ${entityId} as deleted`); @@ -303,15 +301,17 @@ abstract class AbstractBeccaEntity> { } markAsDeletedSimple() { - const constructorData = (this.constructor as unknown as ConstructorData); + const constructorData = this.constructor as unknown as ConstructorData; const entityId = (this as any)[constructorData.primaryKeyName]; const entityName = constructorData.entityName; this.utcDateModified = dateUtils.utcNowDateTime(); - sql.execute(`UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ? + sql.execute( + `UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ? WHERE ${constructorData.primaryKeyName} = ?`, - [this.utcDateModified, entityId]); + [this.utcDateModified, entityId] + ); log.info(`Marking ${entityName} ${entityId} as deleted`); diff --git a/src/becca/entities/battachment.ts b/src/becca/entities/battachment.ts index b26599598..b88b1df2c 100644 --- a/src/becca/entities/battachment.ts +++ b/src/becca/entities/battachment.ts @@ -6,14 +6,14 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js"; import sql from "../../services/sql.js"; import protectedSessionService from "../../services/protected_session.js"; import log from "../../services/log.js"; -import { AttachmentRow } from './rows.js'; +import { AttachmentRow } from "./rows.js"; import BNote from "./bnote.js"; import BBranch from "./bbranch.js"; import noteService from "../../services/notes.js"; const attachmentRoleToNoteTypeMapping = { - 'image': 'image', - 'file': 'file' + image: "image", + file: "file" }; interface ContentOpts { @@ -31,9 +31,15 @@ interface ContentOpts { * larger amounts of data and generally not accessible to the user. */ class BAttachment extends AbstractBeccaEntity { - static get entityName() { return "attachments"; } - static get primaryKeyName() { return "attachmentId"; } - static get hashedProperties() { return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"]; } + static get entityName() { + return "attachments"; + } + static get primaryKeyName() { + return "attachmentId"; + } + static get hashedProperties() { + return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"]; + } noteId?: number; attachmentId?: string; @@ -102,13 +108,15 @@ class BAttachment extends AbstractBeccaEntity { } isContentAvailable() { - return !this.attachmentId // new attachment which was not encrypted yet - || !this.isProtected - || protectedSessionService.isProtectedSessionAvailable() + return ( + !this.attachmentId || // new attachment which was not encrypted yet + !this.isProtected || + protectedSessionService.isProtectedSessionAvailable() + ); } getTitleOrProtected() { - return this.isContentAvailable() ? this.title : '[protected]'; + return this.isContentAvailable() ? this.title : "[protected]"; } decrypt() { @@ -121,8 +129,7 @@ class BAttachment extends AbstractBeccaEntity { try { this.title = protectedSessionService.decryptString(this.title) || ""; this.isDecrypted = true; - } - catch (e: any) { + } catch (e: any) { log.error(`Could not decrypt attachment ${this.attachmentId}: ${e.message} ${e.stack}`); } } @@ -136,22 +143,22 @@ class BAttachment extends AbstractBeccaEntity { this._setContent(content, opts); } - convertToNote(): { note: BNote, branch: BBranch } { + convertToNote(): { note: BNote; branch: BBranch } { // TODO: can this ever be "search"? - if (this.type as string === 'search') { + if ((this.type as string) === "search") { throw new Error(`Note of type search cannot have child notes`); } if (!this.getNote()) { - throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " + - "Converting note revision's attachments to note is not (yet) supported."); + throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " + "Converting note revision's attachments to note is not (yet) supported."); } if (!(this.role in attachmentRoleToNoteTypeMapping)) { throw new Error(`Mapping from attachment role '${this.role}' to note's type is not defined`); } - if (!this.isContentAvailable()) { // isProtected is the same for attachment + if (!this.isContentAvailable()) { + // isProtected is the same for attachment throw new Error(`Cannot convert protected attachment outside of protected session`); } @@ -168,7 +175,7 @@ class BAttachment extends AbstractBeccaEntity { const parentNote = this.getNote(); - if (this.role === 'image' && parentNote.type === 'text') { + if (this.role === "image" && parentNote.type === "text") { const origContent = parentNote.getContent(); if (typeof origContent !== "string") { @@ -191,7 +198,7 @@ class BAttachment extends AbstractBeccaEntity { } getFileName() { - const type = this.role === 'image' ? 'image' : 'file'; + const type = this.role === "image" ? "image" : "file"; return utils.formatDownloadTitle(this.title, type, this.mime); } @@ -200,9 +207,14 @@ class BAttachment extends AbstractBeccaEntity { super.beforeSaving(); if (this.position === undefined || this.position === null) { - this.position = 10 + sql.getValue(`SELECT COALESCE(MAX(position), 0) + this.position = + 10 + + sql.getValue( + `SELECT COALESCE(MAX(position), 0) FROM attachments - WHERE ownerId = ?`, [this.noteId]); + WHERE ownerId = ?`, + [this.noteId] + ); } this.dateModified = dateUtils.localNowDateTime(); @@ -234,8 +246,7 @@ class BAttachment extends AbstractBeccaEntity { if (pojo.isProtected) { if (this.isDecrypted) { pojo.title = protectedSessionService.encrypt(pojo.title || "") || undefined; - } - else { + } else { // updating protected note outside of protected session means we will keep original ciphertexts delete pojo.title; } diff --git a/src/becca/entities/battribute.ts b/src/becca/entities/battribute.ts index 66ef83ca8..746f493a1 100644 --- a/src/becca/entities/battribute.ts +++ b/src/becca/entities/battribute.ts @@ -5,7 +5,7 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js"; import dateUtils from "../../services/date_utils.js"; import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js"; import sanitizeAttributeName from "../../services/sanitize_attribute_name.js"; -import { AttributeRow, AttributeType } from './rows.js'; +import { AttributeRow, AttributeType } from "./rows.js"; interface SavingOpts { skipValidation?: boolean; @@ -16,9 +16,15 @@ interface SavingOpts { * and relation (representing named relationship between source and target note) */ class BAttribute extends AbstractBeccaEntity { - static get entityName() { return "attributes"; } - static get primaryKeyName() { return "attributeId"; } - static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable"]; } + static get entityName() { + return "attributes"; + } + static get primaryKeyName() { + return "attributeId"; + } + static get hashedProperties() { + return ["attributeId", "noteId", "type", "name", "value", "isInheritable"]; + } attributeId!: string; noteId!: string; @@ -40,16 +46,7 @@ class BAttribute extends AbstractBeccaEntity { } updateFromRow(row: AttributeRow) { - this.update([ - row.attributeId, - row.noteId, - row.type, - row.name, - row.value, - row.isInheritable, - row.position, - row.utcDateModified - ]); + this.update([row.attributeId, row.noteId, row.type, row.name, row.value, row.isInheritable, row.position, row.utcDateModified]); } update([attributeId, noteId, type, name, value, isInheritable, position, utcDateModified]: any) { @@ -72,7 +69,7 @@ class BAttribute extends AbstractBeccaEntity { if (!(this.noteId in this.becca.notes)) { // entities can come out of order in sync, create skeleton which will be filled later - this.becca.addNote(this.noteId, new BNote({noteId: this.noteId})); + this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId })); } this.becca.notes[this.noteId].ownedAttributes.push(this); @@ -97,22 +94,22 @@ class BAttribute extends AbstractBeccaEntity { throw new Error(`Invalid empty name in attribute '${this.attributeId}' of note '${this.noteId}'`); } - if (this.type === 'relation' && !(this.value in this.becca.notes)) { + if (this.type === "relation" && !(this.value in this.becca.notes)) { throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it targets not existing note '${this.value}'.`); } } get isAffectingSubtree() { - return this.isInheritable - || (this.type === 'relation' && ['template', 'inherit'].includes(this.name)); + return this.isInheritable || (this.type === "relation" && ["template", "inherit"].includes(this.name)); } - get targetNoteId() { // alias - return this.type === 'relation' ? this.value : undefined; + get targetNoteId() { + // alias + return this.type === "relation" ? this.value : undefined; } isAutoLink() { - return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name); + return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name); } get note() { @@ -120,7 +117,7 @@ class BAttribute extends AbstractBeccaEntity { } get targetNote() { - if (this.type === 'relation') { + if (this.type === "relation") { return this.becca.notes[this.value]; } } @@ -136,7 +133,7 @@ class BAttribute extends AbstractBeccaEntity { } getTargetNote() { - if (this.type !== 'relation') { + if (this.type !== "relation") { throw new Error(`Attribute '${this.attributeId}' is not a relation.`); } @@ -148,7 +145,7 @@ class BAttribute extends AbstractBeccaEntity { } isDefinition() { - return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:')); + return this.type === "label" && (this.name.startsWith("label:") || this.name.startsWith("relation:")); } getDefinition() { @@ -156,9 +153,9 @@ class BAttribute extends AbstractBeccaEntity { } getDefinedName() { - if (this.type === 'label' && this.name.startsWith('label:')) { + if (this.type === "label" && this.name.startsWith("label:")) { return this.name.substr(6); - } else if (this.type === 'label' && this.name.startsWith('relation:')) { + } else if (this.type === "label" && this.name.startsWith("relation:")) { return this.name.substr(9); } else { return this.name; @@ -182,7 +179,8 @@ class BAttribute extends AbstractBeccaEntity { } if (this.position === undefined || this.position === null) { - const maxExistingPosition = this.getNote().getAttributes() + const maxExistingPosition = this.getNote() + .getAttributes() .reduce((maxPosition, attr) => Math.max(maxPosition, attr.position || 0), 0); this.position = maxExistingPosition + 10; diff --git a/src/becca/entities/bblob.ts b/src/becca/entities/bblob.ts index 7ce94eb2e..f72453578 100644 --- a/src/becca/entities/bblob.ts +++ b/src/becca/entities/bblob.ts @@ -3,9 +3,15 @@ import { BlobRow } from "./rows.js"; // TODO: Why this does not extend the abstract becca? class BBlob extends AbstractBeccaEntity { - static get entityName() { return "blobs"; } - static get primaryKeyName() { return "blobId"; } - static get hashedProperties() { return ["blobId", "content"]; } + static get entityName() { + return "blobs"; + } + static get primaryKeyName() { + return "blobId"; + } + static get hashedProperties() { + return ["blobId", "content"]; + } content!: string | Buffer; contentLength!: number; diff --git a/src/becca/entities/bbranch.ts b/src/becca/entities/bbranch.ts index 674136c73..443cbfd15 100644 --- a/src/becca/entities/bbranch.ts +++ b/src/becca/entities/bbranch.ts @@ -7,7 +7,7 @@ import utils from "../../services/utils.js"; import TaskContext from "../../services/task_context.js"; import cls from "../../services/cls.js"; import log from "../../services/log.js"; -import { BranchRow } from './rows.js'; +import { BranchRow } from "./rows.js"; import handlers from "../../services/handlers.js"; /** @@ -18,10 +18,16 @@ import handlers from "../../services/handlers.js"; * Always check noteId instead. */ class BBranch extends AbstractBeccaEntity { - static get entityName() { return "branches"; } - static get primaryKeyName() { return "branchId"; } + static get entityName() { + return "branches"; + } + static get primaryKeyName() { + return "branchId"; + } // notePosition is not part of hash because it would produce a lot of updates in case of reordering - static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "prefix"]; } + static get hashedProperties() { + return ["branchId", "noteId", "parentNoteId", "prefix"]; + } branchId?: string; noteId!: string; @@ -42,15 +48,7 @@ class BBranch extends AbstractBeccaEntity { } updateFromRow(row: BranchRow) { - this.update([ - row.branchId, - row.noteId, - row.parentNoteId, - row.prefix, - row.notePosition, - row.isExpanded, - row.utcDateModified - ]); + this.update([row.branchId, row.noteId, row.parentNoteId, row.prefix, row.notePosition, row.isExpanded, row.utcDateModified]); } update([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified]: any) { @@ -78,7 +76,7 @@ class BBranch extends AbstractBeccaEntity { childNote.parentBranches.push(this); } - if (this.noteId === 'root') { + if (this.noteId === "root") { return; } @@ -97,7 +95,7 @@ class BBranch extends AbstractBeccaEntity { get childNote(): BNote { if (!(this.noteId in this.becca.notes)) { // entities can come out of order in sync/import, create skeleton which will be filled later - this.becca.addNote(this.noteId, new BNote({noteId: this.noteId})); + this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId })); } return this.becca.notes[this.noteId]; @@ -109,43 +107,43 @@ class BBranch extends AbstractBeccaEntity { /** @returns root branch will have undefined parent, all other branches have to have a parent note */ get parentNote(): BNote | undefined { - if (!(this.parentNoteId in this.becca.notes) && this.parentNoteId !== 'none') { + if (!(this.parentNoteId in this.becca.notes) && this.parentNoteId !== "none") { // entities can come out of order in sync/import, create skeleton which will be filled later - this.becca.addNote(this.parentNoteId, new BNote({noteId: this.parentNoteId})); + this.becca.addNote(this.parentNoteId, new BNote({ noteId: this.parentNoteId })); } return this.becca.notes[this.parentNoteId]; } get isDeleted() { - return (this.branchId == undefined || !(this.branchId in this.becca.branches)); + return this.branchId == undefined || !(this.branchId in this.becca.branches); } /** - * Branch is weak when its existence should not hinder deletion of its note. - * As a result, note with only weak branches should be immediately deleted. - * An example is shared or bookmarked clones - they are created automatically and exist for technical reasons, - * not as user-intended actions. From user perspective, they don't count as real clones and for the purpose - * of deletion should not act as a clone. - */ + * Branch is weak when its existence should not hinder deletion of its note. + * As a result, note with only weak branches should be immediately deleted. + * An example is shared or bookmarked clones - they are created automatically and exist for technical reasons, + * not as user-intended actions. From user perspective, they don't count as real clones and for the purpose + * of deletion should not act as a clone. + */ get isWeak() { - return ['_share', '_lbBookmarks'].includes(this.parentNoteId); + return ["_share", "_lbBookmarks"].includes(this.parentNoteId); } /** - * Delete a branch. If this is a last note's branch, delete the note as well. - * - * @param deleteId - optional delete identified - * - * @returns true if note has been deleted, false otherwise - */ + * Delete a branch. If this is a last note's branch, delete the note as well. + * + * @param deleteId - optional delete identified + * + * @returns true if note has been deleted, false otherwise + */ deleteBranch(deleteId?: string, taskContext?: TaskContext): boolean { if (!deleteId) { deleteId = utils.randomString(10); } if (!taskContext) { - taskContext = new TaskContext('no-progress-reporting'); + taskContext = new TaskContext("no-progress-reporting"); } taskContext.increaseProgressCount(); @@ -157,13 +155,11 @@ class BBranch extends AbstractBeccaEntity { if (parentBranches.length === 1 && parentBranches[0] === this) { // needs to be run before branches and attributes are deleted and thus attached relations disappear - handlers.runAttachedRelations(note, 'runOnNoteDeletion', note); + handlers.runAttachedRelations(note, "runOnNoteDeletion", note); } } - if (this.noteId === 'root' - || this.noteId === cls.getHoistedNoteId()) { - + if (this.noteId === "root" || this.noteId === cls.getHoistedNoteId()) { throw new Error("Can't delete root or hoisted branch/note"); } @@ -203,8 +199,7 @@ class BBranch extends AbstractBeccaEntity { note.markAsDeleted(deleteId); return true; - } - else { + } else { return false; } } @@ -225,8 +220,9 @@ class BBranch extends AbstractBeccaEntity { continue; } - if (maxNotePos < childBranch.notePosition - && childBranch.noteId !== '_hidden' // hidden has a very large notePosition to always stay last + if ( + maxNotePos < childBranch.notePosition && + childBranch.noteId !== "_hidden" // hidden has a very large notePosition to always stay last ) { maxNotePos = childBranch.notePosition; } diff --git a/src/becca/entities/betapi_token.ts b/src/becca/entities/betapi_token.ts index bfd4383e3..d315029a2 100644 --- a/src/becca/entities/betapi_token.ts +++ b/src/becca/entities/betapi_token.ts @@ -15,9 +15,15 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js"; * from tokenHash and token. */ class BEtapiToken extends AbstractBeccaEntity { - static get entityName() { return "etapi_tokens"; } - static get primaryKeyName() { return "etapiTokenId"; } - static get hashedProperties() { return ["etapiTokenId", "name", "tokenHash", "utcDateCreated", "utcDateModified", "isDeleted"]; } + static get entityName() { + return "etapi_tokens"; + } + static get primaryKeyName() { + return "etapiTokenId"; + } + static get hashedProperties() { + return ["etapiTokenId", "name", "tokenHash", "utcDateCreated", "utcDateModified", "isDeleted"]; + } etapiTokenId?: string; name!: string; @@ -66,7 +72,7 @@ class BEtapiToken extends AbstractBeccaEntity { utcDateCreated: this.utcDateCreated, utcDateModified: this.utcDateModified, isDeleted: this.isDeleted - } + }; } beforeSaving() { diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index 44d0686a6..b8aa69614 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -14,18 +14,18 @@ import TaskContext from "../../services/task_context.js"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc.js"; import eventService from "../../services/events.js"; -import { AttachmentRow, AttributeType, NoteRow, NoteType, RevisionRow } from './rows.js'; +import { AttachmentRow, AttributeType, NoteRow, NoteType, RevisionRow } from "./rows.js"; import BBranch from "./bbranch.js"; import BAttribute from "./battribute.js"; -import { NotePojo } from '../becca-interface.js'; +import { NotePojo } from "../becca-interface.js"; import searchService from "../../services/search/services/search.js"; import cloningService, { CloneResponse } from "../../services/cloning.js"; import noteService from "../../services/notes.js"; import handlers from "../../services/handlers.js"; dayjs.extend(utc); -const LABEL = 'label'; -const RELATION = 'relation'; +const LABEL = "label"; +const RELATION = "relation"; interface NotePathRecord { isArchived: boolean; @@ -47,7 +47,7 @@ interface AttachmentOpts { interface Relationship { parentNoteId: string; - childNoteId: string + childNoteId: string; } interface ConvertOpts { @@ -59,9 +59,15 @@ interface ConvertOpts { * Trilium's main entity, which can represent text note, image, code note, file attachment etc. */ class BNote extends AbstractBeccaEntity { - static get entityName() { return "notes"; } - static get primaryKeyName() { return "noteId"; } - static get hashedProperties() { return ["noteId", "title", "isProtected", "type", "mime", "blobId"]; } + static get entityName() { + return "notes"; + } + static get primaryKeyName() { + return "noteId"; + } + static get hashedProperties() { + return ["noteId", "title", "isProtected", "type", "mime", "blobId"]; + } noteId!: string; title!: string; @@ -105,18 +111,7 @@ class BNote extends AbstractBeccaEntity { } updateFromRow(row: Partial) { - this.update([ - row.noteId, - row.title, - row.type, - row.mime, - row.isProtected, - row.blobId, - row.dateCreated, - row.dateModified, - row.utcDateCreated, - row.utcDateModified - ]); + this.update([row.noteId, row.title, row.type, row.mime, row.isProtected, row.blobId, row.dateCreated, row.dateModified, row.utcDateCreated, row.utcDateModified]); } update([noteId, title, type, mime, isProtected, blobId, dateCreated, dateModified, utcDateCreated, utcDateModified]: any) { @@ -164,13 +159,15 @@ class BNote extends AbstractBeccaEntity { } isContentAvailable() { - return !this.noteId // new note which was not encrypted yet - || !this.isProtected - || protectedSessionService.isProtectedSessionAvailable() + return ( + !this.noteId || // new note which was not encrypted yet + !this.isProtected || + protectedSessionService.isProtectedSessionAvailable() + ); } getTitleOrProtected() { - return this.isContentAvailable() ? this.title : '[protected]'; + return this.isContentAvailable() ? this.title : "[protected]"; } getParentBranches() { @@ -178,15 +175,15 @@ class BNote extends AbstractBeccaEntity { } /** - * Returns strong (as opposed to weak) parent branches. See isWeak for details. - */ + * Returns strong (as opposed to weak) parent branches. See isWeak for details. + */ getStrongParentBranches() { - return this.getParentBranches().filter(branch => !branch.isWeak); + return this.getParentBranches().filter((branch) => !branch.isWeak); } /** - * @deprecated use getParentBranches() instead - */ + * @deprecated use getParentBranches() instead + */ getBranches() { return this.parentBranches; } @@ -204,25 +201,24 @@ class BNote extends AbstractBeccaEntity { } getChildBranches(): BBranch[] { - return this.children - .map(childNote => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId)) as BBranch[]; + return this.children.map((childNote) => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId)) as BBranch[]; } /** - * Note content has quite special handling - it's not a separate entity, but a lazily loaded - * part of Note entity with its own sync. Reasons behind this hybrid design has been: - * - * - content can be quite large, and it's not necessary to load it / fill memory for any note access even if we don't need a content, especially for bulk operations like search - * - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records) - * - but to the user note content and title changes are one and the same - single dateModified (so all changes must go through Note and content is not a separate entity) - */ + * Note content has quite special handling - it's not a separate entity, but a lazily loaded + * part of Note entity with its own sync. Reasons behind this hybrid design has been: + * + * - content can be quite large, and it's not necessary to load it / fill memory for any note access even if we don't need a content, especially for bulk operations like search + * - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records) + * - but to the user note content and title changes are one and the same - single dateModified (so all changes must go through Note and content is not a separate entity) + */ getContent() { return this._getContent(); } /** - * @throws Error in case of invalid JSON - */ + * @throws Error in case of invalid JSON + */ getJsonContent(): any | null { const content = this.getContent(); @@ -237,8 +233,7 @@ class BNote extends AbstractBeccaEntity { getJsonContentSafely() { try { return this.getJsonContent(); - } - catch (e) { + } catch (e) { return null; } } @@ -250,7 +245,7 @@ class BNote extends AbstractBeccaEntity { } setJsonContent(content: {}) { - this.setContent(JSON.stringify(content, null, '\t')); + this.setContent(JSON.stringify(content, null, "\t")); } get dateCreatedObj() { @@ -271,7 +266,7 @@ class BNote extends AbstractBeccaEntity { /** @returns true if this note is the root of the note tree. Root note has "root" noteId */ isRoot() { - return this.noteId === 'root'; + return this.noteId === "root"; } /** @returns true if this note is of application/json content type */ @@ -281,22 +276,20 @@ class BNote extends AbstractBeccaEntity { /** @returns true if this note is JavaScript (code or attachment) */ isJavaScript() { - return (this.type === "code" || this.type === "file" || this.type === 'launcher') - && (this.mime.startsWith("application/javascript") - || this.mime === "application/x-javascript" - || this.mime === "text/javascript"); + return ( + (this.type === "code" || this.type === "file" || this.type === "launcher") && + (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript" || this.mime === "text/javascript") + ); } /** @returns true if this note is HTML */ isHtml() { - return ["code", "file", "render"].includes(this.type) - && this.mime === "text/html"; + return ["code", "file", "render"].includes(this.type) && this.mime === "text/html"; } /** @returns true if this note is an image */ isImage() { - return this.type === 'image' - || (this.type === 'file' && this.mime?.startsWith('image/')); + return this.type === "image" || (this.type === "file" && this.mime?.startsWith("image/")); } /** @deprecated use hasStringContent() instead */ @@ -311,15 +304,15 @@ class BNote extends AbstractBeccaEntity { /** @returns JS script environment - either "frontend" or "backend" */ getScriptEnv() { - if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) { + if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith("env=frontend"))) { return "frontend"; } - if (this.type === 'render') { + if (this.type === "render") { return "frontend"; } - if (this.isJavaScript() && this.mime.endsWith('env=backend')) { + if (this.isJavaScript() && this.mime.endsWith("env=backend")) { return "backend"; } @@ -327,13 +320,13 @@ class BNote extends AbstractBeccaEntity { } /** - * Beware that the method must not create a copy of the array, but actually returns its internal array - * (for performance reasons) - * - * @param type - (optional) attribute type to filter - * @param name - (optional) attribute name to filter - * @returns all note's attributes, including inherited ones - */ + * Beware that the method must not create a copy of the array, but actually returns its internal array + * (for performance reasons) + * + * @param type - (optional) attribute type to filter + * @param name - (optional) attribute name to filter + * @returns all note's attributes, including inherited ones + */ getAttributes(type?: string, name?: string): BAttribute[] { this.__validateTypeName(type, name); this.__ensureAttributeCacheIsAvailable(); @@ -343,15 +336,12 @@ class BNote extends AbstractBeccaEntity { } if (type && name) { - return this.__attributeCache.filter(attr => attr.name === name && attr.type === type); - } - else if (type) { - return this.__attributeCache.filter(attr => attr.type === type); - } - else if (name) { - return this.__attributeCache.filter(attr => attr.name === name); - } - else { + return this.__attributeCache.filter((attr) => attr.name === name && attr.type === type); + } else if (type) { + return this.__attributeCache.filter((attr) => attr.type === type); + } else if (name) { + return this.__attributeCache.filter((attr) => attr.name === name); + } else { return this.__attributeCache; } } @@ -372,7 +362,7 @@ class BNote extends AbstractBeccaEntity { const newPath = [...path, this.noteId]; // inheritable attrs on root are typically not intended to be applied to hidden subtree #3537 - if (this.noteId !== 'root' && this.noteId !== '_hidden') { + if (this.noteId !== "root" && this.noteId !== "_hidden") { for (const parentNote of this.parents) { parentAttributes.push(...parentNote.__getInheritableAttributes(newPath)); } @@ -380,15 +370,17 @@ class BNote extends AbstractBeccaEntity { const templateAttributes = []; - for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates - if (ownedAttr.type === 'relation' && ['template', 'inherit'].includes(ownedAttr.name)) { + for (const ownedAttr of parentAttributes) { + // parentAttributes so we process also inherited templates + if (ownedAttr.type === "relation" && ["template", "inherit"].includes(ownedAttr.name)) { const templateNote = this.becca.notes[ownedAttr.value]; if (templateNote) { templateAttributes.push( - ...templateNote.__getAttributes(newPath) + ...templateNote + .__getAttributes(newPath) // template attr is used as a marker for templates, but it's not meant to be inherited - .filter(attr => !(attr.type === 'label' && (attr.name === 'template' || attr.name === 'workspacetemplate'))) + .filter((attr) => !(attr.type === "label" && (attr.name === "template" || attr.name === "workspacetemplate"))) ); } } @@ -431,55 +423,48 @@ class BNote extends AbstractBeccaEntity { } __validateTypeName(type?: string | null, name?: string | null) { - if (type && type !== 'label' && type !== 'relation') { + if (type && type !== "label" && type !== "relation") { throw new Error(`Unrecognized attribute type '${type}'. Only 'label' and 'relation' are possible values.`); } if (name) { const firstLetter = name.charAt(0); - if (firstLetter === '#' || firstLetter === '~') { + if (firstLetter === "#" || firstLetter === "~") { throw new Error(`Detect '#' or '~' in the attribute's name. In the API, attribute names should be set without these characters.`); } } } hasAttribute(type: string, name: string, value: string | null = null): boolean { - return !!this.getAttributes().find(attr => - attr.name === name - && (value === undefined || value === null || attr.value === value) - && attr.type === type - ); + return !!this.getAttributes().find((attr) => attr.name === name && (value === undefined || value === null || attr.value === value) && attr.type === type); } getAttributeCaseInsensitive(type: string, name: string, value?: string | null) { name = name.toLowerCase(); value = value ? value.toLowerCase() : null; - return this.getAttributes().find( - attr => attr.name.toLowerCase() === name - && (!value || attr.value.toLowerCase() === value) - && attr.type === type); + return this.getAttributes().find((attr) => attr.name.toLowerCase() === name && (!value || attr.value.toLowerCase() === value) && attr.type === type); } getRelationTarget(name: string) { - const relation = this.getAttributes().find(attr => attr.name === name && attr.type === 'relation'); + const relation = this.getAttributes().find((attr) => attr.name === name && attr.type === "relation"); return relation ? relation.targetNote : null; } /** - * @param name - label name - * @param value - label value - * @returns true if label exists (including inherited) - */ + * @param name - label name + * @param value - label value + * @returns true if label exists (including inherited) + */ hasLabel(name: string, value?: string): boolean { return this.hasAttribute(LABEL, name, value); } /** - * @param name - label name - * @returns true if label exists (including inherited) and does not have "false" value. - */ + * @param name - label name + * @returns true if label exists (including inherited) and does not have "false" value. + */ isLabelTruthy(name: string): boolean { const label = this.getLabel(name); @@ -487,127 +472,127 @@ class BNote extends AbstractBeccaEntity { return false; } - return label && label.value !== 'false'; + return label && label.value !== "false"; } /** - * @param name - label name - * @param value - label value - * @returns true if label exists (excluding inherited) - */ + * @param name - label name + * @param value - label value + * @returns true if label exists (excluding inherited) + */ hasOwnedLabel(name: string, value?: string): boolean { return this.hasOwnedAttribute(LABEL, name, value); } /** - * @param name - relation name - * @param value - relation value - * @returns true if relation exists (including inherited) - */ + * @param name - relation name + * @param value - relation value + * @returns true if relation exists (including inherited) + */ hasRelation(name: string, value?: string): boolean { return this.hasAttribute(RELATION, name, value); } /** - * @param name - relation name - * @param value - relation value - * @returns true if relation exists (excluding inherited) - */ + * @param name - relation name + * @param value - relation value + * @returns true if relation exists (excluding inherited) + */ hasOwnedRelation(name: string, value?: string): boolean { return this.hasOwnedAttribute(RELATION, name, value); } /** - * @param name - label name - * @returns label if it exists, null otherwise - */ + * @param name - label name + * @returns label if it exists, null otherwise + */ getLabel(name: string): BAttribute | null { return this.getAttribute(LABEL, name); } /** - * @param name - label name - * @returns label if it exists, null otherwise - */ + * @param name - label name + * @returns label if it exists, null otherwise + */ getOwnedLabel(name: string): BAttribute | null { return this.getOwnedAttribute(LABEL, name); } /** - * @param name - relation name - * @returns relation if it exists, null otherwise - */ + * @param name - relation name + * @returns relation if it exists, null otherwise + */ getRelation(name: string): BAttribute | null { return this.getAttribute(RELATION, name); } /** - * @param name - relation name - * @returns relation if it exists, null otherwise - */ + * @param name - relation name + * @returns relation if it exists, null otherwise + */ getOwnedRelation(name: string): BAttribute | null { return this.getOwnedAttribute(RELATION, name); } /** - * @param name - label name - * @returns label value if label exists, null otherwise - */ + * @param name - label name + * @returns label value if label exists, null otherwise + */ getLabelValue(name: string): string | null { return this.getAttributeValue(LABEL, name); } /** - * @param name - label name - * @returns label value if label exists, null otherwise - */ + * @param name - label name + * @returns label value if label exists, null otherwise + */ getOwnedLabelValue(name: string): string | null { return this.getOwnedAttributeValue(LABEL, name); } /** - * @param name - relation name - * @returns relation value if relation exists, null otherwise - */ + * @param name - relation name + * @returns relation value if relation exists, null otherwise + */ getRelationValue(name: string): string | null { return this.getAttributeValue(RELATION, name); } /** - * @param name - relation name - * @returns relation value if relation exists, null otherwise - */ + * @param name - relation name + * @returns relation value if relation exists, null otherwise + */ getOwnedRelationValue(name: string): string | null { return this.getOwnedAttributeValue(RELATION, name); } /** - * @param attribute type (label, relation, etc.) - * @param name - attribute name - * @param value - attribute value - * @returns true if note has an attribute with given type and name (excluding inherited) - */ + * @param attribute type (label, relation, etc.) + * @param name - attribute name + * @param value - attribute value + * @returns true if note has an attribute with given type and name (excluding inherited) + */ hasOwnedAttribute(type: string, name: string, value?: string): boolean { return !!this.getOwnedAttribute(type, name, value); } /** - * @param type - attribute type (label, relation, etc.) - * @param name - attribute name - * @returns attribute of the given type and name. If there are more such attributes, first is returned. - * Returns null if there's no such attribute belonging to this note. - */ + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @returns attribute of the given type and name. If there are more such attributes, first is returned. + * Returns null if there's no such attribute belonging to this note. + */ getAttribute(type: string, name: string): BAttribute | null { const attributes = this.getAttributes(); - return attributes.find(attr => attr.name === name && attr.type === type) || null; + return attributes.find((attr) => attr.name === name && attr.type === type) || null; } /** - * @param type - attribute type (label, relation, etc.) - * @param name - attribute name - * @returns attribute value of given type and name or null if no such attribute exists. - */ + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @returns attribute value of given type and name or null if no such attribute exists. + */ getAttributeValue(type: string, name: string): string | null { const attr = this.getAttribute(type, name); @@ -615,10 +600,10 @@ class BNote extends AbstractBeccaEntity { } /** - * @param type - attribute type (label, relation, etc.) - * @param name - attribute name - * @returns attribute value of given type and name or null if no such attribute exists. - */ + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @returns attribute value of given type and name or null if no such attribute exists. + */ getOwnedAttributeValue(type: string, name: string): string | null { const attr = this.getOwnedAttribute(type, name); @@ -626,87 +611,83 @@ class BNote extends AbstractBeccaEntity { } /** - * @param name - label name to filter - * @returns all note's labels (attributes with type label), including inherited ones - */ + * @param name - label name to filter + * @returns all note's labels (attributes with type label), including inherited ones + */ getLabels(name?: string): BAttribute[] { return this.getAttributes(LABEL, name); } /** - * @param name - label name to filter - * @returns all note's label values, including inherited ones - */ + * @param name - label name to filter + * @returns all note's label values, including inherited ones + */ getLabelValues(name: string): string[] { - return this.getLabels(name).map(l => l.value); + return this.getLabels(name).map((l) => l.value); } /** - * @param name - label name to filter - * @returns all note's labels (attributes with type label), excluding inherited ones - */ + * @param name - label name to filter + * @returns all note's labels (attributes with type label), excluding inherited ones + */ getOwnedLabels(name: string): BAttribute[] { return this.getOwnedAttributes(LABEL, name); } /** - * @param name - label name to filter - * @returns all note's label values, excluding inherited ones - */ + * @param name - label name to filter + * @returns all note's label values, excluding inherited ones + */ getOwnedLabelValues(name: string): string[] { - return this.getOwnedAttributes(LABEL, name).map(l => l.value); + return this.getOwnedAttributes(LABEL, name).map((l) => l.value); } /** - * @param name - relation name to filter - * @returns all note's relations (attributes with type relation), including inherited ones - */ + * @param name - relation name to filter + * @returns all note's relations (attributes with type relation), including inherited ones + */ getRelations(name?: string): BAttribute[] { return this.getAttributes(RELATION, name); } /** - * @param name - relation name to filter - * @returns all note's relations (attributes with type relation), excluding inherited ones - */ + * @param name - relation name to filter + * @returns all note's relations (attributes with type relation), excluding inherited ones + */ getOwnedRelations(name?: string | null): BAttribute[] { return this.getOwnedAttributes(RELATION, name); } /** - * Beware that the method must not create a copy of the array, but actually returns its internal array - * (for performance reasons) - * - * @param type - (optional) attribute type to filter - * @param name - (optional) attribute name to filter - * @param value - (optional) attribute value to filter - * @returns note's "owned" attributes - excluding inherited ones - */ + * Beware that the method must not create a copy of the array, but actually returns its internal array + * (for performance reasons) + * + * @param type - (optional) attribute type to filter + * @param name - (optional) attribute name to filter + * @param value - (optional) attribute value to filter + * @returns note's "owned" attributes - excluding inherited ones + */ getOwnedAttributes(type: string | null = null, name: string | null = null, value: string | null = null) { this.__validateTypeName(type, name); if (type && name && value !== undefined && value !== null) { - return this.ownedAttributes.filter(attr => attr.name === name && attr.value === value && attr.type === type); - } - else if (type && name) { - return this.ownedAttributes.filter(attr => attr.name === name && attr.type === type); - } - else if (type) { - return this.ownedAttributes.filter(attr => attr.type === type); - } - else if (name) { - return this.ownedAttributes.filter(attr => attr.name === name); - } - else { + return this.ownedAttributes.filter((attr) => attr.name === name && attr.value === value && attr.type === type); + } else if (type && name) { + return this.ownedAttributes.filter((attr) => attr.name === name && attr.type === type); + } else if (type) { + return this.ownedAttributes.filter((attr) => attr.type === type); + } else if (name) { + return this.ownedAttributes.filter((attr) => attr.name === name); + } else { return this.ownedAttributes; } } /** - * @returns attribute belonging to this specific note (excludes inherited attributes) - * - * This method can be significantly faster than the getAttribute() - */ + * @returns attribute belonging to this specific note (excludes inherited attributes) + * + * This method can be significantly faster than the getAttribute() + */ getOwnedAttribute(type: string, name: string, value: string | null = null) { const attrs = this.getOwnedAttributes(type, name, value); @@ -714,7 +695,7 @@ class BNote extends AbstractBeccaEntity { } get isArchived() { - return this.hasAttribute('label', 'archived'); + return this.hasAttribute("label", "archived"); } areAllNotePathsArchived() { @@ -734,7 +715,7 @@ class BNote extends AbstractBeccaEntity { hasInheritableArchivedLabel() { for (const attr of this.getAttributes()) { - if (attr.name === 'archived' && attr.type === LABEL && attr.isInheritable) { + if (attr.name === "archived" && attr.type === LABEL && attr.isInheritable) { return true; } } @@ -755,9 +736,7 @@ class BNote extends AbstractBeccaEntity { } }); - this.parents = this.parentBranches - .map(branch => branch.parentNote) - .filter(note => !!note) as BNote[]; + this.parents = this.parentBranches.map((branch) => branch.parentNote).filter((note) => !!note) as BNote[]; } sortChildren() { @@ -771,17 +750,17 @@ class BNote extends AbstractBeccaEntity { const aBranch = becca.getBranchFromChildAndParent(a.noteId, this.noteId); const bBranch = becca.getBranchFromChildAndParent(b.noteId, this.noteId); - return ((aBranch?.notePosition || 0) - (bBranch?.notePosition || 0)) || 0; + return (aBranch?.notePosition || 0) - (bBranch?.notePosition || 0) || 0; }); } /** - * This is used for: - * - fast searching - * - note similarity evaluation - * - * @returns - returns flattened textual representation of note, prefixes and attributes - */ + * This is used for: + * - fast searching + * - note similarity evaluation + * + * @returns - returns flattened textual representation of note, prefixes and attributes + */ getFlatText() { if (!this.__flatTextCache) { this.__flatTextCache = `${this.noteId} ${this.type} ${this.mime} `; @@ -796,13 +775,13 @@ class BNote extends AbstractBeccaEntity { for (const attr of this.getAttributes()) { // it's best to use space as separator since spaces are filtered from the search string by the tokenization into words - this.__flatTextCache += `${attr.type === 'label' ? '#' : '~'}${attr.name}`; + this.__flatTextCache += `${attr.type === "label" ? "#" : "~"}${attr.name}`; if (attr.value) { this.__flatTextCache += `=${attr.value}`; } - this.__flatTextCache += ' '; + this.__flatTextCache += " "; } this.__flatTextCache = utils.normalize(this.__flatTextCache); @@ -835,7 +814,7 @@ class BNote extends AbstractBeccaEntity { } for (const targetRelation of this.targetRelations) { - if (targetRelation.name === 'template' || targetRelation.name === 'inherit') { + if (targetRelation.name === "template" || targetRelation.name === "inherit") { const note = targetRelation.note; if (note) { @@ -846,17 +825,15 @@ class BNote extends AbstractBeccaEntity { } getRelationDefinitions() { - return this.getLabels() - .filter(l => l.name.startsWith("relation:")); + return this.getLabels().filter((l) => l.name.startsWith("relation:")); } getLabelDefinitions() { - return this.getLabels() - .filter(l => l.name.startsWith("relation:")); + return this.getLabels().filter((l) => l.name.startsWith("relation:")); } isInherited() { - return !!this.targetRelations.find(rel => rel.name === 'template' || rel.name === 'inherit'); + return !!this.targetRelations.find((rel) => rel.name === "template" || rel.name === "inherit"); } getSubtreeNotesIncludingTemplated(): BNote[] { @@ -864,7 +841,7 @@ class BNote extends AbstractBeccaEntity { function inner(note: BNote) { // _hidden is not counted as subtree for the purpose of inheritance - if (set.has(note) || note.noteId === '_hidden') { + if (set.has(note) || note.noteId === "_hidden") { return; } @@ -875,7 +852,7 @@ class BNote extends AbstractBeccaEntity { } for (const targetRelation of note.targetRelations) { - if (targetRelation.name === 'template' || targetRelation.name === 'inherit') { + if (targetRelation.name === "template" || targetRelation.name === "inherit") { const targetNote = targetRelation.note; if (targetNote) { @@ -891,26 +868,23 @@ class BNote extends AbstractBeccaEntity { } getSearchResultNotes(): BNote[] { - if (this.type !== 'search') { + if (this.type !== "search") { return []; } try { const result = searchService.searchFromNote(this); const becca = this.becca; - return (result.searchResultNoteIds) - .map(resultNoteId => becca.notes[resultNoteId]) - .filter(note => !!note); - } - catch (e: any) { + return result.searchResultNoteIds.map((resultNoteId) => becca.notes[resultNoteId]).filter((note) => !!note); + } catch (e: any) { log.error(`Could not resolve search note ${this.noteId}: ${e.message}`); return []; } } - getSubtree({includeArchived = true, includeHidden = false, resolveSearch = false} = {}): { - notes: BNote[], - relationships: Relationship[] + getSubtree({ includeArchived = true, includeHidden = false, resolveSearch = false } = {}): { + notes: BNote[]; + relationships: Relationship[]; } { const noteSet = new Set(); const relationships: Relationship[] = []; // list of tuples parentNoteId -> childNoteId @@ -920,14 +894,13 @@ class BNote extends AbstractBeccaEntity { for (const resultNote of searchNote.getSearchResultNotes()) { addSubtreeNotesInner(resultNote, searchNote); } - } - catch (e: any) { + } catch (e: any) { log.error(`Could not resolve search note ${searchNote?.noteId}: ${e.message}`); } } function addSubtreeNotesInner(note: BNote, parentNote: BNote | null = null) { - if (note.noteId === '_hidden' && !includeHidden) { + if (note.noteId === "_hidden" && !includeHidden) { return; } @@ -949,12 +922,11 @@ class BNote extends AbstractBeccaEntity { noteSet.add(note); - if (note.type === 'search') { + if (note.type === "search") { if (resolveSearch) { resolveSearchNote(note); } - } - else { + } else { for (const childNote of note.children) { addSubtreeNotesInner(childNote, note); } @@ -970,10 +942,8 @@ class BNote extends AbstractBeccaEntity { } /** @returns includes the subtree root note as well */ - getSubtreeNoteIds({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) { - return this.getSubtree({includeArchived, includeHidden, resolveSearch}) - .notes - .map(note => note.noteId); + getSubtreeNoteIds({ includeArchived = true, includeHidden = false, resolveSearch = false } = {}) { + return this.getSubtree({ includeArchived, includeHidden, resolveSearch }).notes.map((note) => note.noteId); } /** @deprecated use getSubtreeNoteIds() instead */ @@ -990,31 +960,31 @@ class BNote extends AbstractBeccaEntity { } get labelCount() { - return this.getAttributes().filter(attr => attr.type === 'label').length; + return this.getAttributes().filter((attr) => attr.type === "label").length; } get ownedLabelCount() { - return this.ownedAttributes.filter(attr => attr.type === 'label').length; + return this.ownedAttributes.filter((attr) => attr.type === "label").length; } get relationCount() { - return this.getAttributes().filter(attr => attr.type === 'relation' && !attr.isAutoLink()).length; + return this.getAttributes().filter((attr) => attr.type === "relation" && !attr.isAutoLink()).length; } get relationCountIncludingLinks() { - return this.getAttributes().filter(attr => attr.type === 'relation').length; + return this.getAttributes().filter((attr) => attr.type === "relation").length; } get ownedRelationCount() { - return this.ownedAttributes.filter(attr => attr.type === 'relation' && !attr.isAutoLink()).length; + return this.ownedAttributes.filter((attr) => attr.type === "relation" && !attr.isAutoLink()).length; } get ownedRelationCountIncludingLinks() { - return this.ownedAttributes.filter(attr => attr.type === 'relation').length; + return this.ownedAttributes.filter((attr) => attr.type === "relation").length; } get targetRelationCount() { - return this.targetRelations.filter(attr => !attr.isAutoLink()).length; + return this.targetRelations.filter((attr) => !attr.isAutoLink()).length; } get targetRelationCountIncludingLinks() { @@ -1055,7 +1025,7 @@ class BNote extends AbstractBeccaEntity { } getAncestorNoteIds(): string[] { - return this.getAncestors().map(note => note.noteId); + return this.getAncestors().map((note) => note.noteId); } hasAncestor(ancestorNoteId: string): boolean { @@ -1069,7 +1039,7 @@ class BNote extends AbstractBeccaEntity { } isInHiddenSubtree() { - return this.noteId === '_hidden' || this.hasAncestor('_hidden'); + return this.noteId === "_hidden" || this.hasAncestor("_hidden"); } getTargetRelations() { @@ -1077,12 +1047,12 @@ class BNote extends AbstractBeccaEntity { } /** @returns returns only notes which are templated, does not include their subtrees - * in effect returns notes which are influenced by note's non-inheritable attributes */ + * in effect returns notes which are influenced by note's non-inheritable attributes */ getInheritingNotes(): BNote[] { const arr: BNote[] = [this]; for (const targetRelation of this.targetRelations) { - if (targetRelation.name === 'template' || targetRelation.name === 'inherit') { + if (targetRelation.name === "template" || targetRelation.name === "inherit") { const note = targetRelation.note; if (note) { @@ -1109,8 +1079,7 @@ class BNote extends AbstractBeccaEntity { } getRevisions(): BRevision[] { - return sql.getRows("SELECT * FROM revisions WHERE noteId = ? ORDER BY revisions.utcDateCreated ASC", [this.noteId]) - .map(row => new BRevision(row)); + return sql.getRows("SELECT * FROM revisions WHERE noteId = ? ORDER BY revisions.utcDateCreated ASC", [this.noteId]).map((row) => new BRevision(row)); } getAttachments(opts: AttachmentOpts = {}) { @@ -1126,8 +1095,7 @@ class BNote extends AbstractBeccaEntity { ORDER BY position` : `SELECT * FROM attachments WHERE ownerId = ? AND isDeleted = 0 ORDER BY position`; - return sql.getRows(query, [this.noteId]) - .map(row => new BAttachment(row)); + return sql.getRows(query, [this.noteId]).map((row) => new BAttachment(row)); } getAttachmentById(attachmentId: string, opts: AttachmentOpts = {}) { @@ -1140,41 +1108,45 @@ class BNote extends AbstractBeccaEntity { WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0` : `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`; - return sql.getRows(query, [this.noteId, attachmentId]) - .map(row => new BAttachment(row))[0]; + return sql.getRows(query, [this.noteId, attachmentId]).map((row) => new BAttachment(row))[0]; } getAttachmentsByRole(role: string): BAttachment[] { - return sql.getRows(` + return sql + .getRows( + ` SELECT attachments.* FROM attachments WHERE ownerId = ? AND role = ? AND isDeleted = 0 - ORDER BY position`, [this.noteId, role]) - .map(row => new BAttachment(row)); + ORDER BY position`, + [this.noteId, role] + ) + .map((row) => new BAttachment(row)); } getAttachmentByTitle(title: string): BAttachment | undefined { // cannot use SQL to filter by title since it can be encrypted - return this.getAttachments().filter(attachment => attachment.title === title)[0]; + return this.getAttachments().filter((attachment) => attachment.title === title)[0]; } /** - * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles) - * - * @returns array of notePaths (each represented by array of noteIds constituting the particular note path) - */ + * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles) + * + * @returns array of notePaths (each represented by array of noteIds constituting the particular note path) + */ getAllNotePaths(): string[][] { - if (this.noteId === 'root') { - return [['root']]; + if (this.noteId === "root") { + return [["root"]]; } const parentNotes = this.getParentNotes(); - const notePaths = parentNotes.length === 1 - ? parentNotes[0].getAllNotePaths() // optimization for the most common case - : parentNotes.flatMap(parentNote => parentNote.getAllNotePaths()); + const notePaths = + parentNotes.length === 1 + ? parentNotes[0].getAllNotePaths() // optimization for the most common case + : parentNotes.flatMap((parentNote) => parentNote.getAllNotePaths()); for (const notePath of notePaths) { notePath.push(this.noteId); @@ -1183,14 +1155,14 @@ class BNote extends AbstractBeccaEntity { return notePaths; } - getSortedNotePathRecords(hoistedNoteId: string = 'root'): NotePathRecord[] { - const isHoistedRoot = hoistedNoteId === 'root'; + getSortedNotePathRecords(hoistedNoteId: string = "root"): NotePathRecord[] { + const isHoistedRoot = hoistedNoteId === "root"; - const notePaths = this.getAllNotePaths().map(path => ({ + 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') + isArchived: path.some((noteId) => this.becca.notes[noteId].isArchived), + isHidden: path.includes("_hidden") })); notePaths.sort((a, b) => { @@ -1209,37 +1181,37 @@ class BNote extends AbstractBeccaEntity { } /** - * Returns a note path considered to be the "best" - * - * @return array of noteIds constituting the particular note path - */ - getBestNotePath(hoistedNoteId: string = 'root'): string[] { + * Returns a note path considered to be the "best" + * + * @return array of noteIds constituting the particular note path + */ + getBestNotePath(hoistedNoteId: string = "root"): string[] { return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath; } /** - * Returns a note path considered to be the "best" - * - * @return serialized note path (e.g. 'root/a1h315/js725h') - */ - getBestNotePathString(hoistedNoteId: string = 'root'): string { + * Returns a note path considered to be the "best" + * + * @return serialized note path (e.g. 'root/a1h315/js725h') + */ + getBestNotePathString(hoistedNoteId: string = "root"): string { 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 + */ isHiddenCompletely() { - if (this.noteId === 'root') { + if (this.noteId === "root") { return false; } for (const parentNote of this.parents) { - if (parentNote.noteId === 'root') { + if (parentNote.noteId === "root") { return false; - } else if (parentNote.noteId === '_hidden') { + } else if (parentNote.noteId === "_hidden") { continue; } else if (!parentNote.isHiddenCompletely()) { return false; @@ -1250,24 +1222,24 @@ class BNote extends AbstractBeccaEntity { } /** - * @returns true if ancestorNoteId occurs in at least one of the note's paths - */ + * @returns true if ancestorNoteId occurs in at least one of the note's paths + */ isDescendantOfNote(ancestorNoteId: string): boolean { const notePaths = this.getAllNotePaths(); - return notePaths.some(path => path.includes(ancestorNoteId)); + return notePaths.some((path) => path.includes(ancestorNoteId)); } /** - * Update's given attribute's value or creates it if it doesn't exist - * - * @param type - attribute type (label, relation, etc.) - * @param name - attribute name - * @param value - attribute value (optional) - */ + * Update's given attribute's value or creates it if it doesn't exist + * + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @param value - attribute value (optional) + */ setAttribute(type: AttributeType, name: string, value?: string) { const attributes = this.getOwnedAttributes(); - const attr = attributes.find(attr => attr.type === type && attr.name === name); + const attr = attributes.find((attr) => attr.type === type && attr.name === name); value = value?.toString() || ""; @@ -1276,8 +1248,7 @@ class BNote extends AbstractBeccaEntity { attr.value = value; attr.save(); } - } - else { + } else { new BAttribute({ noteId: this.noteId, type: type, @@ -1288,12 +1259,12 @@ class BNote extends AbstractBeccaEntity { } /** - * Removes given attribute name-value pair if it exists. - * - * @param type - attribute type (label, relation, etc.) - * @param name - attribute name - * @param value - attribute value (optional) - */ + * Removes given attribute name-value pair if it exists. + * + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @param value - attribute value (optional) + */ removeAttribute(type: string, name: string, value?: string) { const attributes = this.getOwnedAttributes(); @@ -1305,13 +1276,13 @@ class BNote extends AbstractBeccaEntity { } /** - * Adds a new attribute to this note. The attribute is saved and returned. - * See addLabel, addRelation for more specific methods. - * - * @param type - attribute type (label / relation) - * @param name - name of the attribute, not including the leading ~/# - * @param value - value of the attribute - text for labels, target note ID for relations; optional. - */ + * Adds a new attribute to this note. The attribute is saved and returned. + * See addLabel, addRelation for more specific methods. + * + * @param type - attribute type (label / relation) + * @param name - name of the attribute, not including the leading ~/# + * @param value - value of the attribute - text for labels, target note ID for relations; optional. + */ addAttribute(type: AttributeType, name: string, value: string = "", isInheritable: boolean = false, position: number | null = null): BAttribute { return new BAttribute({ noteId: this.noteId, @@ -1324,100 +1295,99 @@ class BNote extends AbstractBeccaEntity { } /** - * Adds a new label to this note. The label attribute is saved and returned. - * - * @param name - name of the label, not including the leading # - * @param value - text value of the label; optional - */ + * Adds a new label to this note. The label attribute is saved and returned. + * + * @param name - name of the label, not including the leading # + * @param value - text value of the label; optional + */ addLabel(name: string, value: string = "", isInheritable: boolean = false): BAttribute { return this.addAttribute(LABEL, name, value, isInheritable); } /** - * Adds a new relation to this note. The relation attribute is saved and - * returned. - * - * @param name - name of the relation, not including the leading ~ - */ + * Adds a new relation to this note. The relation attribute is saved and + * returned. + * + * @param name - name of the relation, not including the leading ~ + */ addRelation(name: string, targetNoteId: string, isInheritable: boolean = false): BAttribute { return this.addAttribute(RELATION, name, targetNoteId, isInheritable); } /** - * Based on enabled, the attribute is either set or removed. - * - * @param type - attribute type ('relation', 'label' etc.) - * @param enabled - toggle On or Off - * @param name - attribute name - * @param value - attribute value (optional) - */ + * Based on enabled, the attribute is either set or removed. + * + * @param type - attribute type ('relation', 'label' etc.) + * @param enabled - toggle On or Off + * @param name - attribute name + * @param value - attribute value (optional) + */ toggleAttribute(type: AttributeType, enabled: boolean, name: string, value?: string) { if (enabled) { this.setAttribute(type, name, value); - } - else { + } else { this.removeAttribute(type, name, value); } } /** - * Based on enabled, label is either set or removed. - * - * @param enabled - toggle On or Off - * @param name - label name - * @param value - label value (optional) - */ + * Based on enabled, label is either set or removed. + * + * @param enabled - toggle On or Off + * @param name - label name + * @param value - label value (optional) + */ toggleLabel(enabled: boolean, name: string, value?: string) { return this.toggleAttribute(LABEL, enabled, name, value); } /** - * Based on enabled, relation is either set or removed. - * - * @param enabled - toggle On or Off - * @param name - relation name - * @param value - relation value (noteId) - */ + * Based on enabled, relation is either set or removed. + * + * @param enabled - toggle On or Off + * @param name - relation name + * @param value - relation value (noteId) + */ toggleRelation(enabled: boolean, name: string, value?: string) { return this.toggleAttribute(RELATION, enabled, name, value); } /** - * Update's given label's value or creates it if it doesn't exist - * - * @param name - label name - * @param value label value - */ + * Update's given label's value or creates it if it doesn't exist + * + * @param name - label name + * @param value label value + */ setLabel(name: string, value?: string) { return this.setAttribute(LABEL, name, value); } /** - * Update's given relation's value or creates it if it doesn't exist - * - * @param name - relation name - * @param value - relation value (noteId) - */ + * Update's given relation's value or creates it if it doesn't exist + * + * @param name - relation name + * @param value - relation value (noteId) + */ setRelation(name: string, value?: string) { return this.setAttribute(RELATION, name, value); } /** - * Remove label name-value pair, if it exists. - * - * @param name - label name - * @param value - label value - */ + * Remove label name-value pair, if it exists. + * + * @param name - label name + * @param value - label value + */ removeLabel(name: string, value?: string) { return this.removeAttribute(LABEL, name, value); } /** - * Remove the relation name-value pair, if it exists. - * - * @param name - relation name - * @param value - relation value (noteId) - */ + * Remove the relation name-value pair, if it exists. + * + * @param name - relation name + * @param value - relation value (noteId) + */ removeRelation(name: string, value?: string) { return this.removeAttribute(RELATION, name, value); } @@ -1443,11 +1413,11 @@ class BNote extends AbstractBeccaEntity { } isEligibleForConversionToAttachment(opts: ConvertOpts = { autoConversion: false }) { - if (this.type !== 'image' || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) { + if (this.type !== "image" || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) { return false; } - const targetRelations = this.getTargetRelations().filter(relation => relation.name === 'imageLink'); + const targetRelations = this.getTargetRelations().filter((relation) => relation.name === "imageLink"); if (opts.autoConversion && targetRelations.length === 0) { return false; @@ -1460,7 +1430,7 @@ class BNote extends AbstractBeccaEntity { if (referencingNote && parentNote !== referencingNote) { return false; - } else if (parentNote.type !== 'text' || !parentNote.isContentAvailable()) { + } else if (parentNote.type !== "text" || !parentNote.isContentAvailable()) { return false; } @@ -1468,20 +1438,20 @@ class BNote extends AbstractBeccaEntity { } /** - * Some notes are eligible for conversion into an attachment of its parent, note must have these properties: - * - it has exactly one target relation - * - it has a relation from its parent note - * - it has no children - * - it has no clones - * - the parent is of type text - * - both notes are either unprotected or user is in protected session - * - * Currently, works only for image notes. - * - * In the future, this functionality might get more generic and some of the requirements relaxed. - * - * @returns null if note is not eligible for conversion - */ + * Some notes are eligible for conversion into an attachment of its parent, note must have these properties: + * - it has exactly one target relation + * - it has a relation from its parent note + * - it has no children + * - it has no clones + * - the parent is of type text + * - both notes are either unprotected or user is in protected session + * + * Currently, works only for image notes. + * + * In the future, this functionality might get more generic and some of the requirements relaxed. + * + * @returns null if note is not eligible for conversion + */ convertToParentAttachment(opts: ConvertOpts = { autoConversion: false }): BAttachment | null { if (!this.isEligibleForConversionToAttachment(opts)) { return null; @@ -1491,7 +1461,7 @@ class BNote extends AbstractBeccaEntity { const parentNote = this.getParentNotes()[0]; const attachment = parentNote.saveAttachment({ - role: 'image', + role: "image", mime: this.mime, title: this.title, content: content @@ -1518,10 +1488,10 @@ class BNote extends AbstractBeccaEntity { } /** - * (Soft) delete a note and all its descendants. - * - * @param deleteId - optional delete identified - */ + * (Soft) delete a note and all its descendants. + * + * @param deleteId - optional delete identified + */ deleteNote(deleteId: string | null = null, taskContext: TaskContext | null = null) { if (this.isDeleted) { return; @@ -1532,11 +1502,11 @@ class BNote extends AbstractBeccaEntity { } if (!taskContext) { - taskContext = new TaskContext('no-progress-reporting'); + taskContext = new TaskContext("no-progress-reporting"); } // needs to be run before branches and attributes are deleted and thus attached relations disappear - handlers.runAttachedRelations(this, 'runOnNoteDeletion', this); + handlers.runAttachedRelations(this, "runOnNoteDeletion", this); taskContext.noteDeletionHandlerTriggered = true; for (const branch of this.getParentBranches()) { @@ -1551,15 +1521,14 @@ class BNote extends AbstractBeccaEntity { this.__flatTextCache = null; this.isDecrypted = true; - } - catch (e: any) { + } catch (e: any) { log.error(`Could not decrypt note ${this.noteId}: ${e.message} ${e.stack}`); } } } isLaunchBarConfig() { - return this.type === 'launcher' || ['_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(this.noteId); + return this.type === "launcher" || ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(this.noteId); } isOptions() { @@ -1576,19 +1545,22 @@ class BNote extends AbstractBeccaEntity { return sql.transactional(() => { let noteContent = this.getContent(); - const revision = new BRevision({ - noteId: this.noteId, - // title and text should be decrypted now - title: this.title, - type: this.type, - mime: this.mime, - isProtected: this.isProtected, - utcDateLastEdited: this.utcDateModified, - utcDateCreated: dateUtils.utcNowDateTime(), - utcDateModified: dateUtils.utcNowDateTime(), - dateLastEdited: this.dateModified, - dateCreated: dateUtils.localNowDateTime() - }, true); + const revision = new BRevision( + { + noteId: this.noteId, + // title and text should be decrypted now + title: this.title, + type: this.type, + mime: this.mime, + isProtected: this.isProtected, + utcDateLastEdited: this.utcDateModified, + utcDateCreated: dateUtils.utcNowDateTime(), + utcDateModified: dateUtils.utcNowDateTime(), + dateLastEdited: this.dateModified, + dateCreated: dateUtils.localNowDateTime() + }, + true + ); revision.save(); // to generate revisionId, which is then used to save attachments @@ -1602,19 +1574,20 @@ class BNote extends AbstractBeccaEntity { revisionAttachment.ownerId = revision.revisionId; revisionAttachment.setContent(noteAttachment.getContent(), { forceSave: true }); - if (this.type === 'text' && typeof noteContent === "string") { + if (this.type === "text" && typeof noteContent === "string") { // content is rewritten to point to the revision attachments - noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`, - `attachments/${revisionAttachment.attachmentId}`); + noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`, `attachments/${revisionAttachment.attachmentId}`); - noteContent = noteContent.replaceAll(new RegExp(`href="[^"]*attachmentId=${noteAttachment.attachmentId}[^"]*"`, 'gi'), - `href="api/attachments/${revisionAttachment.attachmentId}/download"`); + noteContent = noteContent.replaceAll( + new RegExp(`href="[^"]*attachmentId=${noteAttachment.attachmentId}[^"]*"`, "gi"), + `href="api/attachments/${revisionAttachment.attachmentId}/download"` + ); } } revision.setContent(noteContent); - this.eraseExcessRevisionSnapshots() + this.eraseExcessRevisionSnapshots(); return revision; }); } @@ -1625,14 +1598,14 @@ class BNote extends AbstractBeccaEntity { // lable has a higher priority let revisionSnapshotNumberLimit = parseInt(this.getLabelValue("versioningLimit") ?? ""); if (!Number.isInteger(revisionSnapshotNumberLimit)) { - revisionSnapshotNumberLimit = parseInt(optionService.getOption('revisionSnapshotNumberLimit')); + revisionSnapshotNumberLimit = parseInt(optionService.getOption("revisionSnapshotNumberLimit")); } if (revisionSnapshotNumberLimit >= 0) { const revisions = this.getRevisions(); if (revisions.length - revisionSnapshotNumberLimit > 0) { const revisionIds = revisions .slice(0, revisions.length - revisionSnapshotNumberLimit) - .map(revision => revision.revisionId) + .map((revision) => revision.revisionId) .filter((id): id is string => id !== undefined); eraseService.eraseRevisions(revisionIds); } @@ -1640,33 +1613,35 @@ class BNote extends AbstractBeccaEntity { } /** - * @param matchBy - choose by which property we detect if to update an existing attachment. - * Supported values are either 'attachmentId' (default) or 'title' - */ - saveAttachment({attachmentId, role, mime, title, content, position}: AttachmentRow, matchBy = 'attachmentId') { - if (!['attachmentId', 'title'].includes(matchBy)) { + * @param matchBy - choose by which property we detect if to update an existing attachment. + * Supported values are either 'attachmentId' (default) or 'title' + */ + saveAttachment({ attachmentId, role, mime, title, content, position }: AttachmentRow, matchBy = "attachmentId") { + if (!["attachmentId", "title"].includes(matchBy)) { throw new Error(`Unsupported value '${matchBy}' for matchBy param, has to be either 'attachmentId' or 'title'.`); } let attachment; - if (matchBy === 'title' && title) { + if (matchBy === "title" && title) { attachment = this.getAttachmentByTitle(title); - } else if (matchBy === 'attachmentId' && attachmentId) { + } else if (matchBy === "attachmentId" && attachmentId) { attachment = this.becca.getAttachmentOrThrow(attachmentId); } - attachment = attachment || new BAttachment({ - ownerId: this.noteId, - title, - role, - mime, - isProtected: this.isProtected, - position - }); + attachment = + attachment || + new BAttachment({ + ownerId: this.noteId, + title, + role, + mime, + isProtected: this.isProtected, + position + }); content = content || ""; - attachment.setContent(content, {forceSave: true}); + attachment.setContent(content, { forceSave: true }); return attachment; } @@ -1706,8 +1681,7 @@ class BNote extends AbstractBeccaEntity { if (pojo.isProtected) { if (this.isDecrypted && pojo.title) { pojo.title = protectedSessionService.encrypt(pojo.title) || undefined; - } - else { + } else { // updating protected note outside of protected session means we will keep original ciphertexts delete pojo.title; } diff --git a/src/becca/entities/boption.ts b/src/becca/entities/boption.ts index 4ae5ba7a9..f18be2986 100644 --- a/src/becca/entities/boption.ts +++ b/src/becca/entities/boption.ts @@ -2,15 +2,21 @@ import dateUtils from "../../services/date_utils.js"; import AbstractBeccaEntity from "./abstract_becca_entity.js"; -import { OptionRow } from './rows.js'; +import { OptionRow } from "./rows.js"; /** * Option represents a name-value pair, either directly configurable by the user or some system property. */ class BOption extends AbstractBeccaEntity { - static get entityName() { return "options"; } - static get primaryKeyName() { return "name"; } - static get hashedProperties() { return ["name", "value"]; } + static get entityName() { + return "options"; + } + static get primaryKeyName() { + return "name"; + } + static get hashedProperties() { + return ["name", "value"]; + } name!: string; value!: string; @@ -43,7 +49,7 @@ class BOption extends AbstractBeccaEntity { value: this.value, isSynced: this.isSynced, utcDateModified: this.utcDateModified - } + }; } } diff --git a/src/becca/entities/brecent_note.ts b/src/becca/entities/brecent_note.ts index f9f31a98f..d4c54ba2e 100644 --- a/src/becca/entities/brecent_note.ts +++ b/src/becca/entities/brecent_note.ts @@ -9,9 +9,15 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js"; * RecentNote represents recently visited note. */ class BRecentNote extends AbstractBeccaEntity { - static get entityName() { return "recent_notes"; } - static get primaryKeyName() { return "noteId"; } - static get hashedProperties() { return ["noteId", "notePath"]; } + static get entityName() { + return "recent_notes"; + } + static get primaryKeyName() { + return "noteId"; + } + static get hashedProperties() { + return ["noteId", "notePath"]; + } noteId!: string; notePath!: string; @@ -33,7 +39,7 @@ class BRecentNote extends AbstractBeccaEntity { noteId: this.noteId, notePath: this.notePath, utcDateCreated: this.utcDateCreated - } + }; } } diff --git a/src/becca/entities/brevision.ts b/src/becca/entities/brevision.ts index 8ba6fbead..352302318 100644 --- a/src/becca/entities/brevision.ts +++ b/src/becca/entities/brevision.ts @@ -7,7 +7,7 @@ import becca from "../becca.js"; import AbstractBeccaEntity from "./abstract_becca_entity.js"; import sql from "../../services/sql.js"; import BAttachment from "./battachment.js"; -import { AttachmentRow, RevisionRow } from './rows.js'; +import { AttachmentRow, RevisionRow } from "./rows.js"; import eraseService from "../../services/erase.js"; interface ContentOpts { @@ -24,10 +24,15 @@ interface GetByIdOpts { * It's used for seamless note versioning. */ class BRevision extends AbstractBeccaEntity { - static get entityName() { return "revisions"; } - static get primaryKeyName() { return "revisionId"; } - static get hashedProperties() { return ["revisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", - "utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"]; } + static get entityName() { + return "revisions"; + } + static get primaryKeyName() { + return "revisionId"; + } + static get hashedProperties() { + return ["revisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"]; + } revisionId?: string; noteId!: string; @@ -75,25 +80,27 @@ class BRevision extends AbstractBeccaEntity { } isContentAvailable() { - return !this.revisionId // new note which was not encrypted yet - || !this.isProtected - || protectedSessionService.isProtectedSessionAvailable() + return ( + !this.revisionId || // new note which was not encrypted yet + !this.isProtected || + protectedSessionService.isProtectedSessionAvailable() + ); } /* - * Note revision content has quite special handling - it's not a separate entity, but a lazily loaded - * part of Revision entity with its own sync. The reason behind this hybrid design is that - * content can be quite large, and it's not necessary to load it / fill memory for any note access even - * if we don't need a content, especially for bulk operations like search. - * - * This is the same approach as is used for Note's content. - */ + * Note revision content has quite special handling - it's not a separate entity, but a lazily loaded + * part of Revision entity with its own sync. The reason behind this hybrid design is that + * content can be quite large, and it's not necessary to load it / fill memory for any note access even + * if we don't need a content, especially for bulk operations like search. + * + * This is the same approach as is used for Note's content. + */ getContent(): string | Buffer { return this._getContent(); } /** - * @throws Error in case of invalid JSON */ + * @throws Error in case of invalid JSON */ getJsonContent(): {} | null { const content = this.getContent(); @@ -108,8 +115,7 @@ class BRevision extends AbstractBeccaEntity { getJsonContentSafely(): {} | null { try { return this.getJsonContent(); - } - catch (e) { + } catch (e) { return null; } } @@ -119,12 +125,16 @@ class BRevision extends AbstractBeccaEntity { } getAttachments(): BAttachment[] { - return sql.getRows(` + return sql + .getRows( + ` SELECT attachments.* FROM attachments WHERE ownerId = ? - AND isDeleted = 0`, [this.revisionId]) - .map(row => new BAttachment(row)); + AND isDeleted = 0`, + [this.revisionId] + ) + .map((row) => new BAttachment(row)); } getAttachmentById(attachmentId: String, opts: GetByIdOpts = {}): BAttachment | null { @@ -137,29 +147,32 @@ class BRevision extends AbstractBeccaEntity { WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0` : `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`; - return sql.getRows(query, [this.revisionId, attachmentId]) - .map(row => new BAttachment(row))[0]; + return sql.getRows(query, [this.revisionId, attachmentId]).map((row) => new BAttachment(row))[0]; } getAttachmentsByRole(role: string): BAttachment[] { - return sql.getRows(` + return sql + .getRows( + ` SELECT attachments.* FROM attachments WHERE ownerId = ? AND role = ? AND isDeleted = 0 - ORDER BY position`, [this.revisionId, role]) - .map(row => new BAttachment(row)); + ORDER BY position`, + [this.revisionId, role] + ) + .map((row) => new BAttachment(row)); } getAttachmentByTitle(title: string): BAttachment { // cannot use SQL to filter by title since it can be encrypted - return this.getAttachments().filter(attachment => attachment.title === title)[0]; + return this.getAttachments().filter((attachment) => attachment.title === title)[0]; } /** - * Revisions are not soft-deletable, they are immediately hard-deleted (erased). - */ + * Revisions are not soft-deletable, they are immediately hard-deleted (erased). + */ eraseRevision() { if (this.revisionId) { eraseService.eraseRevisions([this.revisionId]); @@ -199,8 +212,7 @@ class BRevision extends AbstractBeccaEntity { if (pojo.isProtected) { if (protectedSessionService.isProtectedSessionAvailable()) { pojo.title = protectedSessionService.encrypt(this.title) || undefined; - } - else { + } else { // updating protected note outside of protected session means we will keep original ciphertexts delete pojo.title; } diff --git a/src/becca/entities/rows.ts b/src/becca/entities/rows.ts index bced96c78..d2d327541 100644 --- a/src/becca/entities/rows.ts +++ b/src/becca/entities/rows.ts @@ -100,8 +100,25 @@ export interface BranchRow { * end user. Those types should be used only for checking against, they are * not for direct use. */ -export const ALLOWED_NOTE_TYPES = [ "file", "image", "search", "noteMap", "launcher", "doc", "contentWidget", "text", "relationMap", "render", "canvas", "mermaid", "book", "webView", "code", "mindMap" ] as const; -export type NoteType = typeof ALLOWED_NOTE_TYPES[number]; +export const ALLOWED_NOTE_TYPES = [ + "file", + "image", + "search", + "noteMap", + "launcher", + "doc", + "contentWidget", + "text", + "relationMap", + "render", + "canvas", + "mermaid", + "book", + "webView", + "code", + "mindMap" +] as const; +export type NoteType = (typeof ALLOWED_NOTE_TYPES)[number]; export interface NoteRow { noteId: string; diff --git a/src/becca/entity_constructor.ts b/src/becca/entity_constructor.ts index 49476961c..b6dd4c52b 100644 --- a/src/becca/entity_constructor.ts +++ b/src/becca/entity_constructor.ts @@ -1,4 +1,4 @@ -import { ConstructorData } from './becca-interface.js'; +import { ConstructorData } from "./becca-interface.js"; import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import BAttachment from "./entities/battachment.js"; import BAttribute from "./entities/battribute.js"; @@ -13,15 +13,15 @@ import BRevision from "./entities/brevision.js"; type EntityClass = new (row?: any) => AbstractBeccaEntity; const ENTITY_NAME_TO_ENTITY: Record & EntityClass> = { - "attachments": BAttachment, - "attributes": BAttribute, - "blobs": BBlob, - "branches": BBranch, - "etapi_tokens": BEtapiToken, - "notes": BNote, - "options": BOption, - "recent_notes": BRecentNote, - "revisions": BRevision + attachments: BAttachment, + attributes: BAttribute, + blobs: BBlob, + branches: BBranch, + etapi_tokens: BEtapiToken, + notes: BNote, + options: BOption, + recent_notes: BRecentNote, + revisions: BRevision }; function getEntityFromEntityName(entityName: keyof typeof ENTITY_NAME_TO_ENTITY) { diff --git a/src/becca/similarity.ts b/src/becca/similarity.ts index 3c9b071b4..5313419d6 100644 --- a/src/becca/similarity.ts +++ b/src/becca/similarity.ts @@ -7,11 +7,7 @@ import BNote from "./entities/bnote.js"; const DEBUG = false; -const IGNORED_ATTRS = [ - "datenote", - "monthnote", - "yearnote" -]; +const IGNORED_ATTRS = ["datenote", "monthnote", "yearnote"]; const IGNORED_ATTR_NAMES = [ "includenotelink", @@ -30,7 +26,7 @@ const IGNORED_ATTR_NAMES = [ "similarnoteswidgetdisabled", "disableinclusion", "rendernote", - "pageurl", + "pageurl" ]; interface DateLimits { @@ -42,9 +38,9 @@ interface DateLimits { function filterUrlValue(value: string) { return value - .replace(/https?:\/\//ig, "") - .replace(/www.js\./ig, "") - .replace(/(\.net|\.com|\.org|\.info|\.edu)/ig, ""); + .replace(/https?:\/\//gi, "") + .replace(/www.js\./gi, "") + .replace(/(\.net|\.com|\.org|\.info|\.edu)/gi, ""); } function buildRewardMap(note: BNote) { @@ -61,8 +57,7 @@ function buildRewardMap(note: BNote) { const currentReward = map.get(word) || 0; // reward grows with the length of matched string - const length = word.length - - 0.9; // to penalize specifically very short words - 1 and 2 characters + const length = word.length - 0.9; // to penalize specifically very short words - 1 and 2 characters map.set(word, currentReward + rewardFactor * Math.pow(length, 0.7)); } @@ -70,7 +65,7 @@ function buildRewardMap(note: BNote) { } for (const ancestorNote of note.getAncestors()) { - if (ancestorNote.noteId === 'root') { + if (ancestorNote.noteId === "root") { continue; } @@ -94,9 +89,7 @@ function buildRewardMap(note: BNote) { } for (const attr of note.getAttributes()) { - if (attr.name.startsWith('child:') - || attr.name.startsWith('relation:') - || attr.name.startsWith('label:')) { + if (attr.name.startsWith("child:") || attr.name.startsWith("relation:") || attr.name.startsWith("label:")) { continue; } @@ -111,13 +104,13 @@ function buildRewardMap(note: BNote) { addToRewardMap(attr.name, reward); } - if (attr.name === 'cliptype') { + if (attr.name === "cliptype") { reward /= 2; } let value = attr.value; - if (value.startsWith('http')) { + if (value.startsWith("http")) { value = filterUrlValue(value); // words in URLs are not that valuable @@ -127,7 +120,7 @@ function buildRewardMap(note: BNote) { addToRewardMap(value, reward); } - if (note.type === 'text' && note.isDecrypted) { + if (note.type === "text" && note.isDecrypted) { const content = note.getContent(); const dom = new JSDOM(content); @@ -135,7 +128,7 @@ function buildRewardMap(note: BNote) { for (const el of dom.window.document.querySelectorAll(elName)) { addToRewardMap(el.textContent, rewardFactor); } - } + }; // the title is the top with weight 1 so smaller headings will have lower weight @@ -154,12 +147,12 @@ function buildRewardMap(note: BNote) { const mimeCache: Record = {}; function trimMime(mime: string) { - if (!mime || mime === 'text/html') { + if (!mime || mime === "text/html") { return; } if (!(mime in mimeCache)) { - const chunks = mime.split('/'); + const chunks = mime.split("/"); let str = ""; @@ -167,7 +160,7 @@ function trimMime(mime: string) { // we're not interested in 'text/' or 'application/' prefix str = chunks[1]; - if (str.startsWith('-x')) { + if (str.startsWith("-x")) { str = str.substr(2); } } @@ -185,7 +178,7 @@ function buildDateLimits(baseNote: BNote): DateLimits { minDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs - 3600 * 1000)), minExcludedDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs - 5 * 1000)), maxExcludedDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs + 5 * 1000)), - maxDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs + 3600 * 1000)), + maxDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs + 3600 * 1000)) }; } @@ -193,9 +186,34 @@ function buildDateLimits(baseNote: BNote): DateLimits { const wordCache = new Map(); const WORD_BLACKLIST = [ - "a", "the", "in", "for", "from", "but", "s", "so", "if", "while", "until", - "whether", "after", "before", "because", "since", "when", "where", "how", - "than", "then", "and", "either", "or", "neither", "nor", "both", "also" + "a", + "the", + "in", + "for", + "from", + "but", + "s", + "so", + "if", + "while", + "until", + "whether", + "after", + "before", + "because", + "since", + "when", + "where", + "how", + "than", + "then", + "and", + "either", + "or", + "neither", + "nor", + "both", + "also" ]; function splitToWords(text: string) { @@ -212,8 +230,7 @@ function splitToWords(text: string) { // special case for english plurals else if (words[idx].length > 2 && words[idx].endsWith("es")) { words[idx] = words[idx].substr(0, words[idx] - 2); - } - else if (words[idx].length > 1 && words[idx].endsWith("s")) { + } else if (words[idx].length > 1 && words[idx].endsWith("s")) { words[idx] = words[idx].substr(0, words[idx] - 1); } } @@ -227,9 +244,7 @@ function splitToWords(text: string) { * that it doesn't actually need to be shown to the user. */ function hasConnectingRelation(sourceNote: BNote, targetNote: BNote) { - return sourceNote.getAttributes().find(attr => attr.type === 'relation' - && ['includenotelink', 'imagelink'].includes(attr.name) - && attr.value === targetNote.noteId); + return sourceNote.getAttributes().find((attr) => attr.type === "relation" && ["includenotelink", "imagelink"].includes(attr.name) && attr.value === targetNote.noteId); } async function findSimilarNotes(noteId: string) { @@ -246,14 +261,13 @@ async function findSimilarNotes(noteId: string) { try { dateLimits = buildDateLimits(baseNote); - } - catch (e: any) { + } catch (e: any) { throw new Error(`Date limits failed with ${e.message}, entity: ${JSON.stringify(baseNote.getPojo())}`); } const rewardMap = buildRewardMap(baseNote); let ancestorRewardCache: Record = {}; - const ancestorNoteIds = new Set(baseNote.getAncestors().map(note => note.noteId)); + const ancestorNoteIds = new Set(baseNote.getAncestors().map((note) => note.noteId)); ancestorNoteIds.add(baseNote.noteId); let displayRewards = false; @@ -270,7 +284,7 @@ async function findSimilarNotes(noteId: string) { const lengthPenalization = 1 / Math.pow(text.length, 0.3); for (const word of splitToWords(text)) { - const reward = (rewardMap.get(word) * factor * lengthPenalization) || 0; + const reward = rewardMap.get(word) * factor * lengthPenalization || 0; if (displayRewards && reward > 0) { console.log(`Reward ${Math.round(reward * 10) / 10} for word: ${word}`); @@ -294,7 +308,6 @@ async function findSimilarNotes(noteId: string) { for (const parentNote of note.parents) { if (!ancestorNoteIds.has(parentNote.noteId)) { - if (displayRewards) { console.log("Considering", parentNote.title); } @@ -304,8 +317,7 @@ async function findSimilarNotes(noteId: string) { } for (const branch of parentNote.getParentBranches()) { - score += gatherRewards(branch.prefix, 0.3) - + gatherAncestorRewards(branch.parentNote); + score += gatherRewards(branch.prefix, 0.3) + gatherAncestorRewards(branch.parentNote); } } } @@ -317,8 +329,7 @@ async function findSimilarNotes(noteId: string) { } function computeScore(candidateNote: BNote) { - let score = gatherRewards(trimMime(candidateNote.mime)) - + gatherAncestorRewards(candidateNote); + let score = gatherRewards(trimMime(candidateNote.mime)) + gatherAncestorRewards(candidateNote); if (candidateNote.isDecrypted) { score += gatherRewards(candidateNote.title); @@ -329,9 +340,7 @@ async function findSimilarNotes(noteId: string) { } for (const attr of candidateNote.getAttributes()) { - if (attr.name.startsWith('child:') - || attr.name.startsWith('relation:') - || attr.name.startsWith('label:')) { + if (attr.name.startsWith("child:") || attr.name.startsWith("relation:") || attr.name.startsWith("label:")) { continue; } @@ -349,8 +358,7 @@ async function findSimilarNotes(noteId: string) { if (!value.startsWith) { log.info(`Unexpected falsy value for attribute ${JSON.stringify(attr.getPojo())}`); continue; - } - else if (value.startsWith('http')) { + } else if (value.startsWith("http")) { value = filterUrlValue(value); // words in URLs are not that valuable @@ -369,13 +377,13 @@ async function findSimilarNotes(noteId: string) { } /** - * We want to improve the standing of notes which have been created in similar time to each other since - * there's a good chance they are related. - * - * But there's an exception - if they were created really close to each other (within few seconds) then - * they are probably part of the import and not created by hand - these OTOH should not benefit. - */ - const {utcDateCreated} = candidateNote; + * We want to improve the standing of notes which have been created in similar time to each other since + * there's a good chance they are related. + * + * But there's an exception - if they were created really close to each other (within few seconds) then + * they are probably part of the import and not created by hand - these OTOH should not benefit. + */ + const { utcDateCreated } = candidateNote; if (utcDateCreated < dateLimits.minExcludedDate || utcDateCreated > dateLimits.maxExcludedDate) { if (utcDateCreated >= dateLimits.minDate && utcDateCreated <= dateLimits.maxDate) { @@ -384,9 +392,7 @@ async function findSimilarNotes(noteId: string) { } score += 1; - } - else if (utcDateCreated.substr(0, 10) === dateLimits.minDate.substr(0, 10) - || utcDateCreated.substr(0, 10) === dateLimits.maxDate.substr(0, 10)) { + } else if (utcDateCreated.substr(0, 10) === dateLimits.minDate.substr(0, 10) || utcDateCreated.substr(0, 10) === dateLimits.maxDate.substr(0, 10)) { if (displayRewards) { console.log("Adding reward for same day of creation"); } @@ -400,9 +406,7 @@ async function findSimilarNotes(noteId: string) { } for (const candidateNote of Object.values(becca.notes)) { - if (candidateNote.noteId === baseNote.noteId - || hasConnectingRelation(candidateNote, baseNote) - || hasConnectingRelation(baseNote, candidateNote)) { + if (candidateNote.noteId === baseNote.noteId || hasConnectingRelation(candidateNote, baseNote) || hasConnectingRelation(baseNote, candidateNote)) { continue; } @@ -420,7 +424,7 @@ async function findSimilarNotes(noteId: string) { score -= 0.5; // archived penalization } - results.push({score, notePath, noteId: candidateNote.noteId}); + results.push({ score, notePath, noteId: candidateNote.noteId }); } i++; @@ -430,13 +434,13 @@ async function findSimilarNotes(noteId: string) { } } - results.sort((a, b) => a.score > b.score ? -1 : 1); + results.sort((a, b) => (a.score > b.score ? -1 : 1)); if (DEBUG) { console.log("REWARD MAP", rewardMap); if (results.length >= 1) { - for (const {noteId} of results) { + for (const { noteId } of results) { const note = becca.notes[noteId]; displayRewards = true; diff --git a/src/etapi/app_info.ts b/src/etapi/app_info.ts index 53cb50ee4..ec5b781c2 100644 --- a/src/etapi/app_info.ts +++ b/src/etapi/app_info.ts @@ -1,9 +1,9 @@ -import { Router } from 'express'; +import { Router } from "express"; import appInfo from "../services/app_info.js"; import eu from "./etapi_utils.js"; function register(router: Router) { - eu.route(router, 'get', '/etapi/app-info', (req, res, next) => { + eu.route(router, "get", "/etapi/app-info", (req, res, next) => { res.status(200).json(appInfo); }); } diff --git a/src/etapi/attachments.ts b/src/etapi/attachments.ts index 949a6984d..38b7d9603 100644 --- a/src/etapi/attachments.ts +++ b/src/etapi/attachments.ts @@ -3,21 +3,21 @@ import eu from "./etapi_utils.js"; import mappers from "./mappers.js"; import v from "./validators.js"; import utils from "../services/utils.js"; -import { Router } from 'express'; -import { AttachmentRow } from '../becca/entities/rows.js'; -import { ValidatorMap } from './etapi-interface.js'; +import { Router } from "express"; +import { AttachmentRow } from "../becca/entities/rows.js"; +import { ValidatorMap } from "./etapi-interface.js"; function register(router: Router) { const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT: ValidatorMap = { - 'ownerId': [v.notNull, v.isNoteId], - 'role': [v.notNull, v.isString], - 'mime': [v.notNull, v.isString], - 'title': [v.notNull, v.isString], - 'position': [v.notNull, v.isInteger], - 'content': [v.isString], + ownerId: [v.notNull, v.isNoteId], + role: [v.notNull, v.isString], + mime: [v.notNull, v.isString], + title: [v.notNull, v.isString], + position: [v.notNull, v.isInteger], + content: [v.isString] }; - eu.route(router, 'post', '/etapi/attachments', (req, res, next) => { + eu.route(router, "post", "/etapi/attachments", (req, res, next) => { const _params: Partial = {}; eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT); const params = _params as AttachmentRow; @@ -30,26 +30,25 @@ function register(router: Router) { const attachment = note.saveAttachment(params); res.status(201).json(mappers.mapAttachmentToPojo(attachment)); - } - catch (e: any) { + } catch (e: any) { throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message); } }); - eu.route(router, 'get', '/etapi/attachments/:attachmentId', (req, res, next) => { + eu.route(router, "get", "/etapi/attachments/:attachmentId", (req, res, next) => { const attachment = eu.getAndCheckAttachment(req.params.attachmentId); res.json(mappers.mapAttachmentToPojo(attachment)); }); const ALLOWED_PROPERTIES_FOR_PATCH = { - 'role': [v.notNull, v.isString], - 'mime': [v.notNull, v.isString], - 'title': [v.notNull, v.isString], - 'position': [v.notNull, v.isInteger], + role: [v.notNull, v.isString], + mime: [v.notNull, v.isString], + title: [v.notNull, v.isString], + position: [v.notNull, v.isInteger] }; - eu.route(router, 'patch', '/etapi/attachments/:attachmentId', (req, res, next) => { + eu.route(router, "patch", "/etapi/attachments/:attachmentId", (req, res, next) => { const attachment = eu.getAndCheckAttachment(req.params.attachmentId); if (attachment.isProtected) { @@ -62,7 +61,7 @@ function register(router: Router) { res.json(mappers.mapAttachmentToPojo(attachment)); }); - eu.route(router, 'get', '/etapi/attachments/:attachmentId/content', (req, res, next) => { + eu.route(router, "get", "/etapi/attachments/:attachmentId/content", (req, res, next) => { const attachment = eu.getAndCheckAttachment(req.params.attachmentId); if (attachment.isProtected) { @@ -71,15 +70,15 @@ function register(router: Router) { const filename = utils.formatDownloadTitle(attachment.title, attachment.role, attachment.mime); - res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); + res.setHeader("Content-Disposition", utils.getContentDisposition(filename)); res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - res.setHeader('Content-Type', attachment.mime); + res.setHeader("Content-Type", attachment.mime); res.send(attachment.getContent()); }); - eu.route(router, 'put', '/etapi/attachments/:attachmentId/content', (req, res, next) => { + eu.route(router, "put", "/etapi/attachments/:attachmentId/content", (req, res, next) => { const attachment = eu.getAndCheckAttachment(req.params.attachmentId); if (attachment.isProtected) { @@ -91,7 +90,7 @@ function register(router: Router) { return res.sendStatus(204); }); - eu.route(router, 'delete', '/etapi/attachments/:attachmentId', (req, res, next) => { + eu.route(router, "delete", "/etapi/attachments/:attachmentId", (req, res, next) => { const attachment = becca.getAttachment(req.params.attachmentId); if (!attachment) { diff --git a/src/etapi/attributes.ts b/src/etapi/attributes.ts index 4d749744b..94e2e23ec 100644 --- a/src/etapi/attributes.ts +++ b/src/etapi/attributes.ts @@ -3,29 +3,29 @@ import eu from "./etapi_utils.js"; import mappers from "./mappers.js"; import attributeService from "../services/attributes.js"; import v from "./validators.js"; -import { Router } from 'express'; -import { AttributeRow } from '../becca/entities/rows.js'; -import { ValidatorMap } from './etapi-interface.js'; +import { Router } from "express"; +import { AttributeRow } from "../becca/entities/rows.js"; +import { ValidatorMap } from "./etapi-interface.js"; function register(router: Router) { - eu.route(router, 'get', '/etapi/attributes/:attributeId', (req, res, next) => { + eu.route(router, "get", "/etapi/attributes/:attributeId", (req, res, next) => { const attribute = eu.getAndCheckAttribute(req.params.attributeId); res.json(mappers.mapAttributeToPojo(attribute)); }); const ALLOWED_PROPERTIES_FOR_CREATE_ATTRIBUTE: ValidatorMap = { - 'attributeId': [v.mandatory, v.notNull, v.isValidEntityId], - 'noteId': [v.mandatory, v.notNull, v.isNoteId], - 'type': [v.mandatory, v.notNull, v.isAttributeType], - 'name': [v.mandatory, v.notNull, v.isString], - 'value': [v.notNull, v.isString], - 'isInheritable': [v.notNull, v.isBoolean], - 'position': [v.notNull, v.isInteger] + attributeId: [v.mandatory, v.notNull, v.isValidEntityId], + noteId: [v.mandatory, v.notNull, v.isNoteId], + type: [v.mandatory, v.notNull, v.isAttributeType], + name: [v.mandatory, v.notNull, v.isString], + value: [v.notNull, v.isString], + isInheritable: [v.notNull, v.isBoolean], + position: [v.notNull, v.isInteger] }; - eu.route(router, 'post', '/etapi/attributes', (req, res, next) => { - if (req.body.type === 'relation') { + eu.route(router, "post", "/etapi/attributes", (req, res, next) => { + if (req.body.type === "relation") { eu.getAndCheckNote(req.body.value); } @@ -37,27 +37,26 @@ function register(router: Router) { const attr = attributeService.createAttribute(params); res.status(201).json(mappers.mapAttributeToPojo(attr)); - } - catch (e: any) { + } catch (e: any) { throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message); } }); const ALLOWED_PROPERTIES_FOR_PATCH_LABEL = { - 'value': [v.notNull, v.isString], - 'position': [v.notNull, v.isInteger] + value: [v.notNull, v.isString], + position: [v.notNull, v.isInteger] }; const ALLOWED_PROPERTIES_FOR_PATCH_RELATION = { - 'position': [v.notNull, v.isInteger] + position: [v.notNull, v.isInteger] }; - eu.route(router, 'patch', '/etapi/attributes/:attributeId', (req, res, next) => { + eu.route(router, "patch", "/etapi/attributes/:attributeId", (req, res, next) => { const attribute = eu.getAndCheckAttribute(req.params.attributeId); - if (attribute.type === 'label') { + if (attribute.type === "label") { eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_LABEL); - } else if (attribute.type === 'relation') { + } else if (attribute.type === "relation") { eu.getAndCheckNote(req.body.value); eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_RELATION); @@ -68,7 +67,7 @@ function register(router: Router) { res.json(mappers.mapAttributeToPojo(attribute)); }); - eu.route(router, 'delete', '/etapi/attributes/:attributeId', (req, res, next) => { + eu.route(router, "delete", "/etapi/attributes/:attributeId", (req, res, next) => { const attribute = becca.getAttribute(req.params.attributeId); if (!attribute) { diff --git a/src/etapi/auth.ts b/src/etapi/auth.ts index 4ca2fb45b..ce44eaab1 100644 --- a/src/etapi/auth.ts +++ b/src/etapi/auth.ts @@ -2,24 +2,24 @@ import becca from "../becca/becca.js"; import eu from "./etapi_utils.js"; import passwordEncryptionService from "../services/encryption/password_encryption.js"; import etapiTokenService from "../services/etapi_tokens.js"; -import { RequestHandler, Router } from 'express'; +import { RequestHandler, Router } from "express"; function register(router: Router, loginMiddleware: RequestHandler[]) { - eu.NOT_AUTHENTICATED_ROUTE(router, 'post', '/etapi/auth/login', loginMiddleware, (req, res, next) => { - const {password, tokenName} = req.body; + eu.NOT_AUTHENTICATED_ROUTE(router, "post", "/etapi/auth/login", loginMiddleware, (req, res, next) => { + const { password, tokenName } = req.body; if (!passwordEncryptionService.verifyPassword(password)) { throw new eu.EtapiError(401, "WRONG_PASSWORD", "Wrong password."); } - const {authToken} = etapiTokenService.createToken(tokenName || "ETAPI login"); + const { authToken } = etapiTokenService.createToken(tokenName || "ETAPI login"); res.status(201).json({ authToken }); }); - eu.route(router, 'post', '/etapi/auth/logout', (req, res, next) => { + eu.route(router, "post", "/etapi/auth/logout", (req, res, next) => { const parsed = etapiTokenService.parseAuthToken(req.headers.authorization); if (!parsed || !parsed.etapiTokenId) { @@ -41,4 +41,4 @@ function register(router: Router, loginMiddleware: RequestHandler[]) { export default { register -} +}; diff --git a/src/etapi/backup.ts b/src/etapi/backup.ts index f1b80630d..3b9f3f874 100644 --- a/src/etapi/backup.ts +++ b/src/etapi/backup.ts @@ -4,7 +4,7 @@ import eu from "./etapi_utils.js"; import backupService from "../services/backup.js"; function register(router: Router) { - eu.route(router, 'put', '/etapi/backup/:backupName', async (req, res, next) => { + eu.route(router, "put", "/etapi/backup/:backupName", async (req, res, next) => { await backupService.backupNow(req.params.backupName); res.sendStatus(204); diff --git a/src/etapi/branches.ts b/src/etapi/branches.ts index 8a6b61e2f..ab1425947 100644 --- a/src/etapi/branches.ts +++ b/src/etapi/branches.ts @@ -9,21 +9,21 @@ import v from "./validators.js"; import { BranchRow } from "../becca/entities/rows.js"; function register(router: Router) { - eu.route(router, 'get', '/etapi/branches/:branchId', (req, res, next) => { + eu.route(router, "get", "/etapi/branches/:branchId", (req, res, next) => { const branch = eu.getAndCheckBranch(req.params.branchId); res.json(mappers.mapBranchToPojo(branch)); }); const ALLOWED_PROPERTIES_FOR_CREATE_BRANCH = { - 'noteId': [v.mandatory, v.notNull, v.isNoteId], - 'parentNoteId': [v.mandatory, v.notNull, v.isNoteId], - 'notePosition': [v.notNull, v.isInteger], - 'prefix': [v.isString], - 'isExpanded': [v.notNull, v.isBoolean] + noteId: [v.mandatory, v.notNull, v.isNoteId], + parentNoteId: [v.mandatory, v.notNull, v.isNoteId], + notePosition: [v.notNull, v.isInteger], + prefix: [v.isString], + isExpanded: [v.notNull, v.isBoolean] }; - eu.route(router, 'post', '/etapi/branches', (req, res, next) => { + eu.route(router, "post", "/etapi/branches", (req, res, next) => { const _params = {}; eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_BRANCH); const params: BranchRow = _params as BranchRow; @@ -49,12 +49,12 @@ function register(router: Router) { }); const ALLOWED_PROPERTIES_FOR_PATCH = { - 'notePosition': [v.notNull, v.isInteger], - 'prefix': [v.isString], - 'isExpanded': [v.notNull, v.isBoolean] + notePosition: [v.notNull, v.isInteger], + prefix: [v.isString], + isExpanded: [v.notNull, v.isBoolean] }; - eu.route(router, 'patch', '/etapi/branches/:branchId', (req, res, next) => { + eu.route(router, "patch", "/etapi/branches/:branchId", (req, res, next) => { const branch = eu.getAndCheckBranch(req.params.branchId); eu.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH); @@ -63,7 +63,7 @@ function register(router: Router) { res.json(mappers.mapBranchToPojo(branch)); }); - eu.route(router, 'delete', '/etapi/branches/:branchId', (req, res, next) => { + eu.route(router, "delete", "/etapi/branches/:branchId", (req, res, next) => { const branch = becca.getBranch(req.params.branchId); if (!branch) { @@ -75,7 +75,7 @@ function register(router: Router) { res.sendStatus(204); }); - eu.route(router, 'post', '/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => { + eu.route(router, "post", "/etapi/refresh-note-ordering/:parentNoteId", (req, res, next) => { eu.getAndCheckNote(req.params.parentNoteId); entityChangesService.putNoteReorderingEntityChange(req.params.parentNoteId, "etapi"); diff --git a/src/etapi/etapi-interface.ts b/src/etapi/etapi-interface.ts index b4e816991..0d866690a 100644 --- a/src/etapi/etapi-interface.ts +++ b/src/etapi/etapi-interface.ts @@ -1,3 +1,3 @@ -export type ValidatorFunc = (obj: unknown) => (string | undefined); +export type ValidatorFunc = (obj: unknown) => string | undefined; export type ValidatorMap = Record; diff --git a/src/etapi/etapi.openapi.yaml b/src/etapi/etapi.openapi.yaml index 97a8977de..eed39fd80 100644 --- a/src/etapi/etapi.openapi.yaml +++ b/src/etapi/etapi.openapi.yaml @@ -1,1168 +1,1168 @@ openapi: "3.0.3" info: - version: 1.0.0 - title: ETAPI - description: External Trilium API - contact: - name: zadam - email: zadam.apps@gmail.com - url: https://github.com/zadam/trilium - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.0 + title: ETAPI + description: External Trilium API + contact: + name: zadam + email: zadam.apps@gmail.com + url: https://github.com/zadam/trilium + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html servers: - - url: http://localhost:37740/etapi - - url: http://localhost:8080/etapi + - url: http://localhost:37740/etapi + - url: http://localhost:8080/etapi security: - - EtapiTokenAuth: [] - - EtapiBasicAuth: [] + - EtapiTokenAuth: [] + - EtapiBasicAuth: [] paths: - /create-note: - post: - description: Create a note and place it into the note tree - operationId: createNote - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateNoteDef' - responses: - '201': - description: note created - content: - application/json; charset=utf-8: + /create-note: + post: + description: Create a note and place it into the note tree + operationId: createNote + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateNoteDef" + responses: + "201": + description: note created + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/NoteWithBranch" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /notes: + get: + description: Search notes + operationId: searchNotes + parameters: + - name: search + in: query + required: true + description: search query string as described in https://triliumnext.github.io/Docs/Wiki/search.html + schema: + type: string + examples: + fulltext: + summary: Fulltext search for keywords (not exact match) + value: "towers tolkien" + fulltextExactMatch: + summary: Fulltext search for exact match (notice the double quotes) + value: '"Two Towers"' + fulltextWithLabel: + summary: Fulltext search for keyword AND matching label + value: "towers #book" + - name: fastSearch + in: query + required: false + description: enable fast search (fulltext doesn't look into content) + schema: + type: boolean + default: false + - name: includeArchivedNotes + in: query + required: false + description: search by default ignores archived notes. Set to 'true' to includes archived notes into search results. + schema: + type: boolean + default: false + - name: ancestorNoteId + in: query + required: false + description: search only in a subtree identified by the subtree noteId. By default whole tree is searched. + schema: + $ref: "#/components/schemas/EntityId" + - name: ancestorDepth + in: query + required: false + description: define how deep in the tree should the notes be searched + schema: + type: string + examples: + directChildren: + summary: depth of exactly 1 (direct children) to the ancestor (root if not set) + value: eq1 + grandGrandChildren: + summary: depth of exactly 3 to the ancestor (root if not set) + value: eq3 + lessThan4: + summary: depth less than 4 (so 1, 2, 3) to the ancestor (root if not set) + value: lt4 + greaterThan2: + summary: depth greater than 2 (so 3, 4, 5, 6...) to the ancestor (root if not set) + value: gt4 + - name: orderBy + in: query + required: false + description: name of the property/label to order search results by + schema: + type: string + example: + - title + - "#publicationDate" + - isProtected + - isArchived + - dateCreated + - dateModified + - utcDateCreated + - utcDateModified + - parentCount + - childrenCount + - attributeCount + - labelCount + - ownedLabelCount + - relationCount + - ownedRelationCount + - relationCountIncludingLinks + - ownedRelationCountIncludingLinks + - targetRelationCount + - targetRelationCountIncludingLinks + - contentSize + - contentAndAttachmentsSize + - contentAndAttachmentsAndRevisionsSize + - revisionCount + - name: orderDirection + in: query + required: false + description: order direction, ascending or descending + schema: + type: string + default: asc + enum: + - asc + - desc + - name: limit + in: query + required: false + description: limit the number of results you want to receive + schema: + type: integer + example: 10 + - name: debug + in: query + required: false + description: set to true to get debug information in the response (search query parsing) + schema: + type: boolean + default: false + responses: + "200": + description: search response + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/SearchResponse" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /notes/{noteId}: + parameters: + - name: noteId + in: path + required: true schema: - $ref: '#/components/schemas/NoteWithBranch' - default: - description: unexpected error - content: - application/json; charset=utf-8: + $ref: "#/components/schemas/EntityId" + get: + description: Returns a note identified by its ID + operationId: getNoteById + responses: + "200": + description: note response + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Note" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + patch: + description: patch a note identified by the noteId with changes in the body + operationId: patchNoteById + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Note" + responses: + "200": + description: note updated + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Note" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + delete: + description: deletes a single note based on the noteId supplied + operationId: deleteNoteById + responses: + "204": + description: note deleted + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /notes/{noteId}/content: + parameters: + - name: noteId + in: path + required: true schema: - $ref: '#/components/schemas/Error' - /notes: - get: - description: Search notes - operationId: searchNotes - parameters: - - name: search - in: query - required: true - description: search query string as described in https://triliumnext.github.io/Docs/Wiki/search.html - schema: - type: string - examples: - fulltext: - summary: Fulltext search for keywords (not exact match) - value: 'towers tolkien' - fulltextExactMatch: - summary: Fulltext search for exact match (notice the double quotes) - value: '"Two Towers"' - fulltextWithLabel: - summary: Fulltext search for keyword AND matching label - value: 'towers #book' - - name: fastSearch - in: query - required: false - description: enable fast search (fulltext doesn't look into content) - schema: - type: boolean - default: false - - name: includeArchivedNotes - in: query - required: false - description: search by default ignores archived notes. Set to 'true' to includes archived notes into search results. - schema: - type: boolean - default: false - - name: ancestorNoteId - in: query - required: false - description: search only in a subtree identified by the subtree noteId. By default whole tree is searched. - schema: - $ref: '#/components/schemas/EntityId' - - name: ancestorDepth - in: query - required: false - description: define how deep in the tree should the notes be searched - schema: - type: string - examples: - directChildren: - summary: depth of exactly 1 (direct children) to the ancestor (root if not set) - value: eq1 - grandGrandChildren: - summary: depth of exactly 3 to the ancestor (root if not set) - value: eq3 - lessThan4: - summary: depth less than 4 (so 1, 2, 3) to the ancestor (root if not set) - value: lt4 - greaterThan2: - summary: depth greater than 2 (so 3, 4, 5, 6...) to the ancestor (root if not set) - value: gt4 - - name: orderBy - in: query - required: false - description: name of the property/label to order search results by - schema: - type: string - example: - - title - - '#publicationDate' - - isProtected - - isArchived - - dateCreated - - dateModified - - utcDateCreated - - utcDateModified - - parentCount - - childrenCount - - attributeCount - - labelCount - - ownedLabelCount - - relationCount - - ownedRelationCount - - relationCountIncludingLinks - - ownedRelationCountIncludingLinks - - targetRelationCount - - targetRelationCountIncludingLinks - - contentSize - - contentAndAttachmentsSize - - contentAndAttachmentsAndRevisionsSize - - revisionCount - - name: orderDirection - in: query - required: false - description: order direction, ascending or descending - schema: - type: string - default: asc - enum: - - asc - - desc - - name: limit - in: query - required: false - description: limit the number of results you want to receive - schema: - type: integer - example: 10 - - name: debug - in: query - required: false - description: set to true to get debug information in the response (search query parsing) - schema: - type: boolean - default: false - responses: - '200': - description: search response - content: - application/json; charset=utf-8: + $ref: "#/components/schemas/EntityId" + get: + description: Returns note content identified by its ID + operationId: getNoteContent + responses: + "200": + description: note content response + content: + text/html: + schema: + type: string + put: + description: Updates note content identified by its ID + operationId: putNoteContentById + requestBody: + description: html content of note + required: true + content: + text/plain: + schema: + type: string + responses: + "204": + description: note content updated + /notes/{noteId}/export: + parameters: + - name: noteId + in: path + required: true schema: - $ref: '#/components/schemas/SearchResponse' - default: - description: unexpected error - content: - application/json; charset=utf-8: + $ref: "#/components/schemas/EntityId" + - name: format + in: query + required: false schema: - $ref: '#/components/schemas/Error' - /notes/{noteId}: - parameters: - - name: noteId - in: path - required: true - schema: - $ref: '#/components/schemas/EntityId' - get: - description: Returns a note identified by its ID - operationId: getNoteById - responses: - '200': - description: note response - content: - application/json; charset=utf-8: + enum: + - html + - markdown + default: html + get: + description: Exports ZIP file export of a given note subtree. To export whole document, use "root" for noteId + operationId: exportNoteSubtree + responses: + "200": + description: export ZIP file + content: + application/zip: + schema: + type: string + format: binary + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /notes/{noteId}/import: + parameters: + - name: noteId + in: path + required: true schema: - $ref: '#/components/schemas/Note' - default: - description: unexpected error - content: - application/json; charset=utf-8: + $ref: "#/components/schemas/EntityId" + post: + description: Imports ZIP file into a given note. + operationId: importZip + responses: + "201": + description: note created + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/NoteWithBranch" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /notes/{noteId}/revision: + parameters: + - name: noteId + in: path + required: true schema: - $ref: '#/components/schemas/Error' - patch: - description: patch a note identified by the noteId with changes in the body - operationId: patchNoteById - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Note' - responses: - '200': - description: note updated - content: - application/json; charset=utf-8: + $ref: "#/components/schemas/EntityId" + - name: format + in: query + required: false schema: - $ref: '#/components/schemas/Note' - default: - description: unexpected error - content: - application/json; charset=utf-8: + enum: + - html + - markdown + default: html + post: + description: Create a note revision for the given note + operationId: createRevision + responses: + "204": + description: revision has been created + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /branches: + post: + description: > + Create a branch (clone a note to a different location in the tree). + In case there is a branch between parent note and child note already, + then this will update the existing branch with prefix, notePosition and isExpanded. + operationId: postBranch + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Branch" + responses: + "200": + description: branch updated (branch between parent note and child note already existed) + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Branch" + "201": + description: branch created + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Branch" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /branches/{branchId}: + parameters: + - name: branchId + in: path + required: true schema: - $ref: '#/components/schemas/Error' - delete: - description: deletes a single note based on the noteId supplied - operationId: deleteNoteById - responses: - '204': - description: note deleted - default: - description: unexpected error - content: - application/json; charset=utf-8: + $ref: "#/components/schemas/EntityId" + get: + description: Returns a branch identified by its ID + operationId: getBranchById + responses: + "200": + description: branch response + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Branch" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + patch: + description: patch a branch identified by the branchId with changes in the body. Only prefix and notePosition can be updated. If you want to update other properties, you need to delete the old branch and create a new one. + operationId: patchBranchById + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Branch" + responses: + "200": + description: branch updated + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Branch" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + delete: + description: > + deletes a branch based on the branchId supplied. If this is the last branch of the (child) note, + then the note is deleted as well. + operationId: deleteBranchById + responses: + "204": + description: branch deleted + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /attachments: + post: + description: create an attachment + operationId: postAttachment + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateAttachment" + responses: + "201": + description: attachment created + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Attachment" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /attachments/{attachmentId}: + parameters: + - name: attachmentId + in: path + required: true schema: - $ref: '#/components/schemas/Error' - /notes/{noteId}/content: - parameters: - - name: noteId - in: path - required: true - schema: - $ref: '#/components/schemas/EntityId' - get: - description: Returns note content identified by its ID - operationId: getNoteContent - responses: - '200': - description: note content response - content: - text/html: + $ref: "#/components/schemas/EntityId" + get: + description: Returns an attachment identified by its ID + operationId: getAttachmentById + responses: + "200": + description: attachment response + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Attachment" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + patch: + description: patch an attachment identified by the attachmentId with changes in the body. Only role, mime, title, and position are patchable. + operationId: patchAttachmentById + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Attachment" + responses: + "200": + description: attribute updated + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Attachment" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + delete: + description: deletes an attachment based on the attachmentId supplied. + operationId: deleteAttachmentById + responses: + "204": + description: attachment deleted + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /attachments/{attachmentId}/content: + parameters: + - name: attachmentId + in: path + required: true schema: - type: string - put: - description: Updates note content identified by its ID - operationId: putNoteContentById - requestBody: - description: html content of note - required: true - content: - text/plain: - schema: - type: string - responses: - '204': - description: note content updated - /notes/{noteId}/export: - parameters: - - name: noteId - in: path - required: true - schema: - $ref: '#/components/schemas/EntityId' - - name: format - in: query - required: false - schema: - enum: - - html - - markdown - default: html - get: - description: Exports ZIP file export of a given note subtree. To export whole document, use "root" for noteId - operationId: exportNoteSubtree - responses: - '200': - description: export ZIP file - content: - application/zip: + $ref: "#/components/schemas/EntityId" + get: + description: Returns attachment content identified by its ID + operationId: getAttachmentContent + responses: + "200": + description: attachment content response + content: + text/html: + schema: + type: string + put: + description: Updates attachment content identified by its ID + operationId: putAttachmentContentById + requestBody: + description: html content of attachment + required: true + content: + text/plain: + schema: + type: string + responses: + "204": + description: attachment content updated + /attributes: + post: + description: create an attribute for a given note + operationId: postAttribute + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Attribute" + responses: + "201": + description: attribute created + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Attribute" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /attributes/{attributeId}: + parameters: + - name: attributeId + in: path + required: true schema: - type: string - format: binary - default: - description: unexpected error - content: - application/json; charset=utf-8: + $ref: "#/components/schemas/EntityId" + get: + description: Returns an attribute identified by its ID + operationId: getAttributeById + responses: + "200": + description: attribute response + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Attribute" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + patch: + description: patch an attribute identified by the attributeId with changes in the body. For labels, only value and position can be updated. For relations, only position can be updated. If you want to modify other properties, you need to delete the old attribute and create a new one. + operationId: patchAttributeById + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Attribute" + responses: + "200": + description: attribute updated + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Attribute" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + delete: + description: deletes an attribute based on the attributeId supplied. + operationId: deleteAttributeById + responses: + "204": + description: attribute deleted + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /refresh-note-ordering/{parentNoteId}: + parameters: + - name: parentNoteId + in: path + required: true schema: - $ref: '#/components/schemas/Error' - /notes/{noteId}/import: - parameters: - - name: noteId - in: path - required: true - schema: - $ref: '#/components/schemas/EntityId' - post: - description: Imports ZIP file into a given note. - operationId: importZip - responses: - '201': - description: note created - content: - application/json; charset=utf-8: + $ref: "#/components/schemas/EntityId" + post: + description: > + notePositions in branches are not automatically pushed to connected clients and need a specific instruction. + If you want your changes to be in effect immediately, call this service after setting branches' notePosition. + Note that you need to supply "parentNoteId" of branch(es) with changed positions. + operationId: postRefreshNoteOrdering + responses: + "204": + description: note ordering will be asynchronously updated in all connected clients + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /inbox/{date}: + get: + description: > + returns an "inbox" note, into which note can be created. Date will be used depending on whether the inbox + is a fixed note (identified with #inbox label) or a day note in a journal. + operationId: getInboxNote + parameters: + - name: date + in: path + required: true + schema: + type: string + format: date + example: 2022-02-22 + responses: + "200": + description: inbox note + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Note" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /calendar/days/{date}: + get: + description: returns a day note for a given date. Gets created if doesn't exist. + operationId: getDayNote + parameters: + - name: date + in: path + required: true + schema: + type: string + format: date + example: 2022-02-22 + responses: + "200": + description: day note + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Note" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /calendar/weeks/{date}: + get: + description: returns a week note for a given date. Gets created if doesn't exist. + operationId: getWeekNote + parameters: + - name: date + in: path + required: true + schema: + type: string + format: date + example: 2022-02-22 + responses: + "200": + description: week note + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Note" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /calendar/months/{month}: + get: + description: returns a week note for a given date. Gets created if doesn't exist. + operationId: getMonthNote + parameters: + - name: month + in: path + required: true + schema: + type: string + pattern: "[0-9]{4}-[0-9]{2}" + example: 2022-02 + responses: + "200": + description: month note + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Note" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /calendar/years/{year}: + get: + description: returns a week note for a given date. Gets created if doesn't exist. + operationId: getYearNote + parameters: + - name: year + in: path + required: true + schema: + type: string + pattern: "[0-9]{4}-[0-9]{2}" + example: 2022-02 + responses: + "200": + description: year note + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Note" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /auth/login: + post: + description: get an ETAPI token based on password for further use with ETAPI + operationId: login + security: [] # no token based auth for login endpoint + requestBody: + required: true + content: + application/json: + schema: + properties: + password: + type: string + description: user's password used to e.g. login to Trilium server and/or protect notes + responses: + "201": + description: auth token + content: + application/json; charset=utf-8: + schema: + properties: + authToken: + type: string + example: Bc4bFn0Ffiok_4NpbVCDnFz7B2WU+pdhW8B5Ne3DiR5wXrEyqdjgRIsk= + "429": + description: Client IP has been blacklisted because too many requests (possibly failed authentications) were made within a short time frame, try again later + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /auth/logout: + post: + description: logout (delete/deactivate) an ETAPI token + operationId: logout + responses: + "204": + description: logout successful + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /app-info: + get: + description: returns information about the running Trilium instance + operationId: getAppInfo + responses: + "200": + description: app info + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/AppInfo" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" + /backup/{backupName}: + parameters: + - name: backupName + in: path + required: true + description: If the backupName is e.g. "now", then the backup will be written to "backup-now.db" file schema: - $ref: '#/components/schemas/NoteWithBranch' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /notes/{noteId}/revision: - parameters: - - name: noteId - in: path - required: true - schema: - $ref: '#/components/schemas/EntityId' - - name: format - in: query - required: false - schema: - enum: - - html - - markdown - default: html - post: - description: Create a note revision for the given note - operationId: createRevision - responses: - '204': - description: revision has been created - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /branches: - post: - description: > - Create a branch (clone a note to a different location in the tree). - In case there is a branch between parent note and child note already, - then this will update the existing branch with prefix, notePosition and isExpanded. - operationId: postBranch - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Branch' - responses: - '200': - description: branch updated (branch between parent note and child note already existed) - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Branch' - '201': - description: branch created - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Branch' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /branches/{branchId}: - parameters: - - name: branchId - in: path - required: true - schema: - $ref: '#/components/schemas/EntityId' - get: - description: Returns a branch identified by its ID - operationId: getBranchById - responses: - '200': - description: branch response - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Branch' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - patch: - description: patch a branch identified by the branchId with changes in the body. Only prefix and notePosition can be updated. If you want to update other properties, you need to delete the old branch and create a new one. - operationId: patchBranchById - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Branch' - responses: - '200': - description: branch updated - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Branch' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - delete: - description: > - deletes a branch based on the branchId supplied. If this is the last branch of the (child) note, - then the note is deleted as well. - operationId: deleteBranchById - responses: - '204': - description: branch deleted - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /attachments: - post: - description: create an attachment - operationId: postAttachment - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateAttachment' - responses: - '201': - description: attachment created - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Attachment' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /attachments/{attachmentId}: - parameters: - - name: attachmentId - in: path - required: true - schema: - $ref: '#/components/schemas/EntityId' - get: - description: Returns an attachment identified by its ID - operationId: getAttachmentById - responses: - '200': - description: attachment response - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Attachment' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - patch: - description: patch an attachment identified by the attachmentId with changes in the body. Only role, mime, title, and position are patchable. - operationId: patchAttachmentById - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Attachment' - responses: - '200': - description: attribute updated - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Attachment' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - delete: - description: deletes an attachment based on the attachmentId supplied. - operationId: deleteAttachmentById - responses: - '204': - description: attachment deleted - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /attachments/{attachmentId}/content: - parameters: - - name: attachmentId - in: path - required: true - schema: - $ref: '#/components/schemas/EntityId' - get: - description: Returns attachment content identified by its ID - operationId: getAttachmentContent - responses: - '200': - description: attachment content response - content: - text/html: - schema: - type: string - put: - description: Updates attachment content identified by its ID - operationId: putAttachmentContentById - requestBody: - description: html content of attachment - required: true - content: - text/plain: - schema: - type: string - responses: - '204': - description: attachment content updated - /attributes: - post: - description: create an attribute for a given note - operationId: postAttribute - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Attribute' - responses: - '201': - description: attribute created - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Attribute' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /attributes/{attributeId}: - parameters: - - name: attributeId - in: path - required: true - schema: - $ref: '#/components/schemas/EntityId' - get: - description: Returns an attribute identified by its ID - operationId: getAttributeById - responses: - '200': - description: attribute response - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Attribute' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - patch: - description: patch an attribute identified by the attributeId with changes in the body. For labels, only value and position can be updated. For relations, only position can be updated. If you want to modify other properties, you need to delete the old attribute and create a new one. - operationId: patchAttributeById - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Attribute' - responses: - '200': - description: attribute updated - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Attribute' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - delete: - description: deletes an attribute based on the attributeId supplied. - operationId: deleteAttributeById - responses: - '204': - description: attribute deleted - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /refresh-note-ordering/{parentNoteId}: - parameters: - - name: parentNoteId - in: path - required: true - schema: - $ref: '#/components/schemas/EntityId' - post: - description: > - notePositions in branches are not automatically pushed to connected clients and need a specific instruction. - If you want your changes to be in effect immediately, call this service after setting branches' notePosition. - Note that you need to supply "parentNoteId" of branch(es) with changed positions. - operationId: postRefreshNoteOrdering - responses: - '204': - description: note ordering will be asynchronously updated in all connected clients - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /inbox/{date}: - get: - description: > - returns an "inbox" note, into which note can be created. Date will be used depending on whether the inbox - is a fixed note (identified with #inbox label) or a day note in a journal. - operationId: getInboxNote - parameters: - - name: date - in: path - required: true - schema: - type: string - format: date - example: 2022-02-22 - responses: - '200': - description: inbox note - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Note' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /calendar/days/{date}: - get: - description: returns a day note for a given date. Gets created if doesn't exist. - operationId: getDayNote - parameters: - - name: date - in: path - required: true - schema: - type: string - format: date - example: 2022-02-22 - responses: - '200': - description: day note - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Note' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /calendar/weeks/{date}: - get: - description: returns a week note for a given date. Gets created if doesn't exist. - operationId: getWeekNote - parameters: - - name: date - in: path - required: true - schema: - type: string - format: date - example: 2022-02-22 - responses: - '200': - description: week note - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Note' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /calendar/months/{month}: - get: - description: returns a week note for a given date. Gets created if doesn't exist. - operationId: getMonthNote - parameters: - - name: month - in: path - required: true - schema: - type: string - pattern: '[0-9]{4}-[0-9]{2}' - example: 2022-02 - responses: - '200': - description: month note - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Note' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /calendar/years/{year}: - get: - description: returns a week note for a given date. Gets created if doesn't exist. - operationId: getYearNote - parameters: - - name: year - in: path - required: true - schema: - type: string - pattern: '[0-9]{4}-[0-9]{2}' - example: 2022-02 - responses: - '200': - description: year note - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Note' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /auth/login: - post: - description: get an ETAPI token based on password for further use with ETAPI - operationId: login - security: [] # no token based auth for login endpoint - requestBody: - required: true - content: - application/json: - schema: - properties: - password: - type: string - description: user's password used to e.g. login to Trilium server and/or protect notes - responses: - '201': - description: auth token - content: - application/json; charset=utf-8: - schema: - properties: - authToken: - type: string - example: Bc4bFn0Ffiok_4NpbVCDnFz7B2WU+pdhW8B5Ne3DiR5wXrEyqdjgRIsk= - '429': - description: Client IP has been blacklisted because too many requests (possibly failed authentications) were made within a short time frame, try again later - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /auth/logout: - post: - description: logout (delete/deactivate) an ETAPI token - operationId: logout - responses: - '204': - description: logout successful - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /app-info: - get: - description: returns information about the running Trilium instance - operationId: getAppInfo - responses: - '200': - description: app info - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/AppInfo' - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' - /backup/{backupName}: - parameters: - - name: backupName - in: path - required: true - description: If the backupName is e.g. "now", then the backup will be written to "backup-now.db" file - schema: - $ref: '#/components/schemas/StringId' - put: - description: Create a database backup under a given name - operationId: createBackup - responses: - '204': - description: backup has been created - default: - description: unexpected error - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/StringId" + put: + description: Create a database backup under a given name + operationId: createBackup + responses: + "204": + description: backup has been created + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" components: - securitySchemes: - EtapiTokenAuth: - type: apiKey - in: header - name: Authorization - EtapiBasicAuth: - type: http - scheme: basic - description: > - Basic Auth where username is arbitrary string (e.g. "trilium", not checked), - username is the ETAPI token. - To emphasize, do not use Trilium password here (won't work), only the generated - ETAPI token (from Options -> ETAPI) - schemas: - CreateNoteDef: - type: object - required: - - parentNoteId - - title - - type - - content - properties: - parentNoteId: - $ref: '#/components/schemas/EntityId' - description: Note ID of the parent note in the tree - title: - type: string - type: - type: string - enum: - - text - - code - - file - - image - - search - - book - - relationMap - - render - mime: - type: string - description: this needs to be specified only for note types 'code', 'file', 'image'. - example: application/json - content: - type: string - notePosition: - type: integer - description: > - Position of the note in the parent. Normal ordering is 10, 20, 30 ... - So if you want to create a note on the first position, use e.g. 5, for second position 15, for last e.g. 1000000 - prefix: - type: string - description: > - Prefix is branch (placement) specific title prefix for the note. - Let's say you have your note placed into two different places in the tree, - but you want to change the title a bit in one of the placements. For this you can use prefix. - isExpanded: - type: boolean - description: true if this note (as a folder) should appear expanded - noteId: - $ref: '#/components/schemas/EntityId' - description: DON'T specify unless you want to force a specific noteId - branchId: - $ref: '#/components/schemas/EntityId' - description: DON'T specify unless you want to force a specific branchId - dateCreated: - $ref: '#/components/schemas/LocalDateTime' - description: Local timestap of the note creation. Specify only if you want to override the default (current datetime in the current timezone/offset). - utcDateCreated: - $ref: '#/components/schemas/UtcDateTime' - description: UTC timestap of the note creation. Specify only if you want to override the default (current datetime). - Note: - type: object - properties: - noteId: - $ref: '#/components/schemas/EntityId' - readOnly: true - title: - type: string - type: - type: string - enum: [text, code, render, file, image, search, relationMap, book, noteMap, mermaid, webView, shortcut, doc, contentWidget, launcher] - mime: - type: string - isProtected: - type: boolean - readOnly: true - blobId: - type: string - description: ID of the blob object which effectively serves as a content hash - attributes: - $ref: '#/components/schemas/AttributeList' - readOnly: true - parentNoteIds: - $ref: '#/components/schemas/EntityIdList' - readOnly: true - childNoteIds: - $ref: '#/components/schemas/EntityIdList' - readOnly: true - parentBranchIds: - $ref: '#/components/schemas/EntityIdList' - readOnly: true - childBranchIds: - $ref: '#/components/schemas/EntityIdList' - readOnly: true - dateCreated: - $ref: '#/components/schemas/LocalDateTime' - dateModified: - $ref: '#/components/schemas/LocalDateTime' - readOnly: true - utcDateCreated: - $ref: '#/components/schemas/UtcDateTime' - utcDateModified: - $ref: '#/components/schemas/UtcDateTime' - readOnly: true - Branch: - type: object - description: Branch places the note into the tree, it represents the relationship between a parent note and child note - properties: - branchId: - $ref: '#/components/schemas/EntityId' - noteId: - $ref: '#/components/schemas/EntityId' - readOnly: true - description: identifies the child note - parentNoteId: - $ref: '#/components/schemas/EntityId' - readOnly: true - description: identifies the parent note - prefix: - type: string - notePosition: - type: integer - format: int32 - isExpanded: - type: boolean - utcDateModified: - $ref: '#/components/schemas/UtcDateTime' - readOnly: true - NoteWithBranch: - type: object - properties: - note: - $ref: '#/components/schemas/Note' - branch: - $ref: '#/components/schemas/Branch' - Attachment: - type: object - description: Attachment is owned by a note, has title and content - properties: - attachmentId: - $ref: '#/components/schemas/EntityId' - readOnly: true - ownerId: - $ref: '#/components/schemas/EntityId' - description: identifies the owner of the attachment, is either noteId or revisionId - role: - type: string - mime: - type: string - title: - type: string - position: - type: integer - format: int32 - blobId: - type: string - description: ID of the blob object which effectively serves as a content hash - dateModified: - $ref: '#/components/schemas/LocalDateTime' - readOnly: true - utcDateModified: - $ref: '#/components/schemas/UtcDateTime' - readOnly: true - utcDateScheduledForErasureSince: - $ref: '#/components/schemas/UtcDateTime' - readOnly: true - contentLength: - type: integer - format: int32 - CreateAttachment: - type: object - properties: - ownerId: - $ref: '#/components/schemas/EntityId' - description: identifies the owner of the attachment, is either noteId or revisionId - role: - type: string - mime: - type: string - title: - type: string - content: - type: string - position: - type: integer - format: int32 - Attribute: - type: object - description: Attribute (Label, Relation) is a key-value record attached to a note. - properties: - attributeId: - $ref: '#/components/schemas/EntityId' - noteId: - $ref: '#/components/schemas/EntityId' - readOnly: true - description: identifies the child note - type: - type: string - enum: [label, relation] - name: - type: string - pattern: '^[^\s]+' - example: shareCss - value: - type: string - position: - type: integer - format: int32 - isInheritable: - type: boolean - utcDateModified: - $ref: '#/components/schemas/UtcDateTime' - readOnly: true - AttributeList: - type: array - items: - $ref: '#/components/schemas/Attribute' - SearchResponse: - type: object - required: - - results - properties: - results: - type: array - items: - $ref: '#/components/schemas/Note' - debugInfo: - type: object - description: debugging info on parsing the search query enabled with &debug=true parameter - EntityId: - type: string - pattern: '[a-zA-Z0-9_]{4,32}' - example: evnnmvHTCgIn - StringId: - type: string - pattern: '[a-zA-Z0-9_]{1,32}' - example: my_ID - EntityIdList: - type: array - items: - $ref: '#/components/schemas/EntityId' - LocalDateTime: - type: string - pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}[\+\-][0-9]{4}' - example: 2021-12-31 20:18:11.930+0100 - UtcDateTime: - type: string - pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z' - example: 2021-12-31 19:18:11.930Z - AppInfo: - type: object - properties: - appVersion: - type: string - description: Trilium version - example: 0.50.2 - dbVersion: - type: integer - format: int32 - description: DB version - example: 194 - syncVersion: - type: integer - format: int32 - description: Sync protocol version - example: 25 - buildDate: - type: string - format: date-time - description: build date - example: 2022-02-09T22:52:36+01:00 - buildRevision: - type: string - description: git build revision - example: 23daaa2387a0655685377f0a541d154aeec2aae8 - dataDirectory: - type: string - description: data directory where Trilium stores files - example: /home/user/data - clipperProtocolVersion: - type: string - description: version of the supported Trilium Web Clipper protocol - example: 1.0 - utcDateTime: - type: string - description: current UTC date time - example: 2022-03-07T21:54:25.277Z - Error: - type: object - required: - - status - - code - - message - properties: - status: - type: integer - format: int32 - description: HTTP status, identical to the one given in HTTP response - example: 400 - code: - type: string - description: stable string constant - example: NOTE_IS_PROTECTED - message: - type: string - description: Human readable error, potentially with more details, - example: Note 'evnnmvHTCgIn' is protected and cannot be modified through ETAPI + securitySchemes: + EtapiTokenAuth: + type: apiKey + in: header + name: Authorization + EtapiBasicAuth: + type: http + scheme: basic + description: > + Basic Auth where username is arbitrary string (e.g. "trilium", not checked), + username is the ETAPI token. + To emphasize, do not use Trilium password here (won't work), only the generated + ETAPI token (from Options -> ETAPI) + schemas: + CreateNoteDef: + type: object + required: + - parentNoteId + - title + - type + - content + properties: + parentNoteId: + $ref: "#/components/schemas/EntityId" + description: Note ID of the parent note in the tree + title: + type: string + type: + type: string + enum: + - text + - code + - file + - image + - search + - book + - relationMap + - render + mime: + type: string + description: this needs to be specified only for note types 'code', 'file', 'image'. + example: application/json + content: + type: string + notePosition: + type: integer + description: > + Position of the note in the parent. Normal ordering is 10, 20, 30 ... + So if you want to create a note on the first position, use e.g. 5, for second position 15, for last e.g. 1000000 + prefix: + type: string + description: > + Prefix is branch (placement) specific title prefix for the note. + Let's say you have your note placed into two different places in the tree, + but you want to change the title a bit in one of the placements. For this you can use prefix. + isExpanded: + type: boolean + description: true if this note (as a folder) should appear expanded + noteId: + $ref: "#/components/schemas/EntityId" + description: DON'T specify unless you want to force a specific noteId + branchId: + $ref: "#/components/schemas/EntityId" + description: DON'T specify unless you want to force a specific branchId + dateCreated: + $ref: "#/components/schemas/LocalDateTime" + description: Local timestap of the note creation. Specify only if you want to override the default (current datetime in the current timezone/offset). + utcDateCreated: + $ref: "#/components/schemas/UtcDateTime" + description: UTC timestap of the note creation. Specify only if you want to override the default (current datetime). + Note: + type: object + properties: + noteId: + $ref: "#/components/schemas/EntityId" + readOnly: true + title: + type: string + type: + type: string + enum: [text, code, render, file, image, search, relationMap, book, noteMap, mermaid, webView, shortcut, doc, contentWidget, launcher] + mime: + type: string + isProtected: + type: boolean + readOnly: true + blobId: + type: string + description: ID of the blob object which effectively serves as a content hash + attributes: + $ref: "#/components/schemas/AttributeList" + readOnly: true + parentNoteIds: + $ref: "#/components/schemas/EntityIdList" + readOnly: true + childNoteIds: + $ref: "#/components/schemas/EntityIdList" + readOnly: true + parentBranchIds: + $ref: "#/components/schemas/EntityIdList" + readOnly: true + childBranchIds: + $ref: "#/components/schemas/EntityIdList" + readOnly: true + dateCreated: + $ref: "#/components/schemas/LocalDateTime" + dateModified: + $ref: "#/components/schemas/LocalDateTime" + readOnly: true + utcDateCreated: + $ref: "#/components/schemas/UtcDateTime" + utcDateModified: + $ref: "#/components/schemas/UtcDateTime" + readOnly: true + Branch: + type: object + description: Branch places the note into the tree, it represents the relationship between a parent note and child note + properties: + branchId: + $ref: "#/components/schemas/EntityId" + noteId: + $ref: "#/components/schemas/EntityId" + readOnly: true + description: identifies the child note + parentNoteId: + $ref: "#/components/schemas/EntityId" + readOnly: true + description: identifies the parent note + prefix: + type: string + notePosition: + type: integer + format: int32 + isExpanded: + type: boolean + utcDateModified: + $ref: "#/components/schemas/UtcDateTime" + readOnly: true + NoteWithBranch: + type: object + properties: + note: + $ref: "#/components/schemas/Note" + branch: + $ref: "#/components/schemas/Branch" + Attachment: + type: object + description: Attachment is owned by a note, has title and content + properties: + attachmentId: + $ref: "#/components/schemas/EntityId" + readOnly: true + ownerId: + $ref: "#/components/schemas/EntityId" + description: identifies the owner of the attachment, is either noteId or revisionId + role: + type: string + mime: + type: string + title: + type: string + position: + type: integer + format: int32 + blobId: + type: string + description: ID of the blob object which effectively serves as a content hash + dateModified: + $ref: "#/components/schemas/LocalDateTime" + readOnly: true + utcDateModified: + $ref: "#/components/schemas/UtcDateTime" + readOnly: true + utcDateScheduledForErasureSince: + $ref: "#/components/schemas/UtcDateTime" + readOnly: true + contentLength: + type: integer + format: int32 + CreateAttachment: + type: object + properties: + ownerId: + $ref: "#/components/schemas/EntityId" + description: identifies the owner of the attachment, is either noteId or revisionId + role: + type: string + mime: + type: string + title: + type: string + content: + type: string + position: + type: integer + format: int32 + Attribute: + type: object + description: Attribute (Label, Relation) is a key-value record attached to a note. + properties: + attributeId: + $ref: "#/components/schemas/EntityId" + noteId: + $ref: "#/components/schemas/EntityId" + readOnly: true + description: identifies the child note + type: + type: string + enum: [label, relation] + name: + type: string + pattern: '^[^\s]+' + example: shareCss + value: + type: string + position: + type: integer + format: int32 + isInheritable: + type: boolean + utcDateModified: + $ref: "#/components/schemas/UtcDateTime" + readOnly: true + AttributeList: + type: array + items: + $ref: "#/components/schemas/Attribute" + SearchResponse: + type: object + required: + - results + properties: + results: + type: array + items: + $ref: "#/components/schemas/Note" + debugInfo: + type: object + description: debugging info on parsing the search query enabled with &debug=true parameter + EntityId: + type: string + pattern: "[a-zA-Z0-9_]{4,32}" + example: evnnmvHTCgIn + StringId: + type: string + pattern: "[a-zA-Z0-9_]{1,32}" + example: my_ID + EntityIdList: + type: array + items: + $ref: "#/components/schemas/EntityId" + LocalDateTime: + type: string + pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}[\+\-][0-9]{4}' + example: 2021-12-31 20:18:11.930+0100 + UtcDateTime: + type: string + pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z' + example: 2021-12-31 19:18:11.930Z + AppInfo: + type: object + properties: + appVersion: + type: string + description: Trilium version + example: 0.50.2 + dbVersion: + type: integer + format: int32 + description: DB version + example: 194 + syncVersion: + type: integer + format: int32 + description: Sync protocol version + example: 25 + buildDate: + type: string + format: date-time + description: build date + example: 2022-02-09T22:52:36+01:00 + buildRevision: + type: string + description: git build revision + example: 23daaa2387a0655685377f0a541d154aeec2aae8 + dataDirectory: + type: string + description: data directory where Trilium stores files + example: /home/user/data + clipperProtocolVersion: + type: string + description: version of the supported Trilium Web Clipper protocol + example: 1.0 + utcDateTime: + type: string + description: current UTC date time + example: 2022-03-07T21:54:25.277Z + Error: + type: object + required: + - status + - code + - message + properties: + status: + type: integer + format: int32 + description: HTTP status, identical to the one given in HTTP response + example: 400 + code: + type: string + description: stable string constant + example: NOTE_IS_PROTECTED + message: + type: string + description: Human readable error, potentially with more details, + example: Note 'evnnmvHTCgIn' is protected and cannot be modified through ETAPI diff --git a/src/etapi/etapi_utils.ts b/src/etapi/etapi_utils.ts index 435de51d2..ae6e310d7 100644 --- a/src/etapi/etapi_utils.ts +++ b/src/etapi/etapi_utils.ts @@ -4,8 +4,8 @@ import log from "../services/log.js"; import becca from "../becca/becca.js"; import etapiTokenService from "../services/etapi_tokens.js"; import config from "../services/config.js"; -import { NextFunction, Request, RequestHandler, Response, Router } from 'express'; -import { ValidatorMap } from './etapi-interface.js'; +import { NextFunction, Request, RequestHandler, Response, Router } from "express"; +import { ValidatorMap } from "./etapi-interface.js"; import { ApiRequestHandler } from "../routes/routes.js"; const GENERIC_CODE = "GENERIC"; @@ -30,20 +30,21 @@ class EtapiError extends Error { function sendError(res: Response, statusCode: number, code: string, message: string) { return res - .set('Content-Type', 'application/json') + .set("Content-Type", "application/json") .status(statusCode) - .send(JSON.stringify({ - "status": statusCode, - "code": code, - "message": message - })); + .send( + JSON.stringify({ + status: statusCode, + code: code, + message: message + }) + ); } function checkEtapiAuth(req: Request, res: Response, next: NextFunction) { if (noAuthentication || etapiTokenService.isValidAuthHeader(req.headers.authorization)) { next(); - } - else { + } else { sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated"); } } @@ -54,8 +55,8 @@ function processRequest(req: Request, res: Response, routeHandler: ApiRequestHan cls.namespace.bindEmitter(res); cls.init(() => { - cls.set('componentId', "etapi"); - cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']); + cls.set("componentId", "etapi"); + cls.set("localNowDateTime", req.headers["trilium-local-now-datetime"]); const cb = () => routeHandler(req, res, next); @@ -85,19 +86,17 @@ function getAndCheckNote(noteId: string) { if (note) { return note; - } - else { + } else { throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`); } } function getAndCheckAttachment(attachmentId: string) { - const attachment = becca.getAttachment(attachmentId, {includeContentLength: true}); + const attachment = becca.getAttachment(attachmentId, { includeContentLength: true }); if (attachment) { return attachment; - } - else { + } else { throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`); } } @@ -107,8 +106,7 @@ function getAndCheckBranch(branchId: string) { if (branch) { return branch; - } - else { + } else { throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`); } } @@ -118,8 +116,7 @@ function getAndCheckAttribute(attributeId: string) { if (attribute) { return attribute; - } - else { + } else { throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`); } } @@ -128,8 +125,7 @@ function validateAndPatch(target: any, source: any, allowedProperties: Validator for (const key of Object.keys(source)) { if (!(key in allowedProperties)) { throw new EtapiError(400, "PROPERTY_NOT_ALLOWED", `Property '${key}' is not allowed for this method.`); - } - else { + } else { for (const validator of allowedProperties[key]) { const validationResult = validator(source[key]); @@ -157,4 +153,4 @@ export default { getAndCheckBranch, getAndCheckAttribute, getAndCheckAttachment -} +}; diff --git a/src/etapi/mappers.ts b/src/etapi/mappers.ts index 1caf21cf4..33cd2c29f 100644 --- a/src/etapi/mappers.ts +++ b/src/etapi/mappers.ts @@ -15,11 +15,11 @@ function mapNoteToPojo(note: BNote) { dateModified: note.dateModified, utcDateCreated: note.utcDateCreated, utcDateModified: note.utcDateModified, - parentNoteIds: note.getParentNotes().map(p => p.noteId), - childNoteIds: note.getChildNotes().map(ch => ch.noteId), - parentBranchIds: note.getParentBranches().map(p => p.branchId), - childBranchIds: note.getChildBranches().map(ch => ch.branchId), - attributes: note.getAttributes().map(attr => mapAttributeToPojo(attr)) + parentNoteIds: note.getParentNotes().map((p) => p.noteId), + childNoteIds: note.getChildNotes().map((ch) => ch.noteId), + parentBranchIds: note.getParentBranches().map((p) => p.branchId), + childBranchIds: note.getChildBranches().map((ch) => ch.branchId), + attributes: note.getAttributes().map((attr) => mapAttributeToPojo(attr)) }; } diff --git a/src/etapi/notes.ts b/src/etapi/notes.ts index 173dd0ec8..855cb0c95 100644 --- a/src/etapi/notes.ts +++ b/src/etapi/notes.ts @@ -9,28 +9,28 @@ import searchService from "../services/search/services/search.js"; import SearchContext from "../services/search/search_context.js"; import zipExportService from "../services/export/zip.js"; import zipImportService from "../services/import/zip.js"; -import { Request, Router } from 'express'; -import { ParsedQs } from 'qs'; -import { NoteParams } from '../services/note-interface.js'; -import { SearchParams } from '../services/search/services/types.js'; -import { ValidatorMap } from './etapi-interface.js'; +import { Request, Router } from "express"; +import { ParsedQs } from "qs"; +import { NoteParams } from "../services/note-interface.js"; +import { SearchParams } from "../services/search/services/types.js"; +import { ValidatorMap } from "./etapi-interface.js"; function register(router: Router) { - eu.route(router, 'get', '/etapi/notes', (req, res, next) => { + eu.route(router, "get", "/etapi/notes", (req, res, next) => { const { search } = req.query; if (typeof search !== "string" || !search?.trim()) { - throw new eu.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory."); + throw new eu.EtapiError(400, "SEARCH_QUERY_PARAM_MANDATORY", "'search' query parameter is mandatory."); } const searchParams = parseSearchParams(req); const searchContext = new SearchContext(searchParams); const searchResults = searchService.findResultsWithQuery(search, searchContext); - const foundNotes = searchResults.map(sr => becca.notes[sr.noteId]); + const foundNotes = searchResults.map((sr) => becca.notes[sr.noteId]); const resp: any = { - results: foundNotes.map(note => mappers.mapNoteToPojo(note)), + results: foundNotes.map((note) => mappers.mapNoteToPojo(note)) }; if (searchContext.debugInfo) { @@ -40,27 +40,27 @@ function register(router: Router) { res.json(resp); }); - eu.route(router, 'get', '/etapi/notes/:noteId', (req, res, next) => { + eu.route(router, "get", "/etapi/notes/:noteId", (req, res, next) => { const note = eu.getAndCheckNote(req.params.noteId); res.json(mappers.mapNoteToPojo(note)); }); const ALLOWED_PROPERTIES_FOR_CREATE_NOTE: ValidatorMap = { - 'parentNoteId': [v.mandatory, v.notNull, v.isNoteId], - 'title': [v.mandatory, v.notNull, v.isString], - 'type': [v.mandatory, v.notNull, v.isNoteType], - 'mime': [v.notNull, v.isString], - 'content': [v.notNull, v.isString], - 'notePosition': [v.notNull, v.isInteger], - 'prefix': [v.notNull, v.isString], - 'isExpanded': [v.notNull, v.isBoolean], - 'noteId': [v.notNull, v.isValidEntityId], - 'dateCreated': [v.notNull, v.isString, v.isLocalDateTime], - 'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime] + parentNoteId: [v.mandatory, v.notNull, v.isNoteId], + title: [v.mandatory, v.notNull, v.isString], + type: [v.mandatory, v.notNull, v.isNoteType], + mime: [v.notNull, v.isString], + content: [v.notNull, v.isString], + notePosition: [v.notNull, v.isInteger], + prefix: [v.notNull, v.isString], + isExpanded: [v.notNull, v.isBoolean], + noteId: [v.notNull, v.isValidEntityId], + dateCreated: [v.notNull, v.isString, v.isLocalDateTime], + utcDateCreated: [v.notNull, v.isString, v.isUtcDateTime] }; - eu.route(router, 'post', '/etapi/create-note', (req, res, next) => { + eu.route(router, "post", "/etapi/create-note", (req, res, next) => { const _params = {}; eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE); const params = _params as NoteParams; @@ -72,21 +72,20 @@ function register(router: Router) { note: mappers.mapNoteToPojo(resp.note), branch: mappers.mapBranchToPojo(resp.branch) }); - } - catch (e: any) { + } catch (e: any) { return eu.sendError(res, 500, eu.GENERIC_CODE, e.message); } }); const ALLOWED_PROPERTIES_FOR_PATCH = { - 'title': [v.notNull, v.isString], - 'type': [v.notNull, v.isString], - 'mime': [v.notNull, v.isString], - 'dateCreated': [v.notNull, v.isString, v.isLocalDateTime], - 'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime] + title: [v.notNull, v.isString], + type: [v.notNull, v.isString], + mime: [v.notNull, v.isString], + dateCreated: [v.notNull, v.isString, v.isLocalDateTime], + utcDateCreated: [v.notNull, v.isString, v.isUtcDateTime] }; - eu.route(router, 'patch', '/etapi/notes/:noteId', (req, res, next) => { + eu.route(router, "patch", "/etapi/notes/:noteId", (req, res, next) => { const note = eu.getAndCheckNote(req.params.noteId); if (note.isProtected) { @@ -99,7 +98,7 @@ function register(router: Router) { res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, 'delete', '/etapi/notes/:noteId', (req, res, next) => { + eu.route(router, "delete", "/etapi/notes/:noteId", (req, res, next) => { const { noteId } = req.params; const note = becca.getNote(noteId); @@ -108,12 +107,12 @@ function register(router: Router) { return res.sendStatus(204); } - note.deleteNote(null, new TaskContext('no-progress-reporting')); + note.deleteNote(null, new TaskContext("no-progress-reporting")); res.sendStatus(204); }); - eu.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => { + eu.route(router, "get", "/etapi/notes/:noteId/content", (req, res, next) => { const note = eu.getAndCheckNote(req.params.noteId); if (note.isProtected) { @@ -122,15 +121,15 @@ function register(router: Router) { const filename = utils.formatDownloadTitle(note.title, note.type, note.mime); - res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); + res.setHeader("Content-Disposition", utils.getContentDisposition(filename)); res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - res.setHeader('Content-Type', note.mime); + res.setHeader("Content-Type", note.mime); res.send(note.getContent()); }); - eu.route(router, 'put', '/etapi/notes/:noteId/content', (req, res, next) => { + eu.route(router, "put", "/etapi/notes/:noteId/content", (req, res, next) => { const note = eu.getAndCheckNote(req.params.noteId); if (note.isProtected) { @@ -144,7 +143,7 @@ function register(router: Router) { return res.sendStatus(204); }); - eu.route(router, 'get', '/etapi/notes/:noteId/export', (req, res, next) => { + eu.route(router, "get", "/etapi/notes/:noteId/export", (req, res, next) => { const note = eu.getAndCheckNote(req.params.noteId); const format = req.query.format || "html"; @@ -152,7 +151,7 @@ function register(router: Router) { throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`); } - const taskContext = new TaskContext('no-progress-reporting'); + const taskContext = new TaskContext("no-progress-reporting"); // technically a branch is being exported (includes prefix), but it's such a minor difference yet usability pain // (e.g. branchIds are not seen in UI), that we export "note export" instead. @@ -161,19 +160,19 @@ function register(router: Router) { zipExportService.exportToZip(taskContext, branch, format as "html" | "markdown", res); }); - eu.route(router, 'post', '/etapi/notes/:noteId/import', (req, res, next) => { + eu.route(router, "post", "/etapi/notes/:noteId/import", (req, res, next) => { const note = eu.getAndCheckNote(req.params.noteId); - const taskContext = new TaskContext('no-progress-reporting'); + const taskContext = new TaskContext("no-progress-reporting"); - zipImportService.importZip(taskContext, req.body, note).then(importedNote => { + zipImportService.importZip(taskContext, req.body, note).then((importedNote) => { res.status(201).json({ note: mappers.mapNoteToPojo(importedNote), - branch: mappers.mapBranchToPojo(importedNote.getParentBranches()[0]), + branch: mappers.mapBranchToPojo(importedNote.getParentBranches()[0]) }); }); // we need better error handling here, async errors won't be properly processed. }); - eu.route(router, 'post', '/etapi/notes/:noteId/revision', (req, res, next) => { + eu.route(router, "post", "/etapi/notes/:noteId/revision", (req, res, next) => { const note = eu.getAndCheckNote(req.params.noteId); note.saveRevision(); @@ -181,27 +180,25 @@ function register(router: Router) { return res.sendStatus(204); }); - eu.route(router, 'get', '/etapi/notes/:noteId/attachments', (req, res, next) => { + eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => { const note = eu.getAndCheckNote(req.params.noteId); - const attachments = note.getAttachments({ includeContentLength: true }) + const attachments = note.getAttachments({ includeContentLength: true }); - res.json( - attachments.map(attachment => mappers.mapAttachmentToPojo(attachment)) - ); + res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment))); }); } function parseSearchParams(req: Request) { const rawSearchParams: SearchParams = { - fastSearch: parseBoolean(req.query, 'fastSearch'), - includeArchivedNotes: parseBoolean(req.query, 'includeArchivedNotes'), - ancestorNoteId: parseString(req.query['ancestorNoteId']), - ancestorDepth: parseString(req.query['ancestorDepth']), // e.g. "eq5" - orderBy: parseString(req.query['orderBy']), + fastSearch: parseBoolean(req.query, "fastSearch"), + includeArchivedNotes: parseBoolean(req.query, "includeArchivedNotes"), + ancestorNoteId: parseString(req.query["ancestorNoteId"]), + ancestorDepth: parseString(req.query["ancestorDepth"]), // e.g. "eq5" + orderBy: parseString(req.query["orderBy"]), // TODO: Check why the order direction was provided as a number, but it's a string everywhere else. - orderDirection: parseOrderDirection(req.query, 'orderDirection') as unknown as string, - limit: parseInteger(req.query, 'limit'), - debug: parseBoolean(req.query, 'debug') + orderDirection: parseOrderDirection(req.query, "orderDirection") as unknown as string, + limit: parseInteger(req.query, "limit"), + debug: parseBoolean(req.query, "debug") }; const searchParams: SearchParams = {}; @@ -230,11 +227,11 @@ function parseBoolean(obj: any, name: string) { return undefined; } - if (!['true', 'false'].includes(obj[name])) { + if (!["true", "false"].includes(obj[name])) { throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'.`); } - return obj[name] === 'true'; + return obj[name] === "true"; } function parseOrderDirection(obj: any, name: string) { @@ -244,7 +241,7 @@ function parseOrderDirection(obj: any, name: string) { const integer = parseInt(obj[name]); - if (!['asc', 'desc'].includes(obj[name])) { + if (!["asc", "desc"].includes(obj[name])) { throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'.`); } diff --git a/src/etapi/spec.ts b/src/etapi/spec.ts index 9c183e9c1..5e27be949 100644 --- a/src/etapi/spec.ts +++ b/src/etapi/spec.ts @@ -4,16 +4,16 @@ import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; -const specPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'etapi.openapi.yaml'); +const specPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "etapi.openapi.yaml"); let spec: string | null = null; function register(router: Router) { - router.get('/etapi/etapi.openapi.yaml', (req, res, next) => { + router.get("/etapi/etapi.openapi.yaml", (req, res, next) => { if (!spec) { - spec = fs.readFileSync(specPath, 'utf8'); + spec = fs.readFileSync(specPath, "utf8"); } - res.header('Content-Type', 'text/plain'); // so that it displays in browser + res.header("Content-Type", "text/plain"); // so that it displays in browser res.status(200).send(spec); }); } diff --git a/src/etapi/special_notes.ts b/src/etapi/special_notes.ts index 1df65c638..14626710d 100644 --- a/src/etapi/special_notes.ts +++ b/src/etapi/special_notes.ts @@ -2,10 +2,10 @@ import specialNotesService from "../services/special_notes.js"; import dateNotesService from "../services/date_notes.js"; import eu from "./etapi_utils.js"; import mappers from "./mappers.js"; -import { Router } from 'express'; +import { Router } from "express"; const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`); -const getMonthInvalidError = (month: string)=> new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`); +const getMonthInvalidError = (month: string) => new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`); const getYearInvalidError = (year: string) => new eu.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`); function isValidDate(date: string) { @@ -17,7 +17,7 @@ function isValidDate(date: string) { } function register(router: Router) { - eu.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => { + eu.route(router, "get", "/etapi/inbox/:date", (req, res, next) => { const { date } = req.params; if (!isValidDate(date)) { @@ -28,7 +28,7 @@ function register(router: Router) { res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, 'get', '/etapi/calendar/days/:date', (req, res, next) => { + eu.route(router, "get", "/etapi/calendar/days/:date", (req, res, next) => { const { date } = req.params; if (!isValidDate(date)) { @@ -39,7 +39,7 @@ function register(router: Router) { res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, 'get', '/etapi/calendar/weeks/:date', (req, res, next) => { + eu.route(router, "get", "/etapi/calendar/weeks/:date", (req, res, next) => { const { date } = req.params; if (!isValidDate(date)) { @@ -50,7 +50,7 @@ function register(router: Router) { res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, 'get', '/etapi/calendar/months/:month', (req, res, next) => { + eu.route(router, "get", "/etapi/calendar/months/:month", (req, res, next) => { const { month } = req.params; if (!/[0-9]{4}-[0-9]{2}/.test(month)) { @@ -61,7 +61,7 @@ function register(router: Router) { res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, 'get', '/etapi/calendar/years/:year', (req, res, next) => { + eu.route(router, "get", "/etapi/calendar/years/:year", (req, res, next) => { const { year } = req.params; if (!/[0-9]{4}/.test(year)) { diff --git a/src/etapi/validators.ts b/src/etapi/validators.ts index 37aafecef..52b24ddc4 100644 --- a/src/etapi/validators.ts +++ b/src/etapi/validators.ts @@ -19,7 +19,7 @@ function isString(obj: unknown) { return; } - if (typeof obj !== 'string') { + if (typeof obj !== "string") { return `'${obj}' is not a string`; } } @@ -45,7 +45,7 @@ function isBoolean(obj: unknown) { return; } - if (typeof obj !== 'boolean') { + if (typeof obj !== "boolean") { return `'${obj}' is not a boolean`; } } @@ -65,7 +65,7 @@ function isNoteId(obj: unknown) { return; } - if (typeof obj !== 'string') { + if (typeof obj !== "string") { return `'${obj}' is not a valid noteId`; } @@ -91,7 +91,7 @@ function isAttributeType(obj: unknown) { return; } - if (typeof obj !== "string" || !['label', 'relation'].includes(obj)) { + if (typeof obj !== "string" || !["label", "relation"].includes(obj)) { return `'${obj}' is not a valid attribute type, allowed types are: label, relation`; } } @@ -101,7 +101,7 @@ function isValidEntityId(obj: unknown) { return; } - if (typeof obj !== 'string' || !/^[A-Za-z0-9_]{4,128}$/.test(obj)) { + if (typeof obj !== "string" || !/^[A-Za-z0-9_]{4,128}$/.test(obj)) { return `'${obj}' is not a valid entityId. Only alphanumeric characters are allowed of length 4 to 32.`; } } diff --git a/src/express.d.ts b/src/express.d.ts index e7b6393f1..eb523dff2 100644 --- a/src/express.d.ts +++ b/src/express.d.ts @@ -4,18 +4,18 @@ export declare module "express-serve-static-core" { interface Request { session: Session & { loggedIn: boolean; - }, + }; headers: { "x-local-date"?: string; "x-labels"?: string; - "authorization"?: string; + authorization?: string; "trilium-cred"?: string; "x-csrf-token"?: string; "trilium-component-id"?: string; "trilium-local-now-datetime"?: string; "trilium-hoisted-note-id"?: string; - } + }; } } diff --git a/src/public/app/components/app_context.ts b/src/public/app/components/app_context.ts index ceaef3882..035eeb0f8 100644 --- a/src/public/app/components/app_context.ts +++ b/src/public/app/components/app_context.ts @@ -98,7 +98,7 @@ export type CommandMappings = { showInfoDialog: ConfirmWithMessageOptions; showConfirmDialog: ConfirmWithMessageOptions; openNewNoteSplit: NoteCommandData; - openInWindow: NoteCommandData, + openInWindow: NoteCommandData; openNoteInNewTab: CommandData; openNoteInNewSplit: CommandData; openNoteInNewWindow: CommandData; @@ -139,11 +139,12 @@ export type CommandMappings = { resetLauncher: ContextMenuCommandData; executeInActiveNoteDetailWidget: CommandData & { - callback: (value: NoteDetailWidget | PromiseLike) => void - }; - executeWithTextEditor: CommandData & ExecuteCommandData & { - callback?: GetTextEditorCallback; + callback: (value: NoteDetailWidget | PromiseLike) => void; }; + executeWithTextEditor: CommandData & + ExecuteCommandData & { + callback?: GetTextEditorCallback; + }; executeWithCodeEditor: CommandData & ExecuteCommandData; executeWithContentElement: CommandData & ExecuteCommandData; executeWithTypeWidget: CommandData & ExecuteCommandData; @@ -177,8 +178,8 @@ export type CommandMappings = { /** Sets the active {@link Screen} (e.g. to toggle the tree sidebar). It triggers the {@link EventMappings.activeScreenChanged} event, but only if the provided screen is different than the current one. */ setActiveScreen: CommandData & { screen: Screen; - } -} + }; +}; type EventMappings = { initialRenderComplete: {}; @@ -195,57 +196,57 @@ type EventMappings = { messages: string[]; }; entitiesReloaded: { - loadResults: LoadResults + loadResults: LoadResults; }; addNewLabel: CommandData; addNewRelation: CommandData; sqlQueryResults: CommandData & { results: SqlExecuteResults; - }, + }; readOnlyTemporarilyDisabled: { - noteContext: NoteContext - }, + noteContext: NoteContext; + }; /** Triggered when the {@link CommandMappings.setActiveScreen} command is invoked. */ activeScreenChanged: { activeScreen: Screen; - }, + }; activeContextChanged: { noteContext: NoteContext; - }, + }; noteSwitched: { noteContext: NoteContext; notePath: string; - }, + }; noteSwitchedAndActivatedEvent: { noteContext: NoteContext; notePath: string; - }, + }; setNoteContext: { noteContext: NoteContext; - }, + }; noteTypeMimeChangedEvent: { noteId: string; - }, + }; reEvaluateHighlightsListWidgetVisibility: { noteId: string | undefined; - }, + }; showHighlightsListWidget: { noteId: string; - } -} + }; +}; export type EventListener = { - [key in T as `${key}Event`]: (data: EventData) => void -} + [key in T as `${key}Event`]: (data: EventData) => void; +}; export type CommandListener = { - [key in T as `${key}Command`]: (data: CommandListenerData) => void -} + [key in T as `${key}Command`]: (data: CommandListenerData) => void; +}; export type CommandListenerData = CommandMappings[T]; export type EventData = EventMappings[T]; -type CommandAndEventMappings = (CommandMappings & EventMappings); +type CommandAndEventMappings = CommandMappings & EventMappings; /** * This type is a discriminated union which contains all the possible commands that can be triggered via {@link AppContext.triggerCommand}. @@ -253,7 +254,7 @@ type CommandAndEventMappings = (CommandMappings & EventMappings); export type CommandNames = keyof CommandMappings; type EventNames = keyof EventMappings; -type FilterByValueType = { [K in keyof T]: T[K] extends ValueType ? K : never; }[keyof T]; +type FilterByValueType = { [K in keyof T]: T[K] extends ValueType ? K : never }[keyof T]; /** * Generic which filters {@link CommandNames} to provide only those commands that take in as data the desired implementation of {@link CommandData}. Mostly useful for contextual menu, to enforce consistency in the commands. @@ -261,7 +262,6 @@ type FilterByValueType = { [K in keyof T]: T[K] extends ValueType export type FilteredCommandNames = keyof Pick>; class AppContext extends Component { - isMainWindow: boolean; components: Component[]; beforeUnloadListeners: WeakRef[]; @@ -304,13 +304,7 @@ class AppContext extends Component { initComponents() { this.tabManager = new TabManager(); - this.components = [ - this.tabManager, - new RootCommandExecutor(), - new Entrypoints(), - new MainTreeExecutors(), - new ShortcutComponent() - ]; + this.components = [this.tabManager, new RootCommandExecutor(), new Entrypoints(), new MainTreeExecutors(), new ShortcutComponent()]; if (utils.isMobile()) { this.components.push(new MobileScreenSwitcherExecutor()); @@ -337,21 +331,21 @@ class AppContext extends Component { $("body").append($renderedWidget); - $renderedWidget.on('click', "[data-trigger-command]", function() { + $renderedWidget.on("click", "[data-trigger-command]", function () { if ($(this).hasClass("disabled")) { return; } - const commandName = $(this).attr('data-trigger-command'); + const commandName = $(this).attr("data-trigger-command"); const $component = $(this).closest(".component"); const component = $component.prop("component"); - component.triggerCommand(commandName, {$el: $(this)}); + component.triggerCommand(commandName, { $el: $(this) }); }); this.child(rootWidget); - this.triggerEvent('initialRenderComplete'); + this.triggerEvent("initialRenderComplete"); } // TODO: Remove ignore once all commands are mapped out. @@ -378,7 +372,7 @@ class AppContext extends Component { } getComponentByEl(el: HTMLElement) { - return $(el).closest(".component").prop('component'); + return $(el).closest(".component").prop("component"); } addBeforeUnloadListener(obj: BeforeUploadListener) { @@ -394,10 +388,10 @@ class AppContext extends Component { const appContext = new AppContext(window.glob.isMainWindow); // we should save all outstanding changes before the page/app is closed -$(window).on('beforeunload', () => { +$(window).on("beforeunload", () => { let allSaved = true; - appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref()); + appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref()); for (const weakRef of appContext.beforeUnloadListeners) { const component = weakRef.deref(); @@ -420,8 +414,8 @@ $(window).on('beforeunload', () => { } }); -$(window).on('hashchange', function() { - const {notePath, ntxId, viewScope} = linkService.parseNavigationStateFromUrl(window.location.href); +$(window).on("hashchange", function () { + const { notePath, ntxId, viewScope } = linkService.parseNavigationStateFromUrl(window.location.href); if (notePath || ntxId) { appContext.tabManager.switchToNoteContext(ntxId, notePath, viewScope); diff --git a/src/public/app/components/component.ts b/src/public/app/components/component.ts index 9300b1f86..cb8f0d9bf 100644 --- a/src/public/app/components/component.ts +++ b/src/public/app/components/component.ts @@ -1,5 +1,5 @@ -import utils from '../services/utils.js'; -import { CommandMappings, CommandNames } from './app_context.js'; +import utils from "../services/utils.js"; +import { CommandMappings, CommandNames } from "./app_context.js"; /** * Abstract class for all components in the Trilium's frontend. @@ -28,7 +28,7 @@ export class TypedComponent> { get sanitizedClassName() { // webpack mangles names and sometimes uses unsafe characters - return this.constructor.name.replace(/[^A-Z0-9]/ig, "_"); + return this.constructor.name.replace(/[^A-Z0-9]/gi, "_"); } setParent(parent: TypedComponent) { @@ -48,18 +48,13 @@ export class TypedComponent> { handleEvent(name: string, data: unknown): Promise | null { try { - const callMethodPromise = this.initialized - ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) - : this.callMethod((this as any)[`${name}Event`], data); + const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data); const childrenPromise = this.handleEventInChildren(name, data); // don't create promises if not needed (optimization) - return callMethodPromise && childrenPromise - ? Promise.all([callMethodPromise, childrenPromise]) - : (callMethodPromise || childrenPromise); - } - catch (e: any) { + return callMethodPromise && childrenPromise ? Promise.all([callMethodPromise, childrenPromise]) : callMethodPromise || childrenPromise; + } catch (e: any) { console.error(`Handling of event '${name}' failed in ${this.constructor.name} with error ${e.message} ${e.stack}`); return null; @@ -101,7 +96,7 @@ export class TypedComponent> { } callMethod(fun: (arg: unknown) => Promise, data: unknown) { - if (typeof fun !== 'function') { + if (typeof fun !== "function") { return; } @@ -111,7 +106,8 @@ export class TypedComponent> { const took = Date.now() - startTime; - if (glob.isDev && took > 20) { // measuring only sync handlers + if (glob.isDev && took > 20) { + // measuring only sync handlers console.log(`Call to ${fun.name} in ${this.componentId} took ${took}ms`); } diff --git a/src/public/app/components/entrypoints.ts b/src/public/app/components/entrypoints.ts index 1aae586a3..571d554b2 100644 --- a/src/public/app/components/entrypoints.ts +++ b/src/public/app/components/entrypoints.ts @@ -1,6 +1,6 @@ import utils from "../services/utils.js"; import dateNoteService from "../services/date_notes.js"; -import protectedSessionHolder from '../services/protected_session_holder.js'; +import protectedSessionHolder from "../services/protected_session_holder.js"; import server from "../services/server.js"; import appContext, { NoteCommandData } from "./app_context.js"; import Component from "./component.js"; @@ -41,7 +41,7 @@ export default class Entrypoints extends Component { openDevToolsCommand() { if (utils.isElectron()) { - utils.dynamicRequire('@electron/remote').getCurrentWindow().toggleDevTools(); + utils.dynamicRequire("@electron/remote").getCurrentWindow().toggleDevTools(); } } @@ -52,20 +52,20 @@ export default class Entrypoints extends Component { return; } - const {note} = await server.post(`notes/${inboxNote.noteId}/children?target=into`, { - content: '', - type: 'text', + const { note } = await server.post(`notes/${inboxNote.noteId}/children?target=into`, { + content: "", + type: "text", isProtected: inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable() }); await ws.waitForMaxKnownEntityChangeId(); - await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, {activate: true}); + await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, { activate: true }); - appContext.triggerEvent('focusAndSelectTitle', {isNewNote: true}); + appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true }); } - async toggleNoteHoistingCommand({noteId = appContext.tabManager.getActiveContextNoteId()}) { + async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) { if (!noteId) { return; } @@ -75,12 +75,12 @@ export default class Entrypoints extends Component { if (noteToHoist?.noteId === activeNoteContext.hoistedNoteId) { await activeNoteContext.unhoist(); - } else if (noteToHoist?.type !== 'search') { + } else if (noteToHoist?.type !== "search") { await activeNoteContext.setHoistedNoteId(noteId); } } - async hoistNoteCommand({noteId}: { noteId: string }) { + async hoistNoteCommand({ noteId }: { noteId: string }) { const noteContext = appContext.tabManager.getActiveContext(); if (noteContext.hoistedNoteId !== noteId) { @@ -102,7 +102,7 @@ export default class Entrypoints extends Component { toggleFullscreenCommand() { if (utils.isElectron()) { - const win = utils.dynamicRequire('@electron/remote').getCurrentWindow(); + const win = utils.dynamicRequire("@electron/remote").getCurrentWindow(); if (win.isFullScreenable()) { win.setFullScreen(!win.isFullScreen()); @@ -115,22 +115,20 @@ export default class Entrypoints extends Component { } logoutCommand() { - const $logoutForm = $('
') - .append($(``)); + const $logoutForm = $('').append($(``)); $("body").append($logoutForm); - $logoutForm.trigger('submit'); + $logoutForm.trigger("submit"); } backInNoteHistoryCommand() { if (utils.isElectron()) { // standard JS version does not work completely correctly in electron - const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents(); + const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex()); webContents.goToIndex(activeIndex - 1); - } - else { + } else { window.history.back(); } } @@ -138,52 +136,50 @@ export default class Entrypoints extends Component { forwardInNoteHistoryCommand() { if (utils.isElectron()) { // standard JS version does not work completely correctly in electron - const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents(); + const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex()); webContents.goToIndex(activeIndex + 1); - } - else { + } else { window.history.forward(); } } async switchToDesktopVersionCommand() { - utils.setCookie('trilium-device', 'desktop'); + utils.setCookie("trilium-device", "desktop"); utils.reloadFrontendApp("Switching to desktop version"); } async switchToMobileVersionCommand() { - utils.setCookie('trilium-device', 'mobile'); + utils.setCookie("trilium-device", "mobile"); utils.reloadFrontendApp("Switching to mobile version"); } - async openInWindowCommand({notePath, hoistedNoteId, viewScope}: NoteCommandData) { - const extraWindowHash = linkService.calculateHash({notePath, hoistedNoteId, viewScope}); + async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) { + const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope }); if (utils.isElectron()) { - const {ipcRenderer} = utils.dynamicRequire('electron'); + const { ipcRenderer } = utils.dynamicRequire("electron"); - ipcRenderer.send('create-extra-window', { extraWindowHash }); - } - else { + ipcRenderer.send("create-extra-window", { extraWindowHash }); + } else { const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`; - window.open(url, '', 'width=1000,height=800'); + window.open(url, "", "width=1000,height=800"); } } async openNewWindowCommand() { - this.openInWindowCommand({notePath: '', hoistedNoteId: 'root'}); + this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" }); } async runActiveNoteCommand() { - const {ntxId, note} = appContext.tabManager.getActiveContext(); + const { ntxId, note } = appContext.tabManager.getActiveContext(); // ctrl+enter is also used elsewhere, so make sure we're running only when appropriate - if (!note || note.type !== 'code') { + if (!note || note.type !== "code") { return; } @@ -192,14 +188,14 @@ export default class Entrypoints extends Component { await bundleService.getAndExecuteBundle(note.noteId); } else if (note.mime.endsWith("env=backend")) { await server.post(`script/run/${note.noteId}`); - } else if (note.mime === 'text/x-sqlite;schema=trilium') { + } else if (note.mime === "text/x-sqlite;schema=trilium") { const resp = await server.post(`sql/execute/${note.noteId}`); if (!resp.success) { toastService.showError(t("entrypoints.sql-error", { message: resp.error })); } - await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: resp.results}); + await appContext.triggerEvent("sqlQueryResults", { ntxId: ntxId, results: resp.results }); } toastService.showMessage(t("entrypoints.note-executed")); diff --git a/src/public/app/components/main_tree_executors.ts b/src/public/app/components/main_tree_executors.ts index add809a08..78251d67c 100644 --- a/src/public/app/components/main_tree_executors.ts +++ b/src/public/app/components/main_tree_executors.ts @@ -19,9 +19,9 @@ export default class MainTreeExecutors extends Component { return; } - const selectedOrActiveNoteIds = this.tree.getSelectedOrActiveNodes().map(node => node.data.noteId); + const selectedOrActiveNoteIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.noteId); - this.triggerCommand('cloneNoteIdsTo', {noteIds: selectedOrActiveNoteIds}); + this.triggerCommand("cloneNoteIdsTo", { noteIds: selectedOrActiveNoteIds }); } async moveNotesToCommand() { @@ -29,9 +29,9 @@ export default class MainTreeExecutors extends Component { return; } - const selectedOrActiveBranchIds = this.tree.getSelectedOrActiveNodes().map(node => node.data.branchId); + const selectedOrActiveBranchIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.branchId); - this.triggerCommand('moveBranchIdsTo', {branchIds: selectedOrActiveBranchIds}); + this.triggerCommand("moveBranchIdsTo", { branchIds: selectedOrActiveBranchIds }); } async createNoteIntoCommand() { @@ -61,12 +61,12 @@ export default class MainTreeExecutors extends Component { const parentNotePath = treeService.getNotePath(node.getParent()); const isProtected = treeService.getParentProtectedStatus(node); - if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) { + if (node.data.noteId === "root" || node.data.noteId === hoistedNoteService.getHoistedNoteId()) { return; } await noteCreateService.createNote(parentNotePath, { - target: 'after', + target: "after", targetBranchId: node.data.branchId, isProtected: isProtected, saveSelection: false diff --git a/src/public/app/components/mobile_screen_switcher.ts b/src/public/app/components/mobile_screen_switcher.ts index 217bbc3fe..a11ed1ffc 100644 --- a/src/public/app/components/mobile_screen_switcher.ts +++ b/src/public/app/components/mobile_screen_switcher.ts @@ -3,15 +3,13 @@ import type { CommandListener, CommandListenerData } from "./app_context.js"; export type Screen = "detail" | "tree"; -export default class MobileScreenSwitcherExecutor extends Component - implements CommandListener<"setActiveScreen"> -{ +export default class MobileScreenSwitcherExecutor extends Component implements CommandListener<"setActiveScreen"> { private activeScreen?: Screen; - setActiveScreenCommand({screen}: CommandListenerData<"setActiveScreen">) { + setActiveScreenCommand({ screen }: CommandListenerData<"setActiveScreen">) { if (screen !== this.activeScreen) { this.activeScreen = screen; - this.triggerEvent('activeScreenChanged', {activeScreen: screen}); + this.triggerEvent("activeScreenChanged", { activeScreen: screen }); } } } diff --git a/src/public/app/components/note_context.ts b/src/public/app/components/note_context.ts index b18ec7e7a..7af3f94af 100644 --- a/src/public/app/components/note_context.ts +++ b/src/public/app/components/note_context.ts @@ -17,10 +17,7 @@ interface SetNoteOpts { export type GetTextEditorCallback = () => void; -class NoteContext extends Component - implements EventListener<"entitiesReloaded"> -{ - +class NoteContext extends Component implements EventListener<"entitiesReloaded"> { ntxId: string | null; hoistedNoteId: string; private mainNtxId: string | null; @@ -30,7 +27,7 @@ class NoteContext extends Component private parentNoteId?: string | null; viewScope?: ViewScope; - constructor(ntxId: string | null = null, hoistedNoteId: string = 'root', mainNtxId: string | null = null) { + constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) { super(); this.ntxId = ntxId || NoteContext.generateNtxId(); @@ -50,7 +47,7 @@ class NoteContext extends Component this.parentNoteId = null; // hoisted note is kept intentionally - this.triggerEvent('noteSwitched', { + this.triggerEvent("noteSwitched", { noteContext: this, notePath: this.notePath }); @@ -81,20 +78,20 @@ class NoteContext extends Component return; } - await this.triggerEvent('beforeNoteSwitch', {noteContext: this}); + await this.triggerEvent("beforeNoteSwitch", { noteContext: this }); utils.closeActiveDialog(); this.notePath = resolvedNotePath; this.viewScope = opts.viewScope; - ({noteId: this.noteId, parentNoteId: this.parentNoteId} = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath)); + ({ noteId: this.noteId, parentNoteId: this.parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath)); this.saveToRecentNotes(resolvedNotePath); protectedSessionHolder.touchProtectedSessionIfNecessary(this.note); if (opts.triggerSwitchEvent) { - await this.triggerEvent('noteSwitched', { + await this.triggerEvent("noteSwitched", { noteContext: this, notePath: this.notePath }); @@ -103,23 +100,20 @@ class NoteContext extends Component await this.setHoistedNoteIfNeeded(); if (utils.isMobile()) { - this.triggerCommand('setActiveScreen', {screen: 'detail'}); + this.triggerCommand("setActiveScreen", { screen: "detail" }); } } async setHoistedNoteIfNeeded() { - if (this.hoistedNoteId === 'root' - && this.notePath?.startsWith("root/_hidden") - && !this.note?.isLabelTruthy("keepCurrentHoisting") - ) { + if (this.hoistedNoteId === "root" && this.notePath?.startsWith("root/_hidden") && !this.note?.isLabelTruthy("keepCurrentHoisting")) { // hidden subtree displays only when hoisted, so it doesn't make sense to keep root as hoisted note - let hoistedNoteId = '_hidden'; + let hoistedNoteId = "_hidden"; if (this.note?.isLaunchBarConfig()) { - hoistedNoteId = '_lbRoot'; + hoistedNoteId = "_lbRoot"; } else if (this.note?.isOptions()) { - hoistedNoteId = '_options'; + hoistedNoteId = "_options"; } await this.setHoistedNoteId(hoistedNoteId); @@ -127,7 +121,7 @@ class NoteContext extends Component } getSubContexts() { - return appContext.tabManager.noteContexts.filter(nc => nc.ntxId === this.ntxId || nc.mainNtxId === this.ntxId); + return appContext.tabManager.noteContexts.filter((nc) => nc.ntxId === this.ntxId || nc.mainNtxId === this.ntxId); } /** @@ -152,13 +146,11 @@ class NoteContext extends Component if (this.mainNtxId) { try { return appContext.tabManager.getNoteContextById(this.mainNtxId); - } - catch (e) { + } catch (e) { this.mainNtxId = null; return this; } - } - else { + } else { return this; } } @@ -167,7 +159,7 @@ class NoteContext extends Component setTimeout(async () => { // we include the note in the recent list only if the user stayed on the note at least 5 seconds if (resolvedNotePath && resolvedNotePath === this.notePath) { - await server.post('recent-notes', { + await server.post("recent-notes", { noteId: this.note?.noteId, notePath: this.notePath }); @@ -183,7 +175,7 @@ class NoteContext extends Component return; } - if (await hoistedNoteService.checkNoteAccess(resolvedNotePath, this) === false) { + if ((await hoistedNoteService.checkNoteAccess(resolvedNotePath, this)) === false) { return; // note is outside of hoisted subtree and user chose not to unhoist } @@ -200,7 +192,7 @@ class NoteContext extends Component /** @returns {string[]} */ get notePathArray() { - return this.notePath ? this.notePath.split('/') : []; + return this.notePath ? this.notePath.split("/") : []; } isActive() { @@ -208,7 +200,7 @@ class NoteContext extends Component } getPojoState() { - if (this.hoistedNoteId !== 'root') { + if (this.hoistedNoteId !== "root") { // keeping empty hoisted tab is esp. important for mobile (e.g. opened launcher config) if (!this.notePath && this.getSubContexts().length === 0) { @@ -223,11 +215,11 @@ class NoteContext extends Component hoistedNoteId: this.hoistedNoteId, active: this.isActive(), viewScope: this.viewScope - } + }; } async unhoist() { - await this.setHoistedNoteId('root'); + await this.setHoistedNoteId("root"); } async setHoistedNoteId(noteIdToHoist: string) { @@ -241,7 +233,7 @@ class NoteContext extends Component await this.setNote(noteIdToHoist); } - await this.triggerEvent('hoistedNoteChanged', { + await this.triggerEvent("hoistedNoteChanged", { noteId: noteIdToHoist, ntxId: this.ntxId }); @@ -254,15 +246,15 @@ class NoteContext extends Component } // "readOnly" is a state valid only for text/code notes - if (!this.note || (this.note.type !== 'text' && this.note.type !== 'code')) { + if (!this.note || (this.note.type !== "text" && this.note.type !== "code")) { return false; } - if (this.note.isLabelTruthy('readOnly')) { + if (this.note.isLabelTruthy("readOnly")) { return true; } - if (this.viewScope?.viewMode === 'source') { + if (this.viewScope?.viewMode === "source") { return true; } @@ -271,24 +263,20 @@ class NoteContext extends Component return false; } - const sizeLimit = this.note.type === 'text' - ? options.getInt('autoReadonlySizeText') - : options.getInt('autoReadonlySizeCode'); + const sizeLimit = this.note.type === "text" ? options.getInt("autoReadonlySizeText") : options.getInt("autoReadonlySizeCode"); - return sizeLimit - && blob.contentLength > sizeLimit - && !this.note.isLabelTruthy('autoReadOnlyDisabled'); + return sizeLimit && blob.contentLength > sizeLimit && !this.note.isLabelTruthy("autoReadOnlyDisabled"); } - async entitiesReloadedEvent({loadResults}: EventData<"entitiesReloaded">) { + async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { if (this.noteId && loadResults.isNoteReloaded(this.noteId)) { - const noteRow = loadResults.getEntityRow('notes', this.noteId); + const noteRow = loadResults.getEntityRow("notes", this.noteId); if (noteRow.isDeleted) { this.noteId = null; this.notePath = null; - this.triggerEvent('noteSwitched', { + this.triggerEvent("noteSwitched", { noteContext: this, notePath: this.notePath }); @@ -297,48 +285,63 @@ class NoteContext extends Component } hasNoteList() { - return this.note - && this.viewScope?.viewMode === 'default' - && this.note.hasChildren() - && ['book', 'text', 'code'].includes(this.note.type) - && this.note.mime !== 'text/x-sqlite;schema=trilium' - && !this.note.isLabelTruthy('hideChildrenOverview'); + return ( + this.note && + this.viewScope?.viewMode === "default" && + this.note.hasChildren() && + ["book", "text", "code"].includes(this.note.type) && + this.note.mime !== "text/x-sqlite;schema=trilium" && + !this.note.isLabelTruthy("hideChildrenOverview") + ); } async getTextEditor(callback?: GetTextEditorCallback) { - return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithTextEditor', { - callback, - resolve, - ntxId: this.ntxId - }))); + return this.timeout( + new Promise((resolve) => + appContext.triggerCommand("executeWithTextEditor", { + callback, + resolve, + ntxId: this.ntxId + }) + ) + ); } async getCodeEditor() { - return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithCodeEditor', { - resolve, - ntxId: this.ntxId - }))); + return this.timeout( + new Promise((resolve) => + appContext.triggerCommand("executeWithCodeEditor", { + resolve, + ntxId: this.ntxId + }) + ) + ); } async getContentElement() { - return this.timeout>(new Promise(resolve => appContext.triggerCommand('executeWithContentElement', { - resolve, - ntxId: this.ntxId - }))); + return this.timeout>( + new Promise((resolve) => + appContext.triggerCommand("executeWithContentElement", { + resolve, + ntxId: this.ntxId + }) + ) + ); } async getTypeWidget() { - return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithTypeWidget', { - resolve, - ntxId: this.ntxId - }))); + return this.timeout( + new Promise((resolve) => + appContext.triggerCommand("executeWithTypeWidget", { + resolve, + ntxId: this.ntxId + }) + ) + ); } timeout(promise: Promise) { - return Promise.race([ - promise, - new Promise(res => setTimeout(() => res(null), 200)) - ]) as Promise; + return Promise.race([promise, new Promise((res) => setTimeout(() => res(null), 200))]) as Promise; } resetViewScope() { @@ -355,9 +358,7 @@ class NoteContext extends Component const { note, viewScope } = this; - let title = viewScope?.viewMode === 'default' - ? note.title - : `${note.title}: ${viewScope?.viewMode}`; + let title = viewScope?.viewMode === "default" ? note.title : `${note.title}: ${viewScope?.viewMode}`; if (viewScope?.attachmentId) { // assuming the attachment has been already loaded diff --git a/src/public/app/components/root_command_executor.ts b/src/public/app/components/root_command_executor.ts index afc3ec8fd..643f02c7b 100644 --- a/src/public/app/components/root_command_executor.ts +++ b/src/public/app/components/root_command_executor.ts @@ -25,11 +25,11 @@ export default class RootCommandExecutor extends Component { const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(sqlConsoleNote.noteId, { activate: true }); - appContext.triggerEvent('focusOnDetail', {ntxId: noteContext.ntxId}); + appContext.triggerEvent("focusOnDetail", { ntxId: noteContext.ntxId }); } - async searchNotesCommand({searchString, ancestorNoteId}: CommandListenerData<"searchNotes">) { - const searchNote = await dateNoteService.createSearchNote({searchString, ancestorNoteId}); + async searchNotesCommand({ searchString, ancestorNoteId }: CommandListenerData<"searchNotes">) { + const searchNote = await dateNoteService.createSearchNote({ searchString, ancestorNoteId }); if (!searchNote) { return; } @@ -41,13 +41,13 @@ export default class RootCommandExecutor extends Component { activate: true }); - appContext.triggerCommand('focusOnSearchDefinition', {ntxId: noteContext.ntxId}); + appContext.triggerCommand("focusOnSearchDefinition", { ntxId: noteContext.ntxId }); } - async searchInSubtreeCommand({notePath}: CommandListenerData<"searchInSubtree">) { + async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) { const noteId = treeService.getNoteIdFromUrl(notePath); - this.searchNotesCommand({ancestorNoteId: noteId}); + this.searchNotesCommand({ ancestorNoteId: noteId }); } openNoteExternallyCommand() { @@ -83,11 +83,11 @@ export default class RootCommandExecutor extends Component { } toggleLeftPaneCommand() { - options.toggle('leftPaneVisible'); + options.toggle("leftPaneVisible"); } async showBackendLogCommand() { - await appContext.tabManager.openTabWithNoteWithHoisting('_backendLog', { activate: true }); + await appContext.tabManager.openTabWithNoteWithHoisting("_backendLog", { activate: true }); } async showLaunchBarSubtreeCommand() { @@ -97,26 +97,26 @@ export default class RootCommandExecutor extends Component { } async showShareSubtreeCommand() { - await this.showAndHoistSubtree('_share'); + await this.showAndHoistSubtree("_share"); } async showHiddenSubtreeCommand() { - await this.showAndHoistSubtree('_hidden'); + await this.showAndHoistSubtree("_hidden"); } - async showOptionsCommand({section}: CommandListenerData<"showOptions">) { - await appContext.tabManager.openContextWithNote(section || '_options', { + async showOptionsCommand({ section }: CommandListenerData<"showOptions">) { + await appContext.tabManager.openContextWithNote(section || "_options", { activate: true, - hoistedNoteId: '_options' + hoistedNoteId: "_options" }); } async showSQLConsoleHistoryCommand() { - await this.showAndHoistSubtree('_sqlConsole'); + await this.showAndHoistSubtree("_sqlConsole"); } async showSearchHistoryCommand() { - await this.showAndHoistSubtree('_search'); + await this.showAndHoistSubtree("_search"); } async showAndHoistSubtree(subtreeNoteId: string) { @@ -133,7 +133,7 @@ export default class RootCommandExecutor extends Component { await appContext.tabManager.openTabWithNoteWithHoisting(notePath, { activate: true, viewScope: { - viewMode: 'source' + viewMode: "source" } }); } @@ -146,7 +146,7 @@ export default class RootCommandExecutor extends Component { await appContext.tabManager.openTabWithNoteWithHoisting(notePath, { activate: true, viewScope: { - viewMode: 'attachments' + viewMode: "attachments" } }); } @@ -159,7 +159,7 @@ export default class RootCommandExecutor extends Component { await appContext.tabManager.openTabWithNoteWithHoisting(notePath, { activate: true, viewScope: { - viewMode: 'attachments' + viewMode: "attachments" } }); } @@ -167,23 +167,43 @@ export default class RootCommandExecutor extends Component { toggleTrayCommand() { if (!utils.isElectron()) return; - const {BrowserWindow} = utils.dynamicRequire('@electron/remote'); - const windows = (BrowserWindow.getAllWindows()) as Electron.BaseWindow[]; - const isVisible = windows.every(w => w.isVisible()); - const action = isVisible ? "hide" : "show" + const { BrowserWindow } = utils.dynamicRequire("@electron/remote"); + const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[]; + const isVisible = windows.every((w) => w.isVisible()); + const action = isVisible ? "hide" : "show"; for (const window of windows) window[action](); } - firstTabCommand() { this.#goToTab(1); } - secondTabCommand() { this.#goToTab(2); } - thirdTabCommand() { this.#goToTab(3); } - fourthTabCommand() { this.#goToTab(4); } - fifthTabCommand() { this.#goToTab(5); } - sixthTabCommand() { this.#goToTab(6); } - seventhTabCommand() { this.#goToTab(7); } - eigthTabCommand() { this.#goToTab(8); } - ninthTabCommand() { this.#goToTab(9); } - lastTabCommand() { this.#goToTab(Number.POSITIVE_INFINITY); } + firstTabCommand() { + this.#goToTab(1); + } + secondTabCommand() { + this.#goToTab(2); + } + thirdTabCommand() { + this.#goToTab(3); + } + fourthTabCommand() { + this.#goToTab(4); + } + fifthTabCommand() { + this.#goToTab(5); + } + sixthTabCommand() { + this.#goToTab(6); + } + seventhTabCommand() { + this.#goToTab(7); + } + eigthTabCommand() { + this.#goToTab(8); + } + ninthTabCommand() { + this.#goToTab(9); + } + lastTabCommand() { + this.#goToTab(Number.POSITIVE_INFINITY); + } #goToTab(tabNumber: number) { const mainNoteContexts = appContext.tabManager.getMainNoteContexts(); diff --git a/src/public/app/components/shortcut_component.ts b/src/public/app/components/shortcut_component.ts index 4d01a0588..d2b1ee8b0 100644 --- a/src/public/app/components/shortcut_component.ts +++ b/src/public/app/components/shortcut_component.ts @@ -5,13 +5,11 @@ import Component from "./component.js"; import froca from "../services/froca.js"; import { AttributeRow } from "../services/load_results.js"; -export default class ShortcutComponent extends Component - implements EventListener<"entitiesReloaded"> -{ +export default class ShortcutComponent extends Component implements EventListener<"entitiesReloaded"> { constructor() { super(); - server.get('keyboard-shortcuts-for-notes').then(shortcutAttributes => { + server.get("keyboard-shortcuts-for-notes").then((shortcutAttributes) => { for (const attr of shortcutAttributes) { this.bindNoteShortcutHandler(attr); } @@ -22,7 +20,8 @@ export default class ShortcutComponent extends Component const handler = () => appContext.tabManager.getActiveContext().setNote(labelOrRow.noteId); const namespace = labelOrRow.attributeId; - if (labelOrRow.isDeleted) { // only applicable if row + if (labelOrRow.isDeleted) { + // only applicable if row if (namespace) { shortcutService.removeGlobalShortcut(namespace); } @@ -31,12 +30,12 @@ export default class ShortcutComponent extends Component } } - async entitiesReloadedEvent({loadResults}: EventData<"entitiesReloaded">) { + async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { for (const attr of loadResults.getAttributeRows()) { - if (attr.type === 'label' && attr.name === 'keyboardShortcut' && attr.noteId) { + if (attr.type === "label" && attr.name === "keyboardShortcut" && attr.noteId) { const note = await froca.getNote(attr.noteId); // launcher shortcuts are handled specifically - if (note && attr && note.type !== 'launcher') { + if (note && attr && note.type !== "launcher") { this.bindNoteShortcutHandler(attr); } } diff --git a/src/public/app/components/tab_manager.js b/src/public/app/components/tab_manager.js index ccff6f89e..7a7b2f2cf 100644 --- a/src/public/app/components/tab_manager.js +++ b/src/public/app/components/tab_manager.js @@ -28,11 +28,9 @@ export default class TabManager extends Component { return; } - const openNoteContexts = this.noteContexts - .map(nc => nc.getPojoState()) - .filter(t => !!t); + const openNoteContexts = this.noteContexts.map((nc) => nc.getPojoState()).filter((t) => !!t); - await server.put('options', { + await server.put("options", { openNoteContexts: JSON.stringify(openNoteContexts) }); }); @@ -47,21 +45,17 @@ export default class TabManager extends Component { /** @type {NoteContext[]} */ get mainNoteContexts() { - return this.noteContexts.filter(nc => !nc.mainNtxId) + return this.noteContexts.filter((nc) => !nc.mainNtxId); } async loadTabs() { try { - const noteContextsToOpen = (appContext.isMainWindow && options.getJson('openNoteContexts')) || []; + const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || []; // preload all notes at once - await froca.getNotes([ - ...noteContextsToOpen.flatMap(tab => - [ treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId] - ), - ], true); + await froca.getNotes([...noteContextsToOpen.flatMap((tab) => [treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId])], true); - const filteredNoteContexts = noteContextsToOpen.filter(openTab => { + const filteredNoteContexts = noteContextsToOpen.filter((openTab) => { const noteId = treeService.getNoteIdFromUrl(openTab.notePath); if (!(noteId in froca.notes)) { // note doesn't exist so don't try to open tab for it @@ -69,7 +63,7 @@ export default class TabManager extends Component { } if (!(openTab.hoistedNoteId in froca.notes)) { - openTab.hoistedNoteId = 'root'; + openTab.hoistedNoteId = "root"; } return true; @@ -82,13 +76,13 @@ export default class TabManager extends Component { parsedFromUrl.ntxId = parsedFromUrl.ntxId || NoteContext.generateNtxId(); // generate already here, so that we later know which one to activate filteredNoteContexts.push({ - notePath: parsedFromUrl.notePath || 'root', + notePath: parsedFromUrl.notePath || "root", ntxId: parsedFromUrl.ntxId, active: true, - hoistedNoteId: parsedFromUrl.hoistedNoteId || 'root', + hoistedNoteId: parsedFromUrl.hoistedNoteId || "root", viewScope: parsedFromUrl.viewScope || {} }); - } else if (!filteredNoteContexts.find(tab => tab.active)) { + } else if (!filteredNoteContexts.find((tab) => tab.active)) { filteredNoteContexts[0].active = true; } @@ -107,27 +101,21 @@ export default class TabManager extends Component { // if there's a notePath in the URL, make sure it's open and active // (useful, for e.g., opening clipped notes from clipper or opening link in an extra window) if (parsedFromUrl.notePath) { - await appContext.tabManager.switchToNoteContext( - parsedFromUrl.ntxId, - parsedFromUrl.notePath, - parsedFromUrl.viewScope, - parsedFromUrl.hoistedNoteId - ); + await appContext.tabManager.switchToNoteContext(parsedFromUrl.ntxId, parsedFromUrl.notePath, parsedFromUrl.viewScope, parsedFromUrl.hoistedNoteId); } else if (parsedFromUrl.searchString) { - await appContext.triggerCommand('searchNotes', { + await appContext.triggerCommand("searchNotes", { searchString: parsedFromUrl.searchString }); } - } - catch (e) { - logError(`Loading note contexts '${options.get('openNoteContexts')}' failed: ${e.message} ${e.stack}`); + } catch (e) { + logError(`Loading note contexts '${options.get("openNoteContexts")}' failed: ${e.message} ${e.stack}`); // try to recover await this.openEmptyTab(); } } - noteSwitchedEvent({noteContext}) { + noteSwitchedEvent({ noteContext }) { if (noteContext.isActive()) { this.setCurrentNavigationStateToHash(); } @@ -147,7 +135,7 @@ export default class TabManager extends Component { const activeNoteContext = this.getActiveContext(); this.updateDocumentTitle(activeNoteContext); - this.triggerEvent('activeNoteChanged'); // trigger this even in on popstate event + this.triggerEvent("activeNoteChanged"); // trigger this even in on popstate event } calculateHash() { @@ -174,12 +162,12 @@ export default class TabManager extends Component { * @returns {NoteContext[]} */ getMainNoteContexts() { - return this.noteContexts.filter(nc => nc.isMainContext()); + return this.noteContexts.filter((nc) => nc.isMainContext()); } /** @returns {NoteContext} */ getNoteContextById(ntxId) { - const noteContext = this.noteContexts.find(nc => nc.ntxId === ntxId); + const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId); if (!noteContext) { throw new Error(`Cannot find noteContext id='${ntxId}'`); @@ -194,9 +182,7 @@ export default class TabManager extends Component { * @returns {NoteContext} */ getActiveContext() { - return this.activeNtxId - ? this.getNoteContextById(this.activeNtxId) - : null; + return this.activeNtxId ? this.getNoteContextById(this.activeNtxId) : null; } /** @@ -205,9 +191,7 @@ export default class TabManager extends Component { * @returns {NoteContext} */ getActiveMainContext() { - return this.activeNtxId - ? this.getNoteContextById(this.activeNtxId).getMainContext() - : null; + return this.activeNtxId ? this.getNoteContextById(this.activeNtxId).getMainContext() : null; } /** @returns {string|null} */ @@ -243,8 +227,7 @@ export default class TabManager extends Component { } async switchToNoteContext(ntxId, notePath, viewScope = {}, hoistedNoteId = null) { - const noteContext = this.noteContexts.find(nc => nc.ntxId === ntxId) - || await this.openEmptyTab(); + const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId) || (await this.openEmptyTab()); await this.activateNoteContext(noteContext.ntxId); @@ -265,10 +248,10 @@ export default class TabManager extends Component { await noteContext.setEmpty(); } - async openEmptyTab(ntxId = null, hoistedNoteId = 'root', mainNtxId = null) { + async openEmptyTab(ntxId = null, hoistedNoteId = "root", mainNtxId = null) { const noteContext = new NoteContext(ntxId, hoistedNoteId, mainNtxId); - const existingNoteContext = this.children.find(nc => nc.ntxId === noteContext.ntxId); + const existingNoteContext = this.children.find((nc) => nc.ntxId === noteContext.ntxId); if (existingNoteContext) { await existingNoteContext.setHoistedNoteId(hoistedNoteId); @@ -278,7 +261,7 @@ export default class TabManager extends Component { this.child(noteContext); - await this.triggerEvent('newNoteContextCreated', {noteContext}); + await this.triggerEvent("newNoteContextCreated", { noteContext }); return noteContext; } @@ -300,12 +283,12 @@ export default class TabManager extends Component { */ async openTabWithNoteWithHoisting(notePath, opts = {}) { const noteContext = this.getActiveContext(); - let hoistedNoteId = 'root'; + let hoistedNoteId = "root"; if (noteContext) { const resolvedNotePath = await treeService.resolveNotePath(notePath, noteContext.hoistedNoteId); - if (resolvedNotePath.includes(noteContext.hoistedNoteId) || resolvedNotePath.includes('_hidden')) { + if (resolvedNotePath.includes(noteContext.hoistedNoteId) || resolvedNotePath.includes("_hidden")) { hoistedNoteId = noteContext.hoistedNoteId; } } @@ -319,7 +302,7 @@ export default class TabManager extends Component { const activate = !!opts.activate; const ntxId = opts.ntxId || null; const mainNtxId = opts.mainNtxId || null; - const hoistedNoteId = opts.hoistedNoteId || 'root'; + const hoistedNoteId = opts.hoistedNoteId || "root"; const viewScope = opts.viewScope || { viewMode: "default" }; const noteContext = await this.openEmptyTab(ntxId, hoistedNoteId, mainNtxId); @@ -335,7 +318,7 @@ export default class TabManager extends Component { if (activate) { this.activateNoteContext(noteContext.ntxId, false); - await this.triggerEvent('noteSwitchedAndActivated', { + await this.triggerEvent("noteSwitchedAndActivated", { noteContext, notePath: noteContext.notePath // resolved note path }); @@ -366,7 +349,7 @@ export default class TabManager extends Component { this.activeNtxId = ntxId; if (triggerEvent) { - await this.triggerEvent('activeContextChanged', { + await this.triggerEvent("activeContextChanged", { noteContext: this.getNoteContextById(ntxId) }); } @@ -388,14 +371,13 @@ export default class TabManager extends Component { try { noteContextToRemove = this.getNoteContextById(ntxId); - } - catch { + } catch { // note context not found return false; } if (noteContextToRemove.isMainContext()) { - const mainNoteContexts = this.getNoteContexts().filter(nc => nc.isMainContext()); + const mainNoteContexts = this.getNoteContexts().filter((nc) => nc.isMainContext()); if (mainNoteContexts.length === 1) { if (noteContextToRemove.isEmpty()) { @@ -415,28 +397,25 @@ export default class TabManager extends Component { } const noteContextsToRemove = noteContextToRemove.getSubContexts(); - const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId); + const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId); - await this.triggerEvent('beforeNoteContextRemove', { ntxIds: ntxIdsToRemove }); + await this.triggerEvent("beforeNoteContextRemove", { ntxIds: ntxIdsToRemove }); if (!noteContextToRemove.isMainContext()) { const siblings = noteContextToRemove.getMainContext().getSubContexts(); - const idx = siblings.findIndex(nc => nc.ntxId === noteContextToRemove.ntxId); + 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(); - } - else if (ntxIdsToRemove.includes(this.activeNtxId)) { - const idx = this.mainNoteContexts.findIndex(nc => nc.ntxId === noteContextToRemove.ntxId); + } else if (ntxIdsToRemove.includes(this.activeNtxId)) { + const idx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === noteContextToRemove.ntxId); if (idx === this.mainNoteContexts.length - 1) { await this.activatePreviousTabCommand(); - } - else { + } else { await this.activateNextTabCommand(); } } @@ -448,15 +427,15 @@ export default class TabManager extends Component { } removeNoteContexts(noteContextsToRemove) { - const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId); + const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId); - const position = this.noteContexts.findIndex(nc => ntxIdsToRemove.includes(nc.ntxId)); + const position = this.noteContexts.findIndex((nc) => ntxIdsToRemove.includes(nc.ntxId)); - this.children = this.children.filter(nc => !ntxIdsToRemove.includes(nc.ntxId)); + this.children = this.children.filter((nc) => !ntxIdsToRemove.includes(nc.ntxId)); this.addToRecentlyClosedTabs(noteContextsToRemove, position); - this.triggerEvent('noteContextRemoved', {ntxIds: ntxIdsToRemove}); + this.triggerEvent("noteContextRemoved", { ntxIds: ntxIdsToRemove }); this.tabsUpdate.scheduleUpdate(); } @@ -466,10 +445,10 @@ export default class TabManager extends Component { return; } - this.recentlyClosedTabs.push({contexts: noteContexts, position: position}); + this.recentlyClosedTabs.push({ contexts: noteContexts, position: position }); } - tabReorderEvent({ntxIdsInOrder}) { + tabReorderEvent({ ntxIdsInOrder }) { const order = {}; let i = 0; @@ -480,18 +459,18 @@ export default class TabManager extends Component { } } - this.children.sort((a, b) => order[a.ntxId] < order[b.ntxId] ? -1 : 1); + this.children.sort((a, b) => (order[a.ntxId] < order[b.ntxId] ? -1 : 1)); this.tabsUpdate.scheduleUpdate(); } - noteContextReorderEvent({ntxIdsInOrder, oldMainNtxId, newMainNtxId}) { + noteContextReorderEvent({ ntxIdsInOrder, oldMainNtxId, newMainNtxId }) { const order = Object.fromEntries(ntxIdsInOrder.map((v, i) => [v, i])); - this.children.sort((a, b) => order[a.ntxId] < order[b.ntxId] ? -1 : 1); + this.children.sort((a, b) => (order[a.ntxId] < order[b.ntxId] ? -1 : 1)); if (oldMainNtxId && newMainNtxId) { - this.children.forEach(c => { + this.children.forEach((c) => { if (c.ntxId === newMainNtxId) { // new main context has null mainNtxId c.mainNtxId = null; @@ -508,7 +487,7 @@ export default class TabManager extends Component { async activateNextTabCommand() { const activeMainNtxId = this.getActiveMainContext().ntxId; - const oldIdx = this.mainNoteContexts.findIndex(nc => nc.ntxId === activeMainNtxId); + const oldIdx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === activeMainNtxId); const newActiveNtxId = this.mainNoteContexts[oldIdx === this.mainNoteContexts.length - 1 ? 0 : oldIdx + 1].ntxId; await this.activateNoteContext(newActiveNtxId); @@ -517,7 +496,7 @@ export default class TabManager extends Component { async activatePreviousTabCommand() { const activeMainNtxId = this.getActiveMainContext().ntxId; - const oldIdx = this.mainNoteContexts.findIndex(nc => nc.ntxId === activeMainNtxId); + const oldIdx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === activeMainNtxId); const newActiveNtxId = this.mainNoteContexts[oldIdx === 0 ? this.mainNoteContexts.length - 1 : oldIdx - 1].ntxId; await this.activateNoteContext(newActiveNtxId); @@ -538,21 +517,21 @@ export default class TabManager extends Component { } async closeAllTabsCommand() { - for (const ntxIdToRemove of this.mainNoteContexts.map(nc => nc.ntxId)) { + for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) { await this.removeNoteContext(ntxIdToRemove); } } - async closeOtherTabsCommand({ntxId}) { - for (const ntxIdToRemove of this.mainNoteContexts.map(nc => nc.ntxId)) { + async closeOtherTabsCommand({ ntxId }) { + for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) { if (ntxIdToRemove !== ntxId) { await this.removeNoteContext(ntxIdToRemove); } } } - async closeRightTabsCommand({ntxId}) { - const ntxIds = this.mainNoteContexts.map(nc => nc.ntxId); + async closeRightTabsCommand({ ntxId }) { + const ntxIds = this.mainNoteContexts.map((nc) => nc.ntxId); const index = ntxIds.indexOf(ntxId); if (index !== -1) { @@ -563,23 +542,23 @@ export default class TabManager extends Component { } } - async closeTabCommand({ntxId}) { + async closeTabCommand({ ntxId }) { await this.removeNoteContext(ntxId); } - async moveTabToNewWindowCommand({ntxId}) { - const {notePath, hoistedNoteId} = this.getNoteContextById(ntxId); + async moveTabToNewWindowCommand({ ntxId }) { + const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId); const removed = await this.removeNoteContext(ntxId); if (removed) { - this.triggerCommand('openInWindow', {notePath, hoistedNoteId}); + this.triggerCommand("openInWindow", { notePath, hoistedNoteId }); } } - async copyTabToNewWindowCommand({ntxId}) { - const {notePath, hoistedNoteId} = this.getNoteContextById(ntxId); - this.triggerCommand('openInWindow', {notePath, hoistedNoteId}); + async copyTabToNewWindowCommand({ ntxId }) { + const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId); + this.triggerCommand("openInWindow", { notePath, hoistedNoteId }); } async reopenLastTabCommand() { @@ -601,40 +580,38 @@ export default class TabManager extends Component { for (const noteContext of noteContexts) { this.child(noteContext); - await this.triggerEvent('newNoteContextCreated', {noteContext}); + await this.triggerEvent("newNoteContextCreated", { noteContext }); } // restore last position of contexts stored in tab manager const ntxsInOrder = [ ...this.noteContexts.slice(0, lastClosedTab.position), ...this.noteContexts.slice(-noteContexts.length), - ...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length), - ] - await this.noteContextReorderEvent({ntxIdsInOrder: ntxsInOrder.map(nc => nc.ntxId)}); + ...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length) + ]; + await this.noteContextReorderEvent({ ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId) }); - let mainNtx = noteContexts.find(nc => nc.isMainContext()); + let mainNtx = noteContexts.find((nc) => nc.isMainContext()); if (mainNtx) { // reopened a tab, need to reorder new tab widget in tab row - await this.triggerEvent('contextsReopened', { + await this.triggerEvent("contextsReopened", { mainNtxId: mainNtx.ntxId, - tabPosition: ntxsInOrder.filter(nc => nc.isMainContext()).findIndex(nc => nc.ntxId === mainNtx.ntxId) + tabPosition: ntxsInOrder.filter((nc) => nc.isMainContext()).findIndex((nc) => nc.ntxId === mainNtx.ntxId) }); } else { // reopened a single split, need to reorder the pane widget in split note container - await this.triggerEvent('contextsReopened', { + await this.triggerEvent("contextsReopened", { ntxId: ntxsInOrder[lastClosedTab.position].ntxId, // this is safe since lastClosedTab.position can never be 0 in this case afterNtxId: ntxsInOrder[lastClosedTab.position - 1].ntxId }); } - const noteContextToActivate = noteContexts.length === 1 - ? noteContexts[0] - : noteContexts.find(nc => nc.isMainContext()); + const noteContextToActivate = noteContexts.length === 1 ? noteContexts[0] : noteContexts.find((nc) => nc.isMainContext()); await this.activateNoteContext(noteContextToActivate.ntxId); - await this.triggerEvent('noteSwitched', { + await this.triggerEvent("noteSwitched", { noteContext: noteContextToActivate, notePath: noteContextToActivate.notePath }); @@ -659,7 +636,7 @@ export default class TabManager extends Component { document.title = titleFragments.join(" - "); } - async entitiesReloadedEvent({loadResults}) { + async entitiesReloadedEvent({ loadResults }) { const activeContext = this.getActiveContext(); if (activeContext && loadResults.isNoteReloaded(activeContext.noteId)) { diff --git a/src/public/app/components/zoom.ts b/src/public/app/components/zoom.ts index 8a39e3db1..938700ba0 100644 --- a/src/public/app/components/zoom.ts +++ b/src/public/app/components/zoom.ts @@ -11,13 +11,13 @@ class ZoomComponent extends Component { if (utils.isElectron()) { options.initializedPromise.then(() => { - const zoomFactor = options.getFloat('zoomFactor'); + const zoomFactor = options.getFloat("zoomFactor"); if (zoomFactor) { this.setZoomFactor(zoomFactor); } }); - window.addEventListener("wheel", event => { + window.addEventListener("wheel", (event) => { if (event.ctrlKey) { this.setZoomFactorAndSave(this.getCurrentZoom() - event.deltaY * 0.001); } @@ -26,8 +26,8 @@ class ZoomComponent extends Component { } setZoomFactor(zoomFactor: string | number) { - const parsedZoomFactor = (typeof zoomFactor !== "number" ? parseFloat(zoomFactor) : zoomFactor); - const webFrame = utils.dynamicRequire('electron').webFrame; + const parsedZoomFactor = typeof zoomFactor !== "number" ? parseFloat(zoomFactor) : zoomFactor; + const webFrame = utils.dynamicRequire("electron").webFrame; webFrame.setZoomFactor(parsedZoomFactor); } @@ -37,15 +37,14 @@ class ZoomComponent extends Component { this.setZoomFactor(zoomFactor); - await options.save('zoomFactor', zoomFactor); - } - else { + await options.save("zoomFactor", zoomFactor); + } else { console.log(`Zoom factor ${zoomFactor} outside of the range, ignored.`); } } getCurrentZoom() { - return utils.dynamicRequire('electron').webFrame.getZoomFactor(); + return utils.dynamicRequire("electron").webFrame.getZoomFactor(); } zoomOutEvent() { @@ -58,7 +57,7 @@ class ZoomComponent extends Component { zoomResetEvent() { this.setZoomFactorAndSave(1); } - + setZoomFactorAndSaveEvent({ zoomFactor }: { zoomFactor: number }) { this.setZoomFactorAndSave(zoomFactor); } diff --git a/src/public/app/desktop.js b/src/public/app/desktop.js index 8dc5ebdff..60dd8fe5d 100644 --- a/src/public/app/desktop.js +++ b/src/public/app/desktop.js @@ -1,10 +1,10 @@ import appContext from "./components/app_context.js"; -import utils from './services/utils.js'; -import noteTooltipService from './services/note_tooltip.js'; +import utils from "./services/utils.js"; +import noteTooltipService from "./services/note_tooltip.js"; import bundleService from "./services/bundle.js"; import toastService from "./services/toast.js"; -import noteAutocompleteService from './services/note_autocomplete.js'; -import macInit from './services/mac_init.js'; +import noteAutocompleteService from "./services/note_autocomplete.js"; +import macInit from "./services/mac_init.js"; import electronContextMenu from "./menus/electron_context_menu.js"; import glob from "./services/glob.js"; import { t } from "./services/i18n.js"; @@ -12,20 +12,19 @@ import options from "./services/options.js"; await appContext.earlyInit(); -bundleService.getWidgetBundlesByParent().then(async widgetBundles => { +bundleService.getWidgetBundlesByParent().then(async (widgetBundles) => { // A dynamic import is required for layouts since they initialize components which require translations. const DesktopLayout = (await import("./layouts/desktop_layout.js")).default; appContext.setLayout(new DesktopLayout(widgetBundles)); - appContext.start() - .catch((e) => { - toastService.showPersistent({ - title: t("toast.critical-error.title"), - icon: "alert", - message: t("toast.critical-error.message", { message: e.message }), - }); - console.error("Critical error occured", e); + appContext.start().catch((e) => { + toastService.showPersistent({ + title: t("toast.critical-error.title"), + icon: "alert", + message: t("toast.critical-error.message", { message: e.message }) }); + console.error("Critical error occured", e); + }); }); glob.setupGlobs(); @@ -45,8 +44,8 @@ if (utils.isElectron()) { } function initOnElectron() { - const electron = utils.dynamicRequire('electron'); - electron.ipcRenderer.on('globalShortcut', async (event, actionName) => appContext.triggerCommand(actionName)); + const electron = utils.dynamicRequire("electron"); + electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName)); const electronRemote = utils.dynamicRequire("@electron/remote"); const currentWindow = electronRemote.getCurrentWindow(); @@ -72,8 +71,7 @@ function initTitleBarButtons(style, currentWindow) { applyWindowsOverlay(); // Register for changes to the native title bar colors. - window.matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", applyWindowsOverlay); + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyWindowsOverlay); } if (window.glob.platform === "darwin") { diff --git a/src/public/app/entities/fattachment.ts b/src/public/app/entities/fattachment.ts index 20ba79c94..67215b33f 100644 --- a/src/public/app/entities/fattachment.ts +++ b/src/public/app/entities/fattachment.ts @@ -23,12 +23,12 @@ class FAttachment { role!: string; mime!: string; title!: string; - isProtected!: boolean; // TODO: Is this used? + isProtected!: boolean; // TODO: Is this used? private dateModified!: string; utcDateModified!: string; private utcDateScheduledForErasureSince!: string; /** - * optionally added to the entity + * optionally added to the entity */ private contentLength!: number; @@ -58,7 +58,7 @@ class FAttachment { } async getBlob() { - return await this.froca.getBlob('attachments', this.attachmentId); + return await this.froca.getBlob("attachments", this.attachmentId); } } diff --git a/src/public/app/entities/fattribute.ts b/src/public/app/entities/fattribute.ts index 379f0588a..5ad29db6b 100644 --- a/src/public/app/entities/fattribute.ts +++ b/src/public/app/entities/fattribute.ts @@ -1,5 +1,5 @@ -import { Froca } from '../services/froca-interface.js'; -import promotedAttributeDefinitionParser from '../services/promoted_attribute_definition_parser.js'; +import { Froca } from "../services/froca-interface.js"; +import promotedAttributeDefinitionParser from "../services/promoted_attribute_definition_parser.js"; /** * There are currently only two types of attributes, labels or relations. @@ -16,7 +16,6 @@ export interface FAttributeRow { isInheritable: boolean; } - /** * Attribute is an abstract concept which has two real uses - label (key - value pair) * and relation (representing named relationship between source and target note) @@ -57,8 +56,9 @@ class FAttribute { return await this.froca.getNote(targetNoteId, true); } - get targetNoteId() { // alias - if (this.type !== 'relation') { + get targetNoteId() { + // alias + if (this.type !== "relation") { throw new Error(`Attribute ${this.attributeId} is not a relation`); } @@ -66,7 +66,7 @@ class FAttribute { } get isAutoLink() { - return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name); + return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name); } get toString() { @@ -74,7 +74,7 @@ class FAttribute { } isDefinition() { - return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:')); + return this.type === "label" && (this.name.startsWith("label:") || this.name.startsWith("relation:")); } getDefinition() { @@ -82,7 +82,7 @@ class FAttribute { } isDefinitionFor(attr: FAttribute) { - return this.type === 'label' && this.name === `${attr.type}:${attr.name}`; + return this.type === "label" && this.name === `${attr.type}:${attr.name}`; } get dto(): Omit { diff --git a/src/public/app/entities/fblob.ts b/src/public/app/entities/fblob.ts index d260f7da6..fc00e1d1f 100644 --- a/src/public/app/entities/fblob.ts +++ b/src/public/app/entities/fblob.ts @@ -1,4 +1,3 @@ - export interface FBlobRow { blobId: string; content: string; @@ -8,7 +7,6 @@ export interface FBlobRow { } export default class FBlob { - blobId: string; /** * can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images) @@ -40,8 +38,7 @@ export default class FBlob { getJsonContentSafely(): unknown | null { try { return this.getJsonContent(); - } - catch (e) { + } catch (e) { return null; } } diff --git a/src/public/app/entities/fbranch.ts b/src/public/app/entities/fbranch.ts index d497c70af..fb1879b38 100644 --- a/src/public/app/entities/fbranch.ts +++ b/src/public/app/entities/fbranch.ts @@ -61,7 +61,7 @@ class FBranch { /** @returns true if it's top level, meaning its parent is the root note */ isTopLevel() { - return this.parentNoteId === 'root'; + return this.parentNoteId === "root"; } get toString() { @@ -69,7 +69,7 @@ class FBranch { } get pojo(): Omit { - const pojo = {...this} as any; + const pojo = { ...this } as any; delete pojo.froca; return pojo; } diff --git a/src/public/app/entities/fnote.ts b/src/public/app/entities/fnote.ts index 0fd19fff5..27ede68ce 100644 --- a/src/public/app/entities/fnote.ts +++ b/src/public/app/entities/fnote.ts @@ -1,33 +1,33 @@ -import server from '../services/server.js'; +import server from "../services/server.js"; import noteAttributeCache from "../services/note_attribute_cache.js"; import ws from "../services/ws.js"; import froca from "../services/froca.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; import cssClassManager from "../services/css_class_manager.js"; -import { Froca } from '../services/froca-interface.js'; -import FAttachment from './fattachment.js'; -import FAttribute, { AttributeType } from './fattribute.js'; -import utils from '../services/utils.js'; +import { Froca } from "../services/froca-interface.js"; +import FAttachment from "./fattachment.js"; +import FAttribute, { AttributeType } from "./fattribute.js"; +import utils from "../services/utils.js"; -const LABEL = 'label'; -const RELATION = 'relation'; +const LABEL = "label"; +const RELATION = "relation"; const NOTE_TYPE_ICONS = { - "file": "bx bx-file", - "image": "bx bx-image", - "code": "bx bx-code", - "render": "bx bx-extension", - "search": "bx bx-file-find", - "relationMap": "bx bxs-network-chart", - "book": "bx bx-book", - "noteMap": "bx bxs-network-chart", - "mermaid": "bx bx-selection", - "canvas": "bx bx-pen", - "webView": "bx bx-globe-alt", - "launcher": "bx bx-link", - "doc": "bx bxs-file-doc", - "contentWidget": "bx bxs-widget", - "mindMap": "bx bx-sitemap" + file: "bx bx-file", + image: "bx bx-image", + code: "bx bx-code", + render: "bx bx-extension", + search: "bx bx-file-find", + relationMap: "bx bxs-network-chart", + book: "bx bx-book", + noteMap: "bx bxs-network-chart", + mermaid: "bx bx-selection", + canvas: "bx bx-pen", + webView: "bx bx-globe-alt", + launcher: "bx bx-link", + doc: "bx bxs-file-doc", + contentWidget: "bx bxs-widget", + mindMap: "bx bx-sitemap" }; /** @@ -65,7 +65,6 @@ export interface NoteMetaData { * Note is the main node and concept in Trilium. */ class FNote { - private froca: Froca; noteId!: string; @@ -119,7 +118,7 @@ class FNote { } addParent(parentNoteId: string, branchId: string, sort = true) { - if (parentNoteId === 'none') { + if (parentNoteId === "none") { return; } @@ -179,8 +178,7 @@ class FNote { try { return JSON.parse(content); - } - catch (e: any) { + } catch (e: any) { console.log(`Cannot parse content of note '${this.noteId}': `, e.message); return null; @@ -217,7 +215,7 @@ class FNote { getChildBranches() { // don't use Object.values() to guarantee order - const branchIds = this.children.map(childNoteId => this.childToBranch[childNoteId]); + const branchIds = this.children.map((childNoteId) => this.childToBranch[childNoteId]); return this.froca.getBranches(branchIds); } @@ -236,7 +234,7 @@ class FNote { this.parents.sort((aNoteId, bNoteId) => { const aBranchId = this.parentToBranch[aNoteId]; - if (aBranchId && aBranchId.startsWith('virt-')) { + if (aBranchId && aBranchId.startsWith("virt-")) { return 1; } @@ -251,7 +249,7 @@ class FNote { } get isArchived() { - return this.hasAttribute('label', 'archived'); + return this.hasAttribute("label", "archived"); } getChildNoteIds() { @@ -271,22 +269,21 @@ class FNote { } async getAttachmentsByRole(role: string) { - return (await this.getAttachments()) - .filter(attachment => attachment.role === role); + return (await this.getAttachments()).filter((attachment) => attachment.role === role); } async getAttachmentById(attachmentId: string) { const attachments = await this.getAttachments(); - return attachments.find(att => att.attachmentId === attachmentId); + return attachments.find((att) => att.attachmentId === attachmentId); } isEligibleForConversionToAttachment() { - if (this.type !== 'image' || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) { + if (this.type !== "image" || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) { return false; } - const targetRelations = this.getTargetRelations().filter(relation => relation.name === 'imageLink'); + const targetRelations = this.getTargetRelations().filter((relation) => relation.name === "imageLink"); if (targetRelations.length > 1) { return false; @@ -297,7 +294,7 @@ class FNote { if (referencingNote && referencingNote !== parentNote) { return false; - } else if (parentNote.type !== 'text' || !parentNote.isContentAvailable()) { + } else if (parentNote.type !== "text" || !parentNote.isContentAvailable()) { return false; } @@ -310,9 +307,7 @@ class FNote { * @returns all note's attributes, including inherited ones */ getOwnedAttributes(type?: AttributeType, name?: string) { - const attrs = this.attributes - .map(attributeId => this.froca.attributes[attributeId]) - .filter(Boolean); // filter out nulls; + const attrs = this.attributes.map((attributeId) => this.froca.attributes[attributeId]).filter(Boolean); // filter out nulls; return this.__filterAttrs(attrs, type, name); } @@ -338,26 +333,27 @@ class FNote { if (!(this.noteId in noteAttributeCache.attributes)) { const newPath = [...path, this.noteId]; - const attrArrs = [ this.getOwnedAttributes() ]; + const attrArrs = [this.getOwnedAttributes()]; // inheritable attrs on root are typically not intended to be applied to hidden subtree #3537 - if (this.noteId !== 'root' && this.noteId !== '_hidden') { + if (this.noteId !== "root" && this.noteId !== "_hidden") { for (const parentNote of this.getParentNotes()) { // these virtual parent-child relationships are also loaded into froca - if (parentNote.type !== 'search') { + if (parentNote.type !== "search") { attrArrs.push(parentNote.__getInheritableAttributes(newPath)); } } } - for (const templateAttr of attrArrs.flat().filter(attr => attr.type === 'relation' && ['template', 'inherit'].includes(attr.name))) { + for (const templateAttr of attrArrs.flat().filter((attr) => attr.type === "relation" && ["template", "inherit"].includes(attr.name))) { const templateNote = this.froca.notes[templateAttr.value]; if (templateNote && templateNote.noteId !== this.noteId) { attrArrs.push( - templateNote.__getCachedAttributes(newPath) + templateNote + .__getCachedAttributes(newPath) // template attr is used as a marker for templates, but it's not meant to be inherited - .filter(attr => !(attr.type === 'label' && (attr.name === 'template' || attr.name === 'workspacetemplate'))) + .filter((attr) => !(attr.type === "label" && (attr.name === "template" || attr.name === "workspacetemplate"))) ); } } @@ -378,7 +374,7 @@ class FNote { } isRoot() { - return this.noteId === 'root'; + return this.noteId === "root"; } /** @@ -387,15 +383,16 @@ class FNote { * @returns array of notePaths (each represented by array of noteIds constituting the particular note path) */ getAllNotePaths(): string[][] { - if (this.noteId === 'root') { - return [['root']]; + if (this.noteId === "root") { + return [["root"]]; } - const parentNotes = this.getParentNotes().filter(note => note.type !== 'search'); + const parentNotes = this.getParentNotes().filter((note) => note.type !== "search"); - const notePaths = parentNotes.length === 1 - ? parentNotes[0].getAllNotePaths() // optimization for the most common case - : parentNotes.flatMap(parentNote => parentNote.getAllNotePaths()); + const notePaths = + parentNotes.length === 1 + ? parentNotes[0].getAllNotePaths() // optimization for the most common case + : parentNotes.flatMap((parentNote) => parentNote.getAllNotePaths()); for (const notePath of notePaths) { notePath.push(this.noteId); @@ -404,15 +401,15 @@ class FNote { return notePaths; } - getSortedNotePathRecords(hoistedNoteId = 'root') { - const isHoistedRoot = hoistedNoteId === 'root'; + getSortedNotePathRecords(hoistedNoteId = "root") { + const isHoistedRoot = hoistedNoteId === "root"; - const notePaths = this.getAllNotePaths().map(path => ({ + const notePaths = this.getAllNotePaths().map((path) => ({ notePath: path, isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId), - isArchived: path.some(noteId => froca.notes[noteId].isArchived), - isSearch: path.find(noteId => froca.notes[noteId].type === 'search'), - isHidden: path.includes('_hidden') + isArchived: path.some((noteId) => froca.notes[noteId].isArchived), + isSearch: path.find((noteId) => froca.notes[noteId].type === "search"), + isHidden: path.includes("_hidden") })); notePaths.sort((a, b) => { @@ -438,7 +435,7 @@ class FNote { * @param {string} [hoistedNoteId='root'] * @return {string[]} array of noteIds constituting the particular note path */ - getBestNotePath(hoistedNoteId = 'root') { + getBestNotePath(hoistedNoteId = "root") { return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath; } @@ -448,7 +445,7 @@ class FNote { * @param {string} [hoistedNoteId='root'] * @return {string} serialized note path (e.g. 'root/a1h315/js725h') */ - getBestNotePathString(hoistedNoteId = 'root') { + getBestNotePathString(hoistedNoteId = "root") { const notePath = this.getBestNotePath(hoistedNoteId); return notePath?.join("/"); @@ -458,16 +455,16 @@ class FNote { * @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree */ isHiddenCompletely() { - if (this.noteId === '_hidden') { + if (this.noteId === "_hidden") { return true; - } else if (this.noteId === 'root') { + } else if (this.noteId === "root") { return false; } for (const parentNote of this.getParentNotes()) { - if (parentNote.noteId === 'root') { + if (parentNote.noteId === "root") { return false; - } else if (parentNote.noteId === '_hidden' || parentNote.type === 'search') { + } else if (parentNote.noteId === "_hidden" || parentNote.type === "search") { continue; } @@ -488,11 +485,11 @@ class FNote { if (!type && !name) { return attributes; } else if (type && name) { - return attributes.filter(attr => attr.name === name && attr.type === type); + return attributes.filter((attr) => attr.name === name && attr.type === type); } else if (type) { - return attributes.filter(attr => attr.type === type); + return attributes.filter((attr) => attr.type === type); } else if (name) { - return attributes.filter(attr => attr.name === name); + return attributes.filter((attr) => attr.name === name); } return []; @@ -501,17 +498,17 @@ class FNote { __getInheritableAttributes(path: string[]) { const attrs = this.__getCachedAttributes(path); - return attrs.filter(attr => attr.isInheritable); + return attrs.filter((attr) => attr.isInheritable); } __validateTypeName(type?: string, name?: string) { - if (type && type !== 'label' && type !== 'relation') { + if (type && type !== "label" && type !== "relation") { throw new Error(`Unrecognized attribute type '${type}'. Only 'label' and 'relation' are possible values.`); } if (name) { const firstLetter = name.charAt(0); - if (firstLetter === '#' || firstLetter === '~') { + if (firstLetter === "#" || firstLetter === "~") { throw new Error(`Detect '#' or '~' in the attribute's name. In the API, attribute names should be set without these characters.`); } } @@ -534,33 +531,27 @@ class FNote { } getIcon() { - const iconClassLabels = this.getLabels('iconClass'); + const iconClassLabels = this.getLabels("iconClass"); const workspaceIconClass = this.getWorkspaceIconClass(); if (iconClassLabels && iconClassLabels.length > 0) { return iconClassLabels[0].value; - } - else if (workspaceIconClass) { + } else if (workspaceIconClass) { return workspaceIconClass; - } - else if (this.noteId === 'root') { + } else if (this.noteId === "root") { return "bx bx-home-alt-2"; } - if (this.noteId === '_share') { + if (this.noteId === "_share") { return "bx bx-share-alt"; - } - else if (this.type === 'text') { + } else if (this.type === "text") { if (this.isFolder()) { return "bx bx-folder"; - } - else { + } else { return "bx bx-note"; } - } - else if (this.type === 'code' && this.mime.startsWith('text/x-sql')) { + } else if (this.type === "code" && this.mime.startsWith("text/x-sql")) { return "bx bx-data"; - } - else { + } else { return NOTE_TYPE_ICONS[this.type]; } } @@ -571,8 +562,7 @@ class FNote { } isFolder() { - return this.type === 'search' - || this.getFilteredChildBranches().length > 0; + return this.type === "search" || this.getFilteredChildBranches().length > 0; } getFilteredChildBranches() { @@ -615,7 +605,7 @@ class FNote { hasAttribute(type: AttributeType, name: string) { const attributes = this.getAttributes(); - return attributes.some(attr => attr.name === name && attr.type === type); + return attributes.some((attr) => attr.name === name && attr.type === type); } /** @@ -635,7 +625,7 @@ class FNote { getOwnedAttribute(type: AttributeType, name: string) { const attributes = this.getOwnedAttributes(); - return attributes.find(attr => attr.name === name && attr.type === type); + return attributes.find((attr) => attr.name === name && attr.type === type); } /** @@ -646,7 +636,7 @@ class FNote { getAttribute(type: AttributeType, name: string) { const attributes = this.getAttributes(); - return attributes.find(attr => attr.name === name && attr.type === type); + return attributes.find((attr) => attr.name === name && attr.type === type); } /** @@ -683,7 +673,9 @@ class FNote { * @param name - label name * @returns true if label exists (including inherited) */ - hasLabel(name: string) { return this.hasAttribute(LABEL, name); } + hasLabel(name: string) { + return this.hasAttribute(LABEL, name); + } /** * @param name - label name @@ -696,68 +688,88 @@ class FNote { return false; } - return label && label.value !== 'false'; + return label && label.value !== "false"; } /** * @param name - relation name * @returns true if relation exists (excluding inherited) */ - hasOwnedRelation(name: string) { return this.hasOwnedAttribute(RELATION, name); } + hasOwnedRelation(name: string) { + return this.hasOwnedAttribute(RELATION, name); + } /** * @param name - relation name * @returns true if relation exists (including inherited) */ - hasRelation(name: string) { return this.hasAttribute(RELATION, name); } + hasRelation(name: string) { + return this.hasAttribute(RELATION, name); + } /** * @param name - label name * @returns label if it exists, null otherwise */ - getOwnedLabel(name: string) { return this.getOwnedAttribute(LABEL, name); } + getOwnedLabel(name: string) { + return this.getOwnedAttribute(LABEL, name); + } /** * @param name - label name * @returns label if it exists, null otherwise */ - getLabel(name: string) { return this.getAttribute(LABEL, name); } + getLabel(name: string) { + return this.getAttribute(LABEL, name); + } /** * @param name - relation name * @returns relation if it exists, null otherwise */ - getOwnedRelation(name: string) { return this.getOwnedAttribute(RELATION, name); } + getOwnedRelation(name: string) { + return this.getOwnedAttribute(RELATION, name); + } /** * @param name - relation name * @returns relation if it exists, null otherwise */ - getRelation(name: string) { return this.getAttribute(RELATION, name); } + getRelation(name: string) { + return this.getAttribute(RELATION, name); + } /** * @param name - label name * @returns label value if label exists, null otherwise */ - getOwnedLabelValue(name: string) { return this.getOwnedAttributeValue(LABEL, name); } + getOwnedLabelValue(name: string) { + return this.getOwnedAttributeValue(LABEL, name); + } /** * @param name - label name * @returns label value if label exists, null otherwise */ - getLabelValue(name: string) { return this.getAttributeValue(LABEL, name); } + getLabelValue(name: string) { + return this.getAttributeValue(LABEL, name); + } /** * @param name - relation name * @returns relation value if relation exists, null otherwise */ - getOwnedRelationValue(name: string) { return this.getOwnedAttributeValue(RELATION, name); } + getOwnedRelationValue(name: string) { + return this.getOwnedAttributeValue(RELATION, name); + } /** * @param name - relation name * @returns relation value if relation exists, null otherwise */ - getRelationValue(name: string) { return this.getAttributeValue(RELATION, name); } + getRelationValue(name: string) { + return this.getAttributeValue(RELATION, name); + } /** * @param name @@ -784,22 +796,19 @@ class FNote { } getNotesToInheritAttributesFrom() { - const relations = [ - ...this.getRelations('template'), - ...this.getRelations('inherit') - ]; + const relations = [...this.getRelations("template"), ...this.getRelations("inherit")]; - return relations.map(rel => this.froca.notes[rel.value]); + return relations.map((rel) => this.froca.notes[rel.value]); } getPromotedDefinitionAttributes() { - if (this.isLabelTruthy('hidePromotedAttributes')) { + if (this.isLabelTruthy("hidePromotedAttributes")) { return []; } const promotedAttrs = this.getAttributes() - .filter(attr => attr.isDefinition()) - .filter(attr => { + .filter((attr) => attr.isDefinition()) + .filter((attr) => { const def = attr.getDefinition(); return def && def.isPromoted; @@ -850,7 +859,7 @@ class FNote { } isInHiddenSubtree() { - return this.noteId === '_hidden' || this.hasAncestor('_hidden'); + return this.noteId === "_hidden" || this.hasAncestor("_hidden"); } /** @@ -862,8 +871,7 @@ class FNote { * Get relations which target this note */ getTargetRelations() { - return this.targetRelations - .map(attributeId => this.froca.attributes[attributeId]); + return this.targetRelations.map((attributeId) => this.froca.attributes[attributeId]); } /** @@ -872,7 +880,7 @@ class FNote { async getTargetRelationSourceNotes() { const targetRelations = this.getTargetRelations(); - return await this.froca.getNotes(targetRelations.map(tr => tr.noteId)); + return await this.froca.getNotes(targetRelations.map((tr) => tr.noteId)); } /** @@ -883,7 +891,7 @@ class FNote { } async getBlob() { - return await this.froca.getBlob('notes', this.noteId); + return await this.froca.getBlob("notes", this.noteId); } toString() { @@ -898,26 +906,26 @@ class FNote { } getCssClass() { - const labels = this.getLabels('cssClass'); - return labels.map(l => l.value).join(' '); + const labels = this.getLabels("cssClass"); + return labels.map((l) => l.value).join(" "); } getWorkspaceIconClass() { - const labels = this.getLabels('workspaceIconClass'); + const labels = this.getLabels("workspaceIconClass"); return labels.length > 0 ? labels[0].value : ""; } getWorkspaceTabBackgroundColor() { - const labels = this.getLabels('workspaceTabBackgroundColor'); + const labels = this.getLabels("workspaceTabBackgroundColor"); return labels.length > 0 ? labels[0].value : ""; } /** @returns true if this note is JavaScript (code or file) */ isJavaScript() { - return (this.type === "code" || this.type === "file" || this.type === 'launcher') - && (this.mime.startsWith("application/javascript") - || this.mime === "application/x-javascript" - || this.mime === "text/javascript"); + return ( + (this.type === "code" || this.type === "file" || this.type === "launcher") && + (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript" || this.mime === "text/javascript") + ); } /** @returns true if this note is HTML */ @@ -927,15 +935,15 @@ class FNote { /** @returns JS script environment - either "frontend" or "backend" */ getScriptEnv() { - if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) { + if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith("env=frontend"))) { return "frontend"; } - if (this.type === 'render') { + if (this.type === "render") { return "frontend"; } - if (this.isJavaScript() && this.mime.endsWith('env=backend')) { + if (this.isJavaScript() && this.mime.endsWith("env=backend")) { return "backend"; } @@ -961,17 +969,17 @@ class FNote { isShared() { for (const parentNoteId of this.parents) { - if (parentNoteId === 'root' || parentNoteId === 'none') { + if (parentNoteId === "root" || parentNoteId === "none") { continue; } const parentNote = froca.notes[parentNoteId]; - if (!parentNote || parentNote.type === 'search') { + if (!parentNote || parentNote.type === "search") { continue; } - if (parentNote.noteId === '_share' || parentNote.isShared()) { + if (parentNote.noteId === "_share" || parentNote.isShared()) { return true; } } @@ -980,11 +988,11 @@ class FNote { } isContentAvailable() { - return !this.isProtected || protectedSessionHolder.isProtectedSessionAvailable() + return !this.isProtected || protectedSessionHolder.isProtectedSessionAvailable(); } isLaunchBarConfig() { - return this.type === 'launcher' || utils.isLaunchBarConfig(this.noteId); + return this.type === "launcher" || utils.isLaunchBarConfig(this.noteId); } isOptions() { diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index 1a0dbdc84..a50dace76 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -94,139 +94,147 @@ export default class DesktopLayout { getRootWidget(appContext) { appContext.noteTreeWidget = new NoteTreeWidget(); - const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal"); + const launcherPaneIsHorizontal = options.get("layoutOrientation") === "horizontal"; const launcherPane = this.#buildLauncherPane(launcherPaneIsHorizontal); - const isElectron = (utils.isElectron()); - const isMac = (window.glob.platform === "darwin"); - const isWindows = (window.glob.platform === "win32"); - const hasNativeTitleBar = (window.glob.hasNativeTitleBar); + const isElectron = utils.isElectron(); + const isMac = window.glob.platform === "darwin"; + const isWindows = window.glob.platform === "win32"; + const hasNativeTitleBar = window.glob.hasNativeTitleBar; /** * If true, the tab bar is displayed above the launcher pane with full width; if false (default), the tab bar is displayed in the rest pane. * On macOS we need to force the full-width tab bar on Electron in order to allow the semaphore (window controls) enough space. */ - const fullWidthTabBar = (launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac)); - const customTitleBarButtons = (!hasNativeTitleBar && !isMac && !isWindows); + const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac); + const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows; return new RootContainer(true) .setParent(appContext) .class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout") - .optChild(fullWidthTabBar, new FlexContainer('row') - .class("tab-row-container") - .child(new FlexContainer( "row").id("tab-row-left-spacer")) - .optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true)) - .child(new TabRowWidget().class("full-width")) - .optChild(customTitleBarButtons, new TitleBarButtonsWidget()) - .css('height', '40px') - .css('background-color', 'var(--launcher-pane-background-color)') - .setParent(appContext) + .optChild( + fullWidthTabBar, + new FlexContainer("row") + .class("tab-row-container") + .child(new FlexContainer("row").id("tab-row-left-spacer")) + .optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true)) + .child(new TabRowWidget().class("full-width")) + .optChild(customTitleBarButtons, new TitleBarButtonsWidget()) + .css("height", "40px") + .css("background-color", "var(--launcher-pane-background-color)") + .setParent(appContext) ) .optChild(launcherPaneIsHorizontal, launcherPane) - .child(new FlexContainer('row') - .css("flex-grow", "1") - .id("horizontal-main-container") - .optChild(!launcherPaneIsHorizontal, launcherPane) - .child(new LeftPaneContainer() - .optChild(!launcherPaneIsHorizontal, new QuickSearchWidget()) - .child(appContext.noteTreeWidget) - .child(...this.customWidgets.get('left-pane')) - ) - .child(new FlexContainer('column') - .id('rest-pane') + .child( + new FlexContainer("row") .css("flex-grow", "1") - .optChild(!fullWidthTabBar, new FlexContainer('row') - .child(new TabRowWidget()) - .optChild(customTitleBarButtons, new TitleBarButtonsWidget()) - .css('height', '40px') + .id("horizontal-main-container") + .optChild(!launcherPaneIsHorizontal, launcherPane) + .child( + new LeftPaneContainer() + .optChild(!launcherPaneIsHorizontal, new QuickSearchWidget()) + .child(appContext.noteTreeWidget) + .child(...this.customWidgets.get("left-pane")) ) - .child(new FlexContainer('row') - .filling() - .collapsible() - .id("vertical-main-container") - .child(new FlexContainer('column') - .filling() - .collapsible() - .id('center-pane') - .child(new SplitNoteContainer(() => - new NoteWrapperWidget() - .child(new FlexContainer('row').class('title-row') - .css("height", "50px") - .css("min-height", "50px") - .css('align-items', "center") - .cssBlock('.title-row > * { margin: 5px; }') - .child(new NoteIconWidget()) - .child(new NoteTitleWidget()) - .child(new SpacerWidget(0, 1)) - .child(new MovePaneButton(true)) - .child(new MovePaneButton(false)) - .child(new ClosePaneButton()) - .child(new CreatePaneButton()) - ) - .child( - new RibbonContainer() - // the order of the widgets matter. Some of these want to "activate" themselves - // when visible. When this happens to multiple of them, the first one "wins". - // promoted attributes should always win. - .ribbon(new ClassicEditorToolbar()) - .ribbon(new ScriptExecutorWidget()) - .ribbon(new SearchDefinitionWidget()) - .ribbon(new EditedNotesWidget()) - .ribbon(new BookPropertiesWidget()) - .ribbon(new NotePropertiesWidget()) - .ribbon(new FilePropertiesWidget()) - .ribbon(new ImagePropertiesWidget()) - .ribbon(new BasicPropertiesWidget()) - .ribbon(new OwnedAttributeListWidget()) - .ribbon(new InheritedAttributesWidget()) - .ribbon(new NotePathsWidget()) - .ribbon(new NoteMapRibbonWidget()) - .ribbon(new SimilarNotesWidget()) - .ribbon(new NoteInfoWidget()) - .button(new RevisionsButton()) - .button(new NoteActionsWidget()) - ) - .child(new SharedInfoWidget()) - .child(new WatchedFileUpdateStatusWidget()) - .child(new FloatingButtons() - .child(new EditButton()) - .child(new ShowTocWidgetButton()) - .child(new ShowHighlightsListWidgetButton()) - .child(new CodeButtonsWidget()) - .child(new RelationMapButtons()) - .child(new CopyImageReferenceButton()) - .child(new SvgExportButton()) - .child(new BacklinksWidget()) - .child(new HideFloatingButtonsButton()) - ) - .child(new MermaidWidget()) - .child( - new ScrollingContainer() - .filling() - .child(new PromotedAttributesWidget()) - .child(new SqlTableSchemasWidget()) - .child(new NoteDetailWidget()) - .child(new NoteListWidget()) - .child(new SearchResultWidget()) - .child(new SqlResultWidget()) - .child(new ScrollPaddingWidget()) - ) - .child(new ApiLogWidget()) - .child(new FindWidget()) - .child( - ...this.customWidgets.get('node-detail-pane'), // typo, let's keep it for a while as BC - ...this.customWidgets.get('note-detail-pane') - ) - ) + .child( + new FlexContainer("column") + .id("rest-pane") + .css("flex-grow", "1") + .optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, new TitleBarButtonsWidget()).css("height", "40px")) + .child( + new FlexContainer("row") + .filling() + .collapsible() + .id("vertical-main-container") + .child( + new FlexContainer("column") + .filling() + .collapsible() + .id("center-pane") + .child( + new SplitNoteContainer(() => + new NoteWrapperWidget() + .child( + new FlexContainer("row") + .class("title-row") + .css("height", "50px") + .css("min-height", "50px") + .css("align-items", "center") + .cssBlock(".title-row > * { margin: 5px; }") + .child(new NoteIconWidget()) + .child(new NoteTitleWidget()) + .child(new SpacerWidget(0, 1)) + .child(new MovePaneButton(true)) + .child(new MovePaneButton(false)) + .child(new ClosePaneButton()) + .child(new CreatePaneButton()) + ) + .child( + new RibbonContainer() + // the order of the widgets matter. Some of these want to "activate" themselves + // when visible. When this happens to multiple of them, the first one "wins". + // promoted attributes should always win. + .ribbon(new ClassicEditorToolbar()) + .ribbon(new ScriptExecutorWidget()) + .ribbon(new SearchDefinitionWidget()) + .ribbon(new EditedNotesWidget()) + .ribbon(new BookPropertiesWidget()) + .ribbon(new NotePropertiesWidget()) + .ribbon(new FilePropertiesWidget()) + .ribbon(new ImagePropertiesWidget()) + .ribbon(new BasicPropertiesWidget()) + .ribbon(new OwnedAttributeListWidget()) + .ribbon(new InheritedAttributesWidget()) + .ribbon(new NotePathsWidget()) + .ribbon(new NoteMapRibbonWidget()) + .ribbon(new SimilarNotesWidget()) + .ribbon(new NoteInfoWidget()) + .button(new RevisionsButton()) + .button(new NoteActionsWidget()) + ) + .child(new SharedInfoWidget()) + .child(new WatchedFileUpdateStatusWidget()) + .child( + new FloatingButtons() + .child(new EditButton()) + .child(new ShowTocWidgetButton()) + .child(new ShowHighlightsListWidgetButton()) + .child(new CodeButtonsWidget()) + .child(new RelationMapButtons()) + .child(new CopyImageReferenceButton()) + .child(new SvgExportButton()) + .child(new BacklinksWidget()) + .child(new HideFloatingButtonsButton()) + ) + .child(new MermaidWidget()) + .child( + new ScrollingContainer() + .filling() + .child(new PromotedAttributesWidget()) + .child(new SqlTableSchemasWidget()) + .child(new NoteDetailWidget()) + .child(new NoteListWidget()) + .child(new SearchResultWidget()) + .child(new SqlResultWidget()) + .child(new ScrollPaddingWidget()) + ) + .child(new ApiLogWidget()) + .child(new FindWidget()) + .child( + ...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC + ...this.customWidgets.get("note-detail-pane") + ) + ) + ) + .child(...this.customWidgets.get("center-pane")) + ) + .child( + new RightPaneContainer() + .child(new TocWidget()) + .child(new HighlightsListWidget()) + .child(...this.customWidgets.get("right-pane")) + ) ) - .child(...this.customWidgets.get('center-pane')) - ) - .child(new RightPaneContainer() - .child(new TocWidget()) - .child(new HighlightsListWidget()) - .child(...this.customWidgets.get('right-pane')) - ) ) - ) ) .child(new BulkActionsDialog()) .child(new AboutDialog()) @@ -254,14 +262,10 @@ export default class DesktopLayout { } #buildLauncherPane(isHorizontal) { - let launcherPane; + let launcherPane; if (isHorizontal) { - launcherPane = new FlexContainer("row") - .css("height", "53px") - .class("horizontal") - .child(new LauncherContainer(true)) - .child(new GlobalMenuWidget(true)) + launcherPane = new FlexContainer("row").css("height", "53px").class("horizontal").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)); } else { launcherPane = new FlexContainer("column") .css("width", "53px") diff --git a/src/public/app/layouts/mobile_layout.ts b/src/public/app/layouts/mobile_layout.ts index af1e8120b..0f31199fd 100644 --- a/src/public/app/layouts/mobile_layout.ts +++ b/src/public/app/layouts/mobile_layout.ts @@ -122,81 +122,71 @@ export default class MobileLayout { .setParent(appContext) .class("horizontal-layout") .cssBlock(MOBILE_CSS) - .child(new FlexContainer("column") - .id("mobile-sidebar-container") - ) - .child(new FlexContainer("row") - .filling() - .id("mobile-rest-container") - .child(new SidebarContainer("tree", 'column') - .class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-3 col-xl-3") - .id("mobile-sidebar-wrapper") - .css("max-height", "100%") - .css('padding-left', "0") - .css('padding-right', "0") - .css('contain', 'content') - .child(new FlexContainer("column") - .filling() - .id("mobile-sidebar-wrapper") - .child(new QuickSearchWidget()) - .child(new NoteTreeWidget() - .cssBlock(FANCYTREE_CSS)))) - .child(new ScreenContainer("detail", "column") - .id("detail-container") - .class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9") - .css("padding-left", "0") - .css("padding-right", "0") - .css('max-height', '100%') - .css("position", "relative") - .child(new FlexContainer('row').contentSized() - .css('font-size', 'larger') - .css('align-items', 'center') - .child(new ToggleSidebarButtonWidget().contentSized()) - .child(new NoteTitleWidget() - .contentSized() - .css("position", "relative") - .css("top", "5px") - .css("padding-left", "0.5em")) - .child(new MobileDetailMenuWidget(true).contentSized()) - ) - .child(new SharedInfoWidget()) - .child(new FloatingButtons() - .child(new EditButton()) - .child(new RelationMapButtons()) - .child(new SvgExportButton()) - .child(new BacklinksWidget()) - .child(new HideFloatingButtonsButton()) - ) - .child(new MermaidWidget()) - .child(new PromotedAttributesWidget()) + .child(new FlexContainer("column").id("mobile-sidebar-container")) + .child( + new FlexContainer("row") + .filling() + .id("mobile-rest-container") .child( - new ScrollingContainer() - .filling() - .contentSized() - .child( - new NoteDetailWidget() - .css('padding', '5px 0 10px 0') - ) - .child(new NoteListWidget()) - .child(new FilePropertiesWidget().css('font-size','smaller')) + new SidebarContainer("tree", "column") + .class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-3 col-xl-3") + .id("mobile-sidebar-wrapper") + .css("max-height", "100%") + .css("padding-left", "0") + .css("padding-right", "0") + .css("contain", "content") + .child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS))) ) - ) - .child(new ProtectedSessionPasswordDialog()) - .child(new ConfirmDialog()) + .child( + new ScreenContainer("detail", "column") + .id("detail-container") + .class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9") + .css("padding-left", "0") + .css("padding-right", "0") + .css("max-height", "100%") + .css("position", "relative") + .child( + new FlexContainer("row") + .contentSized() + .css("font-size", "larger") + .css("align-items", "center") + .child(new ToggleSidebarButtonWidget().contentSized()) + .child(new NoteTitleWidget().contentSized().css("position", "relative").css("top", "5px").css("padding-left", "0.5em")) + .child(new MobileDetailMenuWidget(true).contentSized()) + ) + .child(new SharedInfoWidget()) + .child( + new FloatingButtons() + .child(new EditButton()) + .child(new RelationMapButtons()) + .child(new SvgExportButton()) + .child(new BacklinksWidget()) + .child(new HideFloatingButtonsButton()) + ) + .child(new MermaidWidget()) + .child(new PromotedAttributesWidget()) + .child( + new ScrollingContainer() + .filling() + .contentSized() + .child(new NoteDetailWidget().css("padding", "5px 0 10px 0")) + .child(new NoteListWidget()) + .child(new FilePropertiesWidget().css("font-size", "smaller")) + ) + ) + .child(new ProtectedSessionPasswordDialog()) + .child(new ConfirmDialog()) + ) + .child( + new FlexContainer("column") + .contentSized() + .id("mobile-bottom-bar") + .child(new TabRowWidget().css("height", "40px")) + .child(new FlexContainer("row").class("horizontal").css("height", "53px").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)).id("launcher-pane")) ) - .child(new FlexContainer("column") - .contentSized() - .id("mobile-bottom-bar") - .child(new TabRowWidget().css('height', '40px')) - .child(new FlexContainer("row") - .class("horizontal") - .css("height", "53px") - .child(new LauncherContainer(true)) - .child(new GlobalMenuWidget(true)) - .id("launcher-pane"))) .child(new ClassicEditorToolbar()) .child(new AboutDialog()) .child(new HelpDialog()) - .child(new JumpToNoteDialog()) + .child(new JumpToNoteDialog()); } } diff --git a/src/public/app/menus/context_menu.ts b/src/public/app/menus/context_menu.ts index 3663c5648..25e45e38a 100644 --- a/src/public/app/menus/context_menu.ts +++ b/src/public/app/menus/context_menu.ts @@ -1,6 +1,6 @@ -import { CommandNames } from '../components/app_context.js'; -import keyboardActionService from '../services/keyboard_actions.js'; -import utils from '../services/utils.js'; +import { CommandNames } from "../components/app_context.js"; +import keyboardActionService from "../services/keyboard_actions.js"; +import utils from "../services/utils.js"; interface ContextMenuOptions { x: number; @@ -11,7 +11,7 @@ interface ContextMenuOptions { } interface MenuSeparatorItem { - title: "----" + title: "----"; } export interface MenuCommandItem { @@ -31,7 +31,6 @@ export type MenuItem = MenuCommandItem | MenuSeparato export type MenuHandler = (item: MenuCommandItem, e: JQuery.MouseDownEvent) => void; class ContextMenu { - private $widget: JQuery; private $cover: JQuery; private dateContextMenuOpenedMs: number; @@ -48,7 +47,7 @@ class ContextMenu { if (this.isMobile) { this.$cover.on("click", () => this.hide()); } else { - $(document).on('click', (e) => this.hide()); + $(document).on("click", (e) => this.hide()); } } @@ -102,7 +101,7 @@ class ContextMenu { top = this.options.y - CONTEXT_MENU_OFFSET; } - if (this.options.orientation === 'left' && contextMenuWidth) { + if (this.options.orientation === "left" && contextMenuWidth) { if (this.options.x + CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) { // Overflow: right left = clientWidth - contextMenuWidth - CONTEXT_MENU_OFFSET; @@ -124,11 +123,13 @@ class ContextMenu { } } - this.$widget.css({ - display: "block", - top: top, - left: left - }).addClass("show"); + this.$widget + .css({ + display: "block", + top: top, + left: left + }) + .addClass("show"); } addItems($parent: JQuery, items: MenuItem[]) { @@ -137,7 +138,7 @@ class ContextMenu { continue; } - if (item.title === '----') { + if (item.title === "----") { $parent.append($("
").addClass("dropdown-divider")); } else { const $icon = $(""); @@ -160,23 +161,22 @@ class ContextMenu { const $item = $("
  • ") .addClass("dropdown-item") .append($link) - .on('contextmenu', e => false) + .on("contextmenu", (e) => false) // important to use mousedown instead of click since the former does not change focus // (especially important for focused text for spell check) - .on('mousedown', e => { + .on("mousedown", (e) => { e.stopPropagation(); - if (e.which !== 1) { // only left click triggers menu items + if (e.which !== 1) { + // only left click triggers menu items return false; } if (this.isMobile && "items" in item && item.items) { - const $item = $(e.target) - .closest(".dropdown-item"); + const $item = $(e.target).closest(".dropdown-item"); $item.toggleClass("submenu-open"); - $item.find("ul.dropdown-menu") - .toggleClass("show"); + $item.find("ul.dropdown-menu").toggleClass("show"); return false; } diff --git a/src/public/app/menus/electron_context_menu.ts b/src/public/app/menus/electron_context_menu.ts index c8bbb0a41..43a504d6d 100644 --- a/src/public/app/menus/electron_context_menu.ts +++ b/src/public/app/menus/electron_context_menu.ts @@ -7,17 +7,17 @@ import type { BrowserWindow } from "electron"; import { CommandNames } from "../components/app_context.js"; function setupContextMenu() { - const electron = utils.dynamicRequire('electron'); + const electron = utils.dynamicRequire("electron"); - const remote = utils.dynamicRequire('@electron/remote'); + const remote = utils.dynamicRequire("@electron/remote"); // FIXME: Remove typecast once Electron is properly integrated. - const {webContents} = remote.getCurrentWindow() as BrowserWindow; + const { webContents } = remote.getCurrentWindow() as BrowserWindow; - webContents.on('context-menu', (event, params) => { - const {editFlags} = params; + webContents.on("context-menu", (event, params) => { + const { editFlags } = params; const hasText = params.selectionText.trim().length > 0; const isMac = process.platform === "darwin"; - const platformModifier = isMac ? 'Meta' : 'Ctrl'; + const platformModifier = isMac ? "Meta" : "Ctrl"; const items: MenuItem[] = []; @@ -60,7 +60,7 @@ function setupContextMenu() { }); } - if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') { + if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === "none") { items.push({ title: t("electron_context_menu.copy-link"), uiIcon: "bx bx-copy", @@ -94,9 +94,7 @@ function setupContextMenu() { } if (hasText) { - const shortenedSelection = params.selectionText.length > 15 - ? (`${params.selectionText.substr(0, 13)}…`) - : params.selectionText; + const shortenedSelection = params.selectionText.length > 15 ? `${params.selectionText.substr(0, 13)}…` : params.selectionText; // Read the search engine from the options and fallback to DuckDuckGo if the option is not set. const customSearchEngineName = options.get("customSearchEngineName"); @@ -114,7 +112,7 @@ function setupContextMenu() { // Replace the placeholder with the real search keyword. let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText)); - items.push({title: "----"}); + items.push({ title: "----" }); items.push({ enabled: editFlags.canPaste, @@ -134,8 +132,8 @@ function setupContextMenu() { x: params.x / zoomLevel, y: params.y / zoomLevel, items, - selectMenuItemHandler: ({command, spellingSuggestion}) => { - if (command === 'replaceMisspelling' && spellingSuggestion) { + selectMenuItemHandler: ({ command, spellingSuggestion }) => { + if (command === "replaceMisspelling" && spellingSuggestion) { webContents.insertText(spellingSuggestion); } } diff --git a/src/public/app/menus/image_context_menu.ts b/src/public/app/menus/image_context_menu.ts index b0666ea84..54d7abcf6 100644 --- a/src/public/app/menus/image_context_menu.ts +++ b/src/public/app/menus/image_context_menu.ts @@ -1,4 +1,4 @@ -import { t } from '../services/i18n.js'; +import { t } from "../services/i18n.js"; import utils from "../services/utils.js"; import contextMenu from "./context_menu.js"; import imageService from "../services/image.js"; @@ -11,7 +11,7 @@ function setupContextMenu($image: JQuery) { } $image.prop(PROP_NAME, true); - $image.on('contextmenu', e => { + $image.on("contextmenu", (e) => { e.preventDefault(); contextMenu.show({ @@ -27,17 +27,17 @@ function setupContextMenu($image: JQuery) { title: t("image_context_menu.copy_image_to_clipboard"), command: "copyImageToClipboard", uiIcon: "bx bx-copy" - }, + } ], selectMenuItemHandler: async ({ command }) => { - if (command === 'copyImageReferenceToClipboard') { + if (command === "copyImageReferenceToClipboard") { imageService.copyImageReferenceToClipboard($image); - } else if (command === 'copyImageToClipboard') { + } else if (command === "copyImageToClipboard") { try { - const nativeImage = utils.dynamicRequire('electron').nativeImage; - const clipboard = utils.dynamicRequire('electron').clipboard; + const nativeImage = utils.dynamicRequire("electron").nativeImage; + const clipboard = utils.dynamicRequire("electron").clipboard; - const src = $image.attr('src'); + const src = $image.attr("src"); if (!src) { console.error("Missing src"); return; @@ -46,15 +46,9 @@ function setupContextMenu($image: JQuery) { const response = await fetch(src); const blob = await response.blob(); - clipboard.writeImage( - nativeImage.createFromBuffer( - Buffer.from( - await blob.arrayBuffer() - ) - ) - ); + clipboard.writeImage(nativeImage.createFromBuffer(Buffer.from(await blob.arrayBuffer()))); } catch (error) { - console.error('Failed to copy image to clipboard:', error); + console.error("Failed to copy image to clipboard:", error); } } else { throw new Error(`Unrecognized command '${command}'`); diff --git a/src/public/app/menus/launcher_context_menu.ts b/src/public/app/menus/launcher_context_menu.ts index 0c4aaca6c..da69cb3a9 100644 --- a/src/public/app/menus/launcher_context_menu.ts +++ b/src/public/app/menus/launcher_context_menu.ts @@ -1,17 +1,16 @@ -import treeService, { Node } from '../services/tree.js'; +import treeService, { Node } from "../services/tree.js"; import froca from "../services/froca.js"; import contextMenu, { MenuCommandItem, MenuItem } from "./context_menu.js"; import dialogService from "../services/dialog.js"; import server from "../services/server.js"; -import { t } from '../services/i18n.js'; -import type { SelectMenuItemEventListener } from '../components/events.js'; -import NoteTreeWidget from '../widgets/note_tree.js'; -import { FilteredCommandNames, ContextMenuCommandData } from '../components/app_context.js'; +import { t } from "../services/i18n.js"; +import type { SelectMenuItemEventListener } from "../components/events.js"; +import NoteTreeWidget from "../widgets/note_tree.js"; +import { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js"; type LauncherCommandNames = FilteredCommandNames; export default class LauncherContextMenu implements SelectMenuItemEventListener { - private treeWidget: NoteTreeWidget; private node: Node; @@ -26,48 +25,48 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener< y: e.pageY, items: await this.getMenuItems(), selectMenuItemHandler: (item, e) => this.selectMenuItemHandler(item) - }) + }); } async getMenuItems(): Promise[]> { const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null; const parentNoteId = this.node.getParent().data.noteId; - const isVisibleRoot = note?.noteId === '_lbVisibleLaunchers'; - const isAvailableRoot = note?.noteId === '_lbAvailableLaunchers'; - const isVisibleItem = parentNoteId === '_lbVisibleLaunchers'; - const isAvailableItem = parentNoteId === '_lbAvailableLaunchers'; + const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers"; + const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers"; + const isVisibleItem = parentNoteId === "_lbVisibleLaunchers"; + const isAvailableItem = parentNoteId === "_lbAvailableLaunchers"; const isItem = isVisibleItem || isAvailableItem; const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted const canBeReset = !canBeDeleted && note?.isLaunchBarConfig(); const items: (MenuItem | null)[] = [ - (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-note-launcher"), command: 'addNoteLauncher', uiIcon: "bx bx-note" } : null, - (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-script-launcher"), command: 'addScriptLauncher', uiIcon: "bx bx-code-curly" } : null, - (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-custom-widget"), command: 'addWidgetLauncher', uiIcon: "bx bx-customize" } : null, - (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-spacer"), command: 'addSpacerLauncher', uiIcon: "bx bx-dots-horizontal" } : null, - (isVisibleRoot || isAvailableRoot) ? { title: "----" } : null, + isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-note-launcher"), command: "addNoteLauncher", uiIcon: "bx bx-note" } : null, + isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-script-launcher"), command: "addScriptLauncher", uiIcon: "bx bx-code-curly" } : null, + isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-custom-widget"), command: "addWidgetLauncher", uiIcon: "bx bx-customize" } : null, + isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-spacer"), command: "addSpacerLauncher", uiIcon: "bx bx-dots-horizontal" } : null, + isVisibleRoot || isAvailableRoot ? { title: "----" } : null, isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null, isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null, - (isVisibleItem || isAvailableItem) ? { title: "----" } : null, + isVisibleItem || isAvailableItem ? { title: "----" } : null, { title: `${t("launcher_context_menu.duplicate-launcher")}`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: isItem }, { title: `${t("launcher_context_menu.delete")}`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon", enabled: canBeDeleted }, { title: "----" }, - { title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset} + { title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset } ]; - return items.filter(row => row !== null); + return items.filter((row) => row !== null); } - async selectMenuItemHandler({command}: MenuCommandItem) { + async selectMenuItemHandler({ command }: MenuCommandItem) { if (!command) { return; } - if (command === 'resetLauncher') { + if (command === "resetLauncher") { const confirmed = await dialogService.confirm(t("launcher_context_menu.reset_launcher_confirm", { title: this.node.title })); if (confirmed) { diff --git a/src/public/app/menus/link_context_menu.ts b/src/public/app/menus/link_context_menu.ts index 1c0993f76..e1ecef06d 100644 --- a/src/public/app/menus/link_context_menu.ts +++ b/src/public/app/menus/link_context_menu.ts @@ -8,26 +8,24 @@ function openContextMenu(notePath: string, e: PointerEvent, viewScope: ViewScope x: e.pageX, y: e.pageY, items: [ - {title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external"}, - {title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right"}, - {title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open"} + { title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" }, + { title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" }, + { title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" } ], - selectMenuItemHandler: ({command}) => { + selectMenuItemHandler: ({ command }) => { if (!hoistedNoteId) { hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId; } - if (command === 'openNoteInNewTab') { + if (command === "openNoteInNewTab") { appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope }); - } - else if (command === 'openNoteInNewSplit') { + } else if (command === "openNoteInNewSplit") { const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); - const {ntxId} = subContexts[subContexts.length - 1]; + const { ntxId } = subContexts[subContexts.length - 1]; - appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath, hoistedNoteId, viewScope}); - } - else if (command === 'openNoteInNewWindow') { - appContext.triggerCommand('openInWindow', {notePath, hoistedNoteId, viewScope}); + appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope }); + } else if (command === "openNoteInNewWindow") { + appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); } } }); @@ -35,4 +33,4 @@ function openContextMenu(notePath: string, e: PointerEvent, viewScope: ViewScope export default { openContextMenu -} +}; diff --git a/src/public/app/menus/tree_context_menu.ts b/src/public/app/menus/tree_context_menu.ts index ada4bec21..15e125ffe 100644 --- a/src/public/app/menus/tree_context_menu.ts +++ b/src/public/app/menus/tree_context_menu.ts @@ -1,6 +1,6 @@ -import treeService, { Node } from '../services/tree.js'; +import treeService, { Node } from "../services/tree.js"; import froca from "../services/froca.js"; -import clipboard from '../services/clipboard.js'; +import clipboard from "../services/clipboard.js"; import noteCreateService from "../services/note_create.js"; import contextMenu, { MenuCommandItem, MenuItem } from "./context_menu.js"; import appContext, { ContextMenuCommandData, FilteredCommandNames } from "../components/app_context.js"; @@ -9,9 +9,9 @@ import server from "../services/server.js"; import toastService from "../services/toast.js"; import dialogService from "../services/dialog.js"; import { t } from "../services/i18n.js"; -import NoteTreeWidget from '../widgets/note_tree.js'; -import FAttachment from '../entities/fattachment.js'; -import { SelectMenuItemEventListener } from '../components/events.js'; +import NoteTreeWidget from "../widgets/note_tree.js"; +import FAttachment from "../entities/fattachment.js"; +import { SelectMenuItemEventListener } from "../components/events.js"; // TODO: Deduplicate once client/server is well split. interface ConvertToAttachmentResponse { @@ -21,7 +21,6 @@ interface ConvertToAttachmentResponse { type TreeCommandNames = FilteredCommandNames; export default class TreeContextMenu implements SelectMenuItemEventListener { - private treeWidget: NoteTreeWidget; private node: Node; @@ -36,13 +35,13 @@ export default class TreeContextMenu implements SelectMenuItemEventListener this.selectMenuItemHandler(item) - }) + }); } async getMenuItems(): Promise[]> { const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null; const branch = froca.getBranch(this.node.data.branchId); - const isNotRoot = note?.noteId !== 'root'; + const isNotRoot = note?.noteId !== "root"; const isHoisted = note?.noteId === appContext.tabManager.getActiveContext().hoistedNoteId; const parentNote = isNotRoot && branch ? await froca.getNote(branch.parentNoteId) : null; @@ -50,12 +49,11 @@ export default class TreeContextMenu implements SelectMenuItemEventListener | null)[] = [ @@ -63,117 +61,161 @@ export default class TreeContextMenu implements SelectMenuItemEventListener`, command: "toggleNoteHoisting", uiIcon: "bx bxs-chevrons-up", enabled: noSelectedNotes && notSearch }, - !isHoisted || !isNotRoot ? null : { title: `${t("tree-context-menu.unhoist-note")} `, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" }, - + isHoisted + ? null + : { + title: `${t("tree-context-menu.hoist-note")} `, + command: "toggleNoteHoisting", + uiIcon: "bx bxs-chevrons-up", + enabled: noSelectedNotes && notSearch + }, + !isHoisted || !isNotRoot + ? null + : { title: `${t("tree-context-menu.unhoist-note")} `, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" }, { title: "----" }, - - { title: `${t("tree-context-menu.insert-note-after")}`, command: "insertNoteAfter", uiIcon: "bx bx-plus", + { + title: `${t("tree-context-menu.insert-note-after")}`, + command: "insertNoteAfter", + uiIcon: "bx bx-plus", items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null, - enabled: insertNoteAfterEnabled && noSelectedNotes && notOptions }, + enabled: insertNoteAfterEnabled && noSelectedNotes && notOptions + }, - { title: `${t("tree-context-menu.insert-child-note")}`, command: "insertChildNote", uiIcon: "bx bx-plus", + { + title: `${t("tree-context-menu.insert-child-note")}`, + command: "insertChildNote", + uiIcon: "bx bx-plus", items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null, - enabled: notSearch && noSelectedNotes && notOptions }, - + enabled: notSearch && noSelectedNotes && notOptions + }, { title: "----" }, - { title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes }, { title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes }, + { title: "----" }, + + { + title: t("tree-context-menu.advanced"), + uiIcon: "bx bxs-wrench", + enabled: true, + items: [ + { title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true }, + + { title: "----" }, + + { + title: `${t("tree-context-menu.edit-branch-prefix")} `, + command: "editBranchPrefix", + uiIcon: "bx bx-rename", + enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions + }, + { title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptions }, + { + title: `${t("tree-context-menu.duplicate-subtree")} `, + command: "duplicateSubtree", + uiIcon: "bx bx-outline", + enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions + }, + + { title: "----" }, + + { title: `${t("tree-context-menu.expand-subtree")} `, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes }, + { title: `${t("tree-context-menu.collapse-subtree")} `, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes }, + { + title: `${t("tree-context-menu.sort-by")} `, + command: "sortChildNotes", + uiIcon: "bx bx-sort-down", + enabled: noSelectedNotes && notSearch + }, + + { title: "----" }, + + { title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true }, + { title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptions } + ] + }, { title: "----" }, + { + title: `${t("tree-context-menu.cut")} `, + command: "cutNotesToClipboard", + uiIcon: "bx bx-cut", + enabled: isNotRoot && !isHoisted && parentNotSearch + }, - { title: t("tree-context-menu.advanced"), uiIcon: "bx bxs-wrench", enabled: true, items: [ - { title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true }, + { title: `${t("tree-context-menu.copy-clone")} `, command: "copyNotesToClipboard", uiIcon: "bx bx-copy", enabled: isNotRoot && !isHoisted }, - { title: "----" }, + { + title: `${t("tree-context-menu.paste-into")} `, + command: "pasteNotesFromClipboard", + uiIcon: "bx bx-paste", + enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes + }, - { title: `${t("tree-context-menu.edit-branch-prefix")} `, command: "editBranchPrefix", uiIcon: "bx bx-rename", enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions }, - { title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptions }, - { title: `${t("tree-context-menu.duplicate-subtree")} `, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions }, + { + title: t("tree-context-menu.paste-after"), + command: "pasteNotesAfterFromClipboard", + uiIcon: "bx bx-paste", + enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes + }, - { title: "----" }, + { + title: `${t("tree-context-menu.move-to")} `, + command: "moveNotesTo", + uiIcon: "bx bx-transfer", + enabled: isNotRoot && !isHoisted && parentNotSearch + }, - { title: `${t("tree-context-menu.expand-subtree")} `, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes }, - { title: `${t("tree-context-menu.collapse-subtree")} `, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes }, - { title: `${t("tree-context-menu.sort-by")} `, command: "sortChildNotes", uiIcon: "bx bx-sort-down", enabled: noSelectedNotes && notSearch }, - - { title: "----" }, - - { title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true }, - { title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptions }, - ] }, + { title: `${t("tree-context-menu.clone-to")} `, command: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted }, + { + title: `${t("tree-context-menu.delete")} `, + command: "deleteNotes", + uiIcon: "bx bx-trash destructive-action-icon", + enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions + }, { title: "----" }, + { title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptions }, - { title: `${t("tree-context-menu.cut")} `, command: "cutNotesToClipboard", uiIcon: "bx bx-cut", - enabled: isNotRoot && !isHoisted && parentNotSearch }, - - { title: `${t("tree-context-menu.copy-clone")} `, command: "copyNotesToClipboard", uiIcon: "bx bx-copy", - enabled: isNotRoot && !isHoisted }, - - { title: `${t("tree-context-menu.paste-into")} `, command: "pasteNotesFromClipboard", uiIcon: "bx bx-paste", - enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes }, - - { title: t("tree-context-menu.paste-after"), command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste", - enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes }, - - { title: `${t("tree-context-menu.move-to")} `, command: "moveNotesTo", uiIcon: "bx bx-transfer", - enabled: isNotRoot && !isHoisted && parentNotSearch }, - - { title: `${t("tree-context-menu.clone-to")} `, command: "cloneNotesTo", uiIcon: "bx bx-duplicate", - enabled: isNotRoot && !isHoisted }, - - { title: `${t("tree-context-menu.delete")} `, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon", - enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions }, + { title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", enabled: notSearch && noSelectedNotes && notOptions }, { title: "----" }, - { title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", - enabled: notSearch && noSelectedNotes && notOptions }, - - { title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", - enabled: notSearch && noSelectedNotes && notOptions }, - - - { title: "----" }, - - - { title: `${t("tree-context-menu.search-in-subtree")} `, command: "searchInSubtree", uiIcon: "bx bx-search", - enabled: notSearch && noSelectedNotes }, - + { + title: `${t("tree-context-menu.search-in-subtree")} `, + command: "searchInSubtree", + uiIcon: "bx bx-search", + enabled: notSearch && noSelectedNotes + } ]; - return items.filter(row => row !== null); + return items.filter((row) => row !== null); } - async selectMenuItemHandler({command, type, templateNoteId}: MenuCommandItem) { + async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem) { const notePath = treeService.getNotePath(this.node); - if (command === 'openInTab') { + if (command === "openInTab") { appContext.tabManager.openTabWithNoteWithHoisting(notePath); - } - else if (command === "insertNoteAfter") { + } else if (command === "insertNoteAfter") { const parentNotePath = treeService.getNotePath(this.node.getParent()); const isProtected = treeService.getParentProtectedStatus(this.node); noteCreateService.createNote(parentNotePath, { - target: 'after', + target: "after", targetBranchId: this.node.data.branchId, type: type, isProtected: isProtected, templateNoteId: templateNoteId }); - } - else if (command === "insertChildNote") { + } else if (command === "insertChildNote") { const parentNotePath = treeService.getNotePath(this.node); noteCreateService.createNote(parentNotePath, { @@ -181,15 +223,13 @@ export default class TreeContextMenu implements SelectMenuItemEventListener(`notes/${note.noteId}/convert-to-attachment`); + const { attachment } = await server.post(`notes/${note.noteId}/convert-to-attachment`); if (attachment) { converted++; @@ -208,11 +248,9 @@ export default class TreeContextMenu implements SelectMenuItemEventListener(command, { node: this.node, notePath: notePath, diff --git a/src/public/app/mobile.js b/src/public/app/mobile.js index 2319d7f9e..6690566d4 100644 --- a/src/public/app/mobile.js +++ b/src/public/app/mobile.js @@ -1,8 +1,8 @@ import appContext from "./components/app_context.js"; -import noteAutocompleteService from './services/note_autocomplete.js'; +import noteAutocompleteService from "./services/note_autocomplete.js"; import glob from "./services/glob.js"; -glob.setupGlobs() +glob.setupGlobs(); await appContext.earlyInit(); diff --git a/src/public/app/services/attribute_autocomplete.ts b/src/public/app/services/attribute_autocomplete.ts index 88ac35ead..2721c13d8 100644 --- a/src/public/app/services/attribute_autocomplete.ts +++ b/src/public/app/services/attribute_autocomplete.ts @@ -15,29 +15,34 @@ interface InitOptions { */ function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions) { if (!$el.hasClass("aa-input")) { - $el.autocomplete({ - appendTo: document.querySelector('body'), - hint: false, - openOnFocus: true, - minLength: 0, - tabAutocomplete: false - }, [{ - displayKey: 'name', - // disabling cache is important here because otherwise cache can stay intact when switching between attribute type which will lead to autocomplete displaying attribute names for incorrect attribute type - cache: false, - source: async (term, cb) => { - const type = typeof attributeType === "function" ? attributeType() : attributeType; + $el.autocomplete( + { + appendTo: document.querySelector("body"), + hint: false, + openOnFocus: true, + minLength: 0, + tabAutocomplete: false + }, + [ + { + displayKey: "name", + // disabling cache is important here because otherwise cache can stay intact when switching between attribute type which will lead to autocomplete displaying attribute names for incorrect attribute type + cache: false, + source: async (term, cb) => { + const type = typeof attributeType === "function" ? attributeType() : attributeType; - const names = await server.get(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`); - const result = names.map(name => ({name})); + const names = await server.get(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`); + const result = names.map((name) => ({ name })); - cb(result); - } - }]); + cb(result); + } + } + ] + ); - $el.on('autocomplete:opened', () => { + $el.on("autocomplete:opened", () => { if ($el.attr("readonly")) { - $el.autocomplete('close'); + $el.autocomplete("close"); } }); } @@ -51,7 +56,7 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio if ($el.hasClass("aa-input")) { // we reinit every time because autocomplete seems to have a bug where it retains state from last // open even though the value was reset - $el.autocomplete('destroy'); + $el.autocomplete("destroy"); } let attributeName = ""; @@ -63,34 +68,38 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio return; } - const attributeValues = (await server.get(`attribute-values/${encodeURIComponent(attributeName)}`)) - .map(attribute => ({ value: attribute })); + const attributeValues = (await server.get(`attribute-values/${encodeURIComponent(attributeName)}`)).map((attribute) => ({ value: attribute })); if (attributeValues.length === 0) { return; } - $el.autocomplete({ - appendTo: document.querySelector('body'), - hint: false, - openOnFocus: false, // handled manually - minLength: 0, - tabAutocomplete: false - }, [{ - displayKey: 'value', - cache: false, - source: async function (term, cb) { - term = term.toLowerCase(); + $el.autocomplete( + { + appendTo: document.querySelector("body"), + hint: false, + openOnFocus: false, // handled manually + minLength: 0, + tabAutocomplete: false + }, + [ + { + displayKey: "value", + cache: false, + source: async function (term, cb) { + term = term.toLowerCase(); - const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term)); + const filtered = attributeValues.filter((attr) => attr.value.toLowerCase().includes(term)); - cb(filtered); - } - }]); + cb(filtered); + } + } + ] + ); - $el.on('autocomplete:opened', () => { + $el.on("autocomplete:opened", () => { if ($el.attr("readonly")) { - $el.autocomplete('close'); + $el.autocomplete("close"); } }); @@ -102,4 +111,4 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio export default { initAttributeNameAutocomplete, initLabelValueAutocomplete -} +}; diff --git a/src/public/app/services/attribute_parser.ts b/src/public/app/services/attribute_parser.ts index c2a10d471..d97f6a9c0 100644 --- a/src/public/app/services/attribute_parser.ts +++ b/src/public/app/services/attribute_parser.ts @@ -23,17 +23,16 @@ function lex(str: string) { const tokens: Token[] = []; let quotes: boolean | string = false; - let currentWord = ''; + let currentWord = ""; function isOperatorSymbol(chr: string) { - return ['=', '*', '>', '<', '!'].includes(chr); + return ["=", "*", ">", "<", "!"].includes(chr); } function previousOperatorSymbol() { if (currentWord.length === 0) { return false; - } - else { + } else { return isOperatorSymbol(currentWord[currentWord.length - 1]); } } @@ -42,7 +41,7 @@ function lex(str: string) { * @param endIndex - index of the last character of the token */ function finishWord(endIndex: number) { - if (currentWord === '') { + if (currentWord === "") { return; } @@ -52,54 +51,47 @@ function lex(str: string) { endIndex: endIndex }); - currentWord = ''; + currentWord = ""; } for (let i = 0; i < str.length; i++) { const chr = str[i]; - if (chr === '\\') { - if ((i + 1) < str.length) { + if (chr === "\\") { + if (i + 1 < str.length) { i++; currentWord += str[i]; - } - else { + } else { currentWord += chr; } continue; - } - else if (['"', "'", '`'].includes(chr)) { + } else if (['"', "'", "`"].includes(chr)) { if (!quotes) { if (previousOperatorSymbol()) { finishWord(i - 1); } quotes = chr; - } - else if (quotes === chr) { + } else if (quotes === chr) { quotes = false; finishWord(i - 1); - } - else { + } else { // it's a quote, but within other kind of quotes, so it's valid as a literal character currentWord += chr; } continue; - } - else if (!quotes) { - if (currentWord.length === 0 && (chr === '#' || chr === '~')) { + } else if (!quotes) { + if (currentWord.length === 0 && (chr === "#" || chr === "~")) { currentWord = chr; continue; - } - else if (chr === ' ') { + } else if (chr === " ") { finishWord(i - 1); continue; - } - else if (['(', ')'].includes(chr)) { + } else if (["(", ")"].includes(chr)) { finishWord(i - 1); currentWord = chr; @@ -107,8 +99,7 @@ function lex(str: string) { finishWord(i); continue; - } - else if (previousOperatorSymbol() !== isOperatorSymbol(chr)) { + } else if (previousOperatorSymbol() !== isOperatorSymbol(chr)) { finishWord(i - 1); currentWord += chr; @@ -149,27 +140,22 @@ function parse(tokens: Token[], str: string, allowEmptyRelations = false) { const { text, startIndex } = tokens[i]; function isInheritable() { - if (tokens.length > i + 3 - && tokens[i + 1].text === '(' - && tokens[i + 2].text === 'inheritable' - && tokens[i + 3].text === ')') { - + if (tokens.length > i + 3 && tokens[i + 1].text === "(" && tokens[i + 2].text === "inheritable" && tokens[i + 3].text === ")") { i += 3; return true; - } - else { + } else { return false; } } - if (text.startsWith('#')) { + if (text.startsWith("#")) { const labelName = text.substr(1); checkAttributeName(labelName); const attr: Attribute = { - type: 'label', + type: "label", name: labelName, isInheritable: isInheritable(), startIndex: startIndex, @@ -188,14 +174,13 @@ function parse(tokens: Token[], str: string, allowEmptyRelations = false) { } attrs.push(attr); - } - else if (text.startsWith('~')) { + } else if (text.startsWith("~")) { const relationName = text.substr(1); checkAttributeName(relationName); const attr: Attribute = { - type: 'relation', + type: "relation", name: relationName, isInheritable: isInheritable(), startIndex: startIndex, @@ -204,11 +189,10 @@ function parse(tokens: Token[], str: string, allowEmptyRelations = false) { attrs.push(attr); - if (i + 2 >= tokens.length || tokens[i + 1].text !== '=') { + if (i + 2 >= tokens.length || tokens[i + 1].text !== "=") { if (allowEmptyRelations) { break; - } - else { + } else { throw new Error(`Relation "${text}" in ${context(i)} should point to a note.`); } } @@ -220,12 +204,11 @@ function parse(tokens: Token[], str: string, allowEmptyRelations = false) { notePath = notePath.substr(1); } - const noteId = notePath.split('/').pop(); + const noteId = notePath.split("/").pop(); attr.value = noteId; attr.endIndex = tokens[i].endIndex; - } - else { + } else { throw new Error(`Invalid attribute "${text}" in ${context(i)}`); } } @@ -243,4 +226,4 @@ export default { lex, parse, lexAndParse -} +}; diff --git a/src/public/app/services/attribute_renderer.ts b/src/public/app/services/attribute_renderer.ts index b5f8477ec..a686b7098 100644 --- a/src/public/app/services/attribute_renderer.ts +++ b/src/public/app/services/attribute_renderer.ts @@ -4,17 +4,17 @@ import FAttribute from "../entities/fattribute.js"; import FNote from "../entities/fnote.js"; async function renderAttribute(attribute: FAttribute, renderIsInheritable: boolean) { - const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : ''; + const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : ""; const $attr = $(""); - if (attribute.type === 'label') { + if (attribute.type === "label") { $attr.append(document.createTextNode(`#${attribute.name}${isInheritable}`)); if (attribute.value) { - $attr.append('='); + $attr.append("="); $attr.append(document.createTextNode(formatValue(attribute.value))); } - } else if (attribute.type === 'relation') { + } else if (attribute.type === "relation") { if (attribute.isAutoLink) { return $attr; } @@ -38,17 +38,13 @@ async function renderAttribute(attribute: FAttribute, renderIsInheritable: boole function formatValue(val: string) { if (/^[\p{L}\p{N}\-_,.]+$/u.test(val)) { return val; - } - else if (!val.includes('"')) { + } else if (!val.includes('"')) { return `"${val}"`; - } - else if (!val.includes("'")) { + } else if (!val.includes("'")) { return `'${val}'`; - } - else if (!val.includes("`")) { + } else if (!val.includes("`")) { return `\`${val}\``; - } - else { + } else { return `"${val.replace(/"/g, '\\"')}"`; } } @@ -62,9 +58,8 @@ async function createLink(noteId: string) { return $("", { href: `#root/${noteId}`, - class: 'reference-link' - }) - .text(note.title); + class: "reference-link" + }).text(note.title); } async function renderAttributes(attributes: FAttribute[], renderIsInheritable: boolean) { @@ -84,31 +79,16 @@ async function renderAttributes(attributes: FAttribute[], renderIsInheritable: b return $container; } -const HIDDEN_ATTRIBUTES = [ - 'originalFileName', - 'fileSize', - 'template', - 'inherit', - 'cssClass', - 'iconClass', - 'pageSize', - 'viewType' -]; +const HIDDEN_ATTRIBUTES = ["originalFileName", "fileSize", "template", "inherit", "cssClass", "iconClass", "pageSize", "viewType"]; async function renderNormalAttributes(note: FNote) { const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes(); let attrs = note.getAttributes(); if (promotedDefinitionAttributes.length > 0) { - attrs = attrs.filter(attr => !!promotedDefinitionAttributes.find(promAttr => promAttr.isDefinitionFor(attr))); - } - else { - attrs = attrs.filter( - attr => !attr.isDefinition() - && !attr.isAutoLink - && !HIDDEN_ATTRIBUTES.includes(attr.name) - && attr.noteId === note.noteId - ); + attrs = attrs.filter((attr) => !!promotedDefinitionAttributes.find((promAttr) => promAttr.isDefinitionFor(attr))); + } else { + attrs = attrs.filter((attr) => !attr.isDefinition() && !attr.isAutoLink && !HIDDEN_ATTRIBUTES.includes(attr.name) && attr.noteId === note.noteId); } const $renderedAttributes = await renderAttributes(attrs, false); @@ -116,11 +96,11 @@ async function renderNormalAttributes(note: FNote) { return { count: attrs.length, $renderedAttributes - } + }; } export default { renderAttribute, renderAttributes, renderNormalAttributes -} +}; diff --git a/src/public/app/services/attributes.ts b/src/public/app/services/attributes.ts index 4e3eb1fe2..48ec1c336 100644 --- a/src/public/app/services/attributes.ts +++ b/src/public/app/services/attributes.ts @@ -1,11 +1,11 @@ -import server from './server.js'; -import froca from './froca.js'; -import FNote from '../entities/fnote.js'; -import { AttributeRow } from './load_results.js'; +import server from "./server.js"; +import froca from "./froca.js"; +import FNote from "../entities/fnote.js"; +import { AttributeRow } from "./load_results.js"; async function addLabel(noteId: string, name: string, value: string = "") { await server.put(`notes/${noteId}/attribute`, { - type: 'label', + type: "label", name: name, value: value }); @@ -13,7 +13,7 @@ async function addLabel(noteId: string, name: string, value: string = "") { async function setLabel(noteId: string, name: string, value: string = "") { await server.put(`notes/${noteId}/set-attribute`, { - type: 'label', + type: "label", name: name, value: value }); @@ -68,4 +68,4 @@ export default { setLabel, removeAttributeById, isAffecting -} +}; diff --git a/src/public/app/services/branches.ts b/src/public/app/services/branches.ts index 27884e77e..401ee7b30 100644 --- a/src/public/app/services/branches.ts +++ b/src/public/app/services/branches.ts @@ -1,13 +1,13 @@ -import utils from './utils.js'; -import server from './server.js'; +import utils from "./utils.js"; +import server from "./server.js"; import toastService, { ToastOptions } from "./toast.js"; import froca from "./froca.js"; import hoistedNoteService from "./hoisted_note.js"; import ws from "./ws.js"; import appContext from "../components/app_context.js"; -import { t } from './i18n.js'; -import { Node } from './tree.js'; -import { ResolveOptions } from '../widgets/dialogs/delete_notes.js'; +import { t } from "./i18n.js"; +import { Node } from "./tree.js"; +import { ResolveOptions } from "../widgets/dialogs/delete_notes.js"; // TODO: Deduplicate type with server interface Response { @@ -48,13 +48,7 @@ async function moveAfterBranch(branchIdsToMove: string[], afterBranchId: string) return; } - const forbiddenNoteIds = [ - 'root', - hoistedNoteService.getHoistedNoteId(), - '_lbRoot', - '_lbAvailableLaunchers', - '_lbVisibleLaunchers' - ]; + const forbiddenNoteIds = ["root", hoistedNoteService.getHoistedNoteId(), "_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"]; if (forbiddenNoteIds.includes(afterNote.noteId)) { toastService.showError(t("branches.cannot-move-notes-here")); @@ -79,7 +73,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st return; } - if (newParentBranch.noteId === '_lbRoot') { + if (newParentBranch.noteId === "_lbRoot") { toastService.showError(t("branches.cannot-move-notes-here")); return; } @@ -89,9 +83,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st for (const branchIdToMove of branchIdsToMove) { const branchToMove = froca.getBranch(branchIdToMove); - if (!branchToMove - || branchToMove.noteId === hoistedNoteService.getHoistedNoteId() - || (await branchToMove.getParentNote())?.type === 'search') { + if (!branchToMove || branchToMove.noteId === hoistedNoteService.getHoistedNoteId() || (await branchToMove.getParentNote())?.type === "search") { continue; } @@ -116,10 +108,10 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f if (utils.isMobile()) { proceed = true; deleteAllClones = false; - } - else { - ({proceed, deleteAllClones, eraseNotes} = await new Promise(res => - appContext.triggerCommand('showDeleteNotesDialog', {branchIdsToDelete, callback: res, forceDeleteAllClones}))); + } else { + ({ proceed, deleteAllClones, eraseNotes } = await new Promise((res) => + appContext.triggerCommand("showDeleteNotesDialog", { branchIdsToDelete, callback: res, forceDeleteAllClones }) + )); } if (!proceed) { @@ -128,8 +120,7 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f try { await activateParentNotePath(); - } - catch (e) { + } catch (e) { console.error(e); } @@ -141,7 +132,7 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f counter++; const last = counter === branchIdsToDelete.length; - const query = `?taskId=${taskId}&eraseNotes=${eraseNotes ? 'true' : 'false'}&last=${last ? 'true' : 'false'}`; + const query = `?taskId=${taskId}&eraseNotes=${eraseNotes ? "true" : "false"}&last=${last ? "true" : "false"}`; const branch = froca.getBranch(branchIdToDelete); @@ -170,9 +161,7 @@ async function activateParentNotePath() { } async function moveNodeUpInHierarchy(node: Node) { - if (hoistedNoteService.isHoistedNode(node) - || hoistedNoteService.isTopLevelNode(node) - || node.getParent().data.noteType === 'search') { + if (hoistedNoteService.isHoistedNode(node) || hoistedNoteService.isTopLevelNode(node) || node.getParent().data.noteType === "search") { return; } @@ -193,20 +182,19 @@ async function moveNodeUpInHierarchy(node: Node) { } function filterSearchBranches(branchIds: string[]) { - return branchIds.filter(branchId => !branchId.startsWith('virt-')); + return branchIds.filter((branchId) => !branchId.startsWith("virt-")); } function filterRootNote(branchIds: string[]) { const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); - return branchIds.filter(branchId => { + return branchIds.filter((branchId) => { const branch = froca.getBranch(branchId); if (!branch) { return false; } - return branch.noteId !== 'root' - && branch.noteId !== hoistedNoteId; + return branch.noteId !== "root" && branch.noteId !== hoistedNoteId; }); } @@ -219,17 +207,17 @@ function makeToast(id: string, message: string): ToastOptions { }; } -ws.subscribeToMessages(async message => { - if (message.taskType !== 'deleteNotes') { +ws.subscribeToMessages(async (message) => { + if (message.taskType !== "deleteNotes") { return; } - if (message.type === 'taskError') { + if (message.type === "taskError") { toastService.closePersistent(message.taskId); toastService.showError(message.message); - } else if (message.type === 'taskProgressCount') { + } else if (message.type === "taskProgressCount") { toastService.showPersistent(makeToast(message.taskId, t("branches.delete-notes-in-progress", { count: message.progressCount }))); - } else if (message.type === 'taskSucceeded') { + } else if (message.type === "taskSucceeded") { const toast = makeToast(message.taskId, t("branches.delete-finished-successfully")); toast.closeAfter = 5000; @@ -237,17 +225,17 @@ ws.subscribeToMessages(async message => { } }); -ws.subscribeToMessages(async message => { - if (message.taskType !== 'undeleteNotes') { +ws.subscribeToMessages(async (message) => { + if (message.taskType !== "undeleteNotes") { return; } - if (message.type === 'taskError') { + if (message.type === "taskError") { toastService.closePersistent(message.taskId); toastService.showError(message.message); - } else if (message.type === 'taskProgressCount') { + } else if (message.type === "taskProgressCount") { toastService.showPersistent(makeToast(message.taskId, t("branches.undeleting-notes-in-progress", { count: message.progressCount }))); - } else if (message.type === 'taskSucceeded') { + } else if (message.type === "taskSucceeded") { const toast = makeToast(message.taskId, t("branches.undeleting-notes-finished-successfully")); toast.closeAfter = 5000; @@ -292,5 +280,5 @@ export default { moveNodeUpInHierarchy, cloneNoteAfter, cloneNoteToBranch, - cloneNoteToParentNote, + cloneNoteToParentNote }; diff --git a/src/public/app/services/bulk_action.ts b/src/public/app/services/bulk_action.ts index 5a039ea4f..615e1c6eb 100644 --- a/src/public/app/services/bulk_action.ts +++ b/src/public/app/services/bulk_action.ts @@ -27,7 +27,7 @@ const ACTION_GROUPS = [ }, { title: t("bulk_actions.notes"), - actions: [RenameNoteBulkAction, MoveNoteBulkAction, DeleteNoteBulkAction, DeleteRevisionsBulkAction], + actions: [RenameNoteBulkAction, MoveNoteBulkAction, DeleteNoteBulkAction, DeleteRevisionsBulkAction] }, { title: t("bulk_actions.other"), @@ -53,8 +53,8 @@ const ACTION_CLASSES = [ async function addAction(noteId: string, actionName: string) { await server.post(`notes/${noteId}/attributes`, { - type: 'label', - name: 'action', + type: "label", + name: "action", value: JSON.stringify({ name: actionName }) @@ -64,28 +64,29 @@ async function addAction(noteId: string, actionName: string) { } function parseActions(note: FNote) { - const actionLabels = note.getLabels('action'); + const actionLabels = note.getLabels("action"); - return actionLabels.map(actionAttr => { - let actionDef; + return actionLabels + .map((actionAttr) => { + let actionDef; - try { - actionDef = JSON.parse(actionAttr.value); - } catch (e: any) { - logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`); - return null; - } + try { + actionDef = JSON.parse(actionAttr.value); + } catch (e: any) { + logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`); + return null; + } - const ActionClass = ACTION_CLASSES.find(actionClass => actionClass.actionName === actionDef.name); + const ActionClass = ACTION_CLASSES.find((actionClass) => actionClass.actionName === actionDef.name); - if (!ActionClass) { - logError(`No action class for '${actionDef.name}' found.`); - return null; - } + if (!ActionClass) { + logError(`No action class for '${actionDef.name}' found.`); + return null; + } - return new ActionClass(actionAttr, actionDef); - }) - .filter(action => !!action); + return new ActionClass(actionAttr, actionDef); + }) + .filter((action) => !!action); } export default { diff --git a/src/public/app/services/bundle.ts b/src/public/app/services/bundle.ts index 071d89458..a5498348e 100644 --- a/src/public/app/services/bundle.ts +++ b/src/public/app/services/bundle.ts @@ -31,9 +31,9 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container); try { - return await (function () { + return await function () { return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); - }.call(apiContext)); + }.call(apiContext); } catch (e: any) { const note = await froca.getNote(bundle.noteId); @@ -51,7 +51,6 @@ async function executeStartupBundles() { } class WidgetsByParent { - private byParent: Record; constructor() { @@ -73,11 +72,13 @@ class WidgetsByParent { return []; } - return this.byParent[parentName] - // previously, custom widgets were provided as a single instance, but that has the disadvantage - // for splits where we actually need multiple instaces and thus having a class to instantiate is better - // https://github.com/zadam/trilium/issues/4274 - .map((w: any) => w.prototype ? new w() : w); + return ( + this.byParent[parentName] + // previously, custom widgets were provided as a single instance, but that has the disadvantage + // for splits where we actually need multiple instaces and thus having a class to instantiate is better + // https://github.com/zadam/trilium/issues/4274 + .map((w: any) => (w.prototype ? new w() : w)) + ); } } @@ -121,4 +122,4 @@ export default { getAndExecuteBundle, executeStartupBundles, getWidgetBundlesByParent -} +}; diff --git a/src/public/app/services/clipboard.ts b/src/public/app/services/clipboard.ts index d1cb708e3..feffee065 100644 --- a/src/public/app/services/clipboard.ts +++ b/src/public/app/services/clipboard.ts @@ -13,14 +13,13 @@ async function pasteAfter(afterBranchId: string) { return; } - if (clipboardMode === 'cut') { + if (clipboardMode === "cut") { await branchService.moveAfterBranch(clipboardBranchIds, afterBranchId); clipboardBranchIds = []; clipboardMode = null; - } - else if (clipboardMode === 'copy') { - const clipboardBranches = clipboardBranchIds.map(branchId => froca.getBranch(branchId)); + } else if (clipboardMode === "copy") { + const clipboardBranches = clipboardBranchIds.map((branchId) => froca.getBranch(branchId)); for (const clipboardBranch of clipboardBranches) { if (!clipboardBranch) { @@ -36,8 +35,7 @@ async function pasteAfter(afterBranchId: string) { } // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places - } - else { + } else { toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); } } @@ -47,14 +45,13 @@ async function pasteInto(parentBranchId: string) { return; } - if (clipboardMode === 'cut') { + if (clipboardMode === "cut") { await branchService.moveToParentNote(clipboardBranchIds, parentBranchId); clipboardBranchIds = []; clipboardMode = null; - } - else if (clipboardMode === 'copy') { - const clipboardBranches = clipboardBranchIds.map(branchId => froca.getBranch(branchId)); + } else if (clipboardMode === "copy") { + const clipboardBranches = clipboardBranchIds.map((branchId) => froca.getBranch(branchId)); for (const clipboardBranch of clipboardBranches) { if (!clipboardBranch) { @@ -70,19 +67,18 @@ async function pasteInto(parentBranchId: string) { } // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places - } - else { + } else { toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); } } async function copy(branchIds: string[]) { clipboardBranchIds = branchIds; - clipboardMode = 'copy'; + clipboardMode = "copy"; if (utils.isElectron()) { // https://github.com/zadam/trilium/issues/2401 - const {clipboard} = require('electron'); + const { clipboard } = require("electron"); const links = []; for (const branch of froca.getBranches(clipboardBranchIds)) { @@ -90,7 +86,7 @@ async function copy(branchIds: string[]) { links.push($link[0].outerHTML); } - clipboard.writeHTML(links.join(', ')); + clipboard.writeHTML(links.join(", ")); } toastService.showMessage(t("clipboard.copied")); @@ -100,14 +96,14 @@ function cut(branchIds: string[]) { clipboardBranchIds = branchIds; if (clipboardBranchIds.length > 0) { - clipboardMode = 'cut'; + clipboardMode = "cut"; toastService.showMessage(t("clipboard.cut")); } } function isClipboardEmpty() { - clipboardBranchIds = clipboardBranchIds.filter(branchId => !!froca.getBranch(branchId)); + clipboardBranchIds = clipboardBranchIds.filter((branchId) => !!froca.getBranch(branchId)); return clipboardBranchIds.length === 0; } @@ -118,4 +114,4 @@ export default { cut, copy, isClipboardEmpty -} +}; diff --git a/src/public/app/services/content_renderer.ts b/src/public/app/services/content_renderer.ts index 5fe3f4fad..a18dece1c 100644 --- a/src/public/app/services/content_renderer.ts +++ b/src/public/app/services/content_renderer.ts @@ -22,54 +22,42 @@ interface Options { imageHasZoom?: boolean; } -const CODE_MIME_TYPES = new Set([ - "application/json" -]); +const CODE_MIME_TYPES = new Set(["application/json"]); async function getRenderedContent(this: {} | { ctx: string }, entity: FNote, options: Options = {}) { - options = Object.assign({ - tooltip: false - }, options); + options = Object.assign( + { + tooltip: false + }, + options + ); const type = getRenderingType(entity); // attachment supports only image and file/pdf/audio/video const $renderedContent = $('
    '); - if (type === 'text') { + if (type === "text") { await renderText(entity, $renderedContent); - } - else if (type === 'code') { + } else if (type === "code") { await renderCode(entity, $renderedContent); - } - else if (['image', 'canvas', 'mindMap'].includes(type)) { + } else if (["image", "canvas", "mindMap"].includes(type)) { renderImage(entity, $renderedContent, options); - } - else if (!options.tooltip && ['file', 'pdf', 'audio', 'video'].includes(type)) { + } else if (!options.tooltip && ["file", "pdf", "audio", "video"].includes(type)) { renderFile(entity, type, $renderedContent); - } - else if (type === 'mermaid') { + } else if (type === "mermaid") { await renderMermaid(entity, $renderedContent); - } - else if (type === 'render') { - const $content = $('
    '); + } else if (type === "render") { + const $content = $("
    "); await renderService.render(entity, $content); $renderedContent.append($content); - } - else if (!options.tooltip && type === 'protectedSession') { - const $button = $(``) - .on('click', protectedSessionService.enterProtectedSession); + } else if (!options.tooltip && type === "protectedSession") { + const $button = $(``).on("click", protectedSessionService.enterProtectedSession); - $renderedContent.append( - $("
    ") - .append("
    This note is protected and to access it you need to enter password.
    ") - .append("
    ") - .append($button) - ); - } - else if (entity instanceof FNote) { + $renderedContent.append($("
    ").append("
    This note is protected and to access it you need to enter password.
    ").append("
    ").append($button)); + } else if (entity instanceof FNote) { $renderedContent.append( $("
    ") .css("display", "flex") @@ -98,13 +86,13 @@ async function renderText(note: FNote, $renderedContent: JQuery) { if (blob && !utils.isHtmlEmpty(blob.content)) { $renderedContent.append($('
    ').html(blob.content)); - if ($renderedContent.find('span.math-tex').length > 0) { + if ($renderedContent.find("span.math-tex").length > 0) { await libraryLoader.requireLibrary(libraryLoader.KATEX); - renderMathInElement($renderedContent[0], {trust: true}); + renderMathInElement($renderedContent[0], { trust: true }); } - const getNoteIdFromLink = (el: HTMLElement) => treeService.getNoteIdFromUrl($(el).attr('href') || ""); + const getNoteIdFromLink = (el: HTMLElement) => treeService.getNoteIdFromUrl($(el).attr("href") || ""); const referenceLinks = $renderedContent.find("a.reference-link"); const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el)); await froca.getNotes(noteIdsToPrefetch); @@ -143,9 +131,9 @@ function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery") .attr("src", url || "") @@ -171,10 +159,10 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: let entityType, entityId; if (entity instanceof FNote) { - entityType = 'notes'; + entityType = "notes"; entityId = entity.noteId; } else if (entity instanceof FAttachment) { - entityType = 'attachments'; + entityType = "attachments"; entityId = entity.attachmentId; } else { throw new Error(`Can't recognize entity type of '${entity}'`); @@ -182,20 +170,20 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: const $content = $('
    '); - if (type === 'pdf') { + if (type === "pdf") { const $pdfPreview = $(''); $pdfPreview.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open`)); $content.append($pdfPreview); - } else if (type === 'audio') { - const $audioPreview = $('') + } else if (type === "audio") { + const $audioPreview = $("") .attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open-partial`)) .attr("type", entity.mime) .css("width", "100%"); $content.append($audioPreview); - } else if (type === 'video') { - const $videoPreview = $('') + } else if (type === "video") { + const $videoPreview = $("") .attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open-partial`)) .attr("type", entity.mime) .css("width", "100%"); @@ -203,22 +191,18 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: $content.append($videoPreview); } - if (entityType === 'notes' && "noteId" in entity) { + if (entityType === "notes" && "noteId" in entity) { // TODO: we should make this available also for attachments, but there's a problem with "Open externally" support // in attachment list const $downloadButton = $(''); const $openButton = $(''); - $downloadButton.on('click', () => openService.downloadFileNote(entity.noteId)); - $openButton.on('click', () => openService.openNoteExternally(entity.noteId, entity.mime)); + $downloadButton.on("click", () => openService.downloadFileNote(entity.noteId)); + $openButton.on("click", () => openService.openNoteExternally(entity.noteId, entity.mime)); // open doesn't work for protected notes since it works through a browser which isn't in protected session $openButton.toggle(!entity.isProtected); - $content.append( - $('
    ') - .append($downloadButton) - .append($openButton) - ); + $content.append($('
    ').append($downloadButton).append($openButton)); } $renderedContent.append($content); @@ -230,18 +214,16 @@ async function renderMermaid(note: FNote, $renderedContent: JQuery) const blob = await note.getBlob(); const content = blob?.content || ""; - $renderedContent - .css("display", "flex") - .css("justify-content", "space-around"); + $renderedContent.css("display", "flex").css("justify-content", "space-around"); const documentStyle = window.getComputedStyle(document.documentElement); - const mermaidTheme = documentStyle.getPropertyValue('--mermaid-theme'); + const mermaidTheme = documentStyle.getPropertyValue("--mermaid-theme"); - mermaid.mermaidAPI.initialize({startOnLoad: false, theme: mermaidTheme.trim(), securityLevel: 'antiscript'}); + mermaid.mermaidAPI.initialize({ startOnLoad: false, theme: mermaidTheme.trim(), securityLevel: "antiscript" }); try { await loadElkIfNeeded(content); - const {svg} = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content); + const { svg } = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content); $renderedContent.append($(svg)); } catch (e) { @@ -270,10 +252,12 @@ async function renderChildrenList($renderedContent: JQuery, note: F const childNotes = await froca.getNotes(childNoteIds); for (const childNote of childNotes) { - $renderedContent.append(await linkService.createLink(`${note.noteId}/${childNote.noteId}`, { - showTooltip: false, - showNoteIcon: true - })); + $renderedContent.append( + await linkService.createLink(`${note.noteId}/${childNote.noteId}`, { + showTooltip: false, + showNoteIcon: true + }) + ); $renderedContent.append("
    "); } @@ -287,24 +271,23 @@ function getRenderingType(entity: FNote | FAttachment) { type = entity.role; } - const mime = ("mime" in entity && entity.mime); + const mime = "mime" in entity && entity.mime; - if (type === 'file' && mime === 'application/pdf') { - type = 'pdf'; - } else if (type === "file" && mime && CODE_MIME_TYPES.has(mime) ) { + if (type === "file" && mime === "application/pdf") { + type = "pdf"; + } else if (type === "file" && mime && CODE_MIME_TYPES.has(mime)) { type = "code"; - } else if (type === 'file' && mime && mime.startsWith('audio/')) { - type = 'audio'; - } else if (type === 'file' && mime && mime.startsWith('video/')) { - type = 'video'; + } else if (type === "file" && mime && mime.startsWith("audio/")) { + type = "audio"; + } else if (type === "file" && mime && mime.startsWith("video/")) { + type = "video"; } if (entity.isProtected) { if (protectedSessionHolder.isProtectedSessionAvailable()) { protectedSessionHolder.touchProtectedSession(); - } - else { - type = 'protectedSession'; + } else { + type = "protectedSession"; } } diff --git a/src/public/app/services/date_notes.ts b/src/public/app/services/date_notes.ts index bab4deb05..0d604cfa8 100644 --- a/src/public/app/services/date_notes.ts +++ b/src/public/app/services/date_notes.ts @@ -47,7 +47,7 @@ async function getYearNote(year: string) { } async function createSqlConsole() { - const note = await server.post('special-notes/sql-console'); + const note = await server.post("special-notes/sql-console"); await ws.waitForMaxKnownEntityChangeId(); @@ -55,7 +55,7 @@ async function createSqlConsole() { } async function createSearchNote(opts = {}) { - const note = await server.post('special-notes/search-note', opts); + const note = await server.post("special-notes/search-note", opts); await ws.waitForMaxKnownEntityChangeId(); @@ -71,4 +71,4 @@ export default { getYearNote, createSqlConsole, createSearchNote -} +}; diff --git a/src/public/app/services/debounce.ts b/src/public/app/services/debounce.ts index 4f9972a7b..3ccfcd6e5 100644 --- a/src/public/app/services/debounce.ts +++ b/src/public/app/services/debounce.ts @@ -48,14 +48,14 @@ function debounce(func: (...args: unknown[]) => T, waitMs: number, immediate: return result; }; - debounced.clear = function() { + debounced.clear = function () { if (timeout) { clearTimeout(timeout); timeout = null; } }; - debounced.flush = function() { + debounced.flush = function () { if (timeout) { result = func.apply(context, args || []); context = args = null; diff --git a/src/public/app/services/dialog.ts b/src/public/app/services/dialog.ts index 18db1df40..c83edde19 100644 --- a/src/public/app/services/dialog.ts +++ b/src/public/app/services/dialog.ts @@ -3,26 +3,24 @@ import { ConfirmDialogOptions, ConfirmWithMessageOptions } from "../widgets/dial import { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; async function info(message: string) { - return new Promise(res => - appContext.triggerCommand("showInfoDialog", {message, callback: res})); + return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); } async function confirm(message: string) { - return new Promise(res => + return new Promise((res) => appContext.triggerCommand("showConfirmDialog", { message, callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed) - })); + }) + ); } async function confirmDeleteNoteBoxWithNote(title: string) { - return new Promise(res => - appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", {title, callback: res})); + return new Promise((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res })); } async function prompt(props: PromptDialogOptions) { - return new Promise(res => - appContext.triggerCommand("showPromptDialog", {...props, callback: res})); + return new Promise((res) => appContext.triggerCommand("showPromptDialog", { ...props, callback: res })); } export default { diff --git a/src/public/app/services/file_watcher.ts b/src/public/app/services/file_watcher.ts index 0f9ec3bb5..cda3c2852 100644 --- a/src/public/app/services/file_watcher.ts +++ b/src/public/app/services/file_watcher.ts @@ -16,7 +16,7 @@ const fileModificationStatus: Record> = { }; function checkType(type: string) { - if (type !== 'notes' && type !== 'attachments') { + if (type !== "notes" && type !== "attachments") { throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`); } } @@ -40,7 +40,7 @@ function ignoreModification(entityType: string, entityId: string) { } ws.subscribeToMessages(async (message: Message) => { - if (message.type !== 'openedFileUpdated') { + if (message.type !== "openedFileUpdated") { return; } @@ -48,7 +48,7 @@ ws.subscribeToMessages(async (message: Message) => { fileModificationStatus[message.entityType][message.entityId] = message; - appContext.triggerEvent('openedFileUpdated', { + appContext.triggerEvent("openedFileUpdated", { entityType: message.entityType, entityId: message.entityId, lastModifiedMs: message.lastModifiedMs, @@ -60,4 +60,4 @@ export default { getFileModificationStatus, fileModificationUploaded, ignoreModification -} +}; diff --git a/src/public/app/services/froca-interface.ts b/src/public/app/services/froca-interface.ts index 01dd83bc7..c678e220b 100644 --- a/src/public/app/services/froca-interface.ts +++ b/src/public/app/services/froca-interface.ts @@ -21,4 +21,4 @@ export interface Froca { getBranches(branchIds: string[], silentNotFoundError?: boolean): FBranch[]; getAttachmentsForNote(noteId: string): Promise; -} \ No newline at end of file +} diff --git a/src/public/app/services/froca.ts b/src/public/app/services/froca.ts index f84dc37ea..265cefda8 100644 --- a/src/public/app/services/froca.ts +++ b/src/public/app/services/froca.ts @@ -7,7 +7,6 @@ import FBlob, { FBlobRow } from "../entities/fblob.js"; import FAttachment, { FAttachmentRow } from "../entities/fattachment.js"; import { Froca } from "./froca-interface.js"; - interface SubtreeResponse { notes: FNoteRow[]; branches: FBranchRow[]; @@ -44,7 +43,7 @@ class FrocaImpl implements Froca { } async loadInitialTree() { - const resp = await server.get('tree'); + const resp = await server.get("tree"); // clear the cache only directly before adding new content which is important for e.g., switching to protected session @@ -73,7 +72,7 @@ class FrocaImpl implements Froca { const noteIdsToSort = new Set(); for (const noteRow of noteRows) { - const {noteId} = noteRow; + const { noteId } = noteRow; let note = this.notes[noteId]; @@ -81,12 +80,12 @@ class FrocaImpl implements Froca { note.update(noteRow); // search note doesn't have child branches in the database and all the children are virtual branches - if (note.type !== 'search') { + if (note.type !== "search") { for (const childNoteId of note.children) { const childNote = this.notes[childNoteId]; if (childNote) { - childNote.parents = childNote.parents.filter(p => p !== noteId); + childNote.parents = childNote.parents.filter((p) => p !== noteId); delete this.branches[childNote.parentToBranch[noteId]]; delete childNote.parentToBranch[noteId]; @@ -99,7 +98,7 @@ class FrocaImpl implements Froca { // we want to remove all "real" branches (represented in the database) since those will be created // from branches argument but want to preserve all virtual ones from saved search - note.parents = note.parents.filter(parentNoteId => { + note.parents = note.parents.filter((parentNoteId) => { const parentNote = this.notes[parentNoteId]; const branch = this.branches[parentNote.childToBranch[noteId]]; @@ -111,15 +110,14 @@ class FrocaImpl implements Froca { return true; } - parentNote.children = parentNote.children.filter(p => p !== noteId); + parentNote.children = parentNote.children.filter((p) => p !== noteId); delete this.branches[parentNote.childToBranch[noteId]]; delete parentNote.childToBranch[noteId]; return false; }); - } - else { + } else { this.notes[noteId] = new FNote(this, noteRow); } } @@ -145,7 +143,7 @@ class FrocaImpl implements Froca { } for (const attributeRow of attributeRows) { - const {attributeId} = attributeRow; + const { attributeId } = attributeRow; this.attributes[attributeId] = new FAttribute(this, attributeRow); @@ -155,7 +153,7 @@ class FrocaImpl implements Froca { note.attributes.push(attributeId); } - if (attributeRow.type === 'relation') { + if (attributeRow.type === "relation") { const targetNote = this.notes[attributeRow.value]; if (targetNote) { @@ -180,21 +178,21 @@ class FrocaImpl implements Froca { noteIds = Array.from(new Set(noteIds)); // make noteIds unique - const resp = await server.post('tree/load', { noteIds }); + const resp = await server.post("tree/load", { noteIds }); this.addResp(resp); - appContext.triggerEvent('notesReloaded', {noteIds}); + appContext.triggerEvent("notesReloaded", { noteIds }); } async loadSearchNote(noteId: string) { const note = await this.getNote(noteId); - if (!note || note.type !== 'search') { + if (!note || note.type !== "search") { return; } - const {searchResultNoteIds, highlightedTokens, error} = await server.get(`search-note/${note.noteId}`); + const { searchResultNoteIds, highlightedTokens, error } = await server.get(`search-note/${note.noteId}`); if (!Array.isArray(searchResultNoteIds)) { throw new Error(`Search note '${note.noteId}' failed: ${searchResultNoteIds}`); @@ -208,14 +206,16 @@ class FrocaImpl implements Froca { const branches: FBranchRow[] = [...note.getParentBranches(), ...note.getChildBranches()]; - searchResultNoteIds.forEach((resultNoteId, index) => branches.push({ - // branchId should be repeatable since sometimes we reload some notes without rerendering the tree - branchId: `virt-${note.noteId}-${resultNoteId}`, - noteId: resultNoteId, - parentNoteId: note.noteId, - notePosition: (index + 1) * 10, - fromSearchNote: true - })); + searchResultNoteIds.forEach((resultNoteId, index) => + branches.push({ + // branchId should be repeatable since sometimes we reload some notes without rerendering the tree + branchId: `virt-${note.noteId}-${resultNoteId}`, + noteId: resultNoteId, + parentNoteId: note.noteId, + notePosition: (index + 1) * 10, + fromSearchNote: true + }) + ); // update this note with standard (parent) branches + virtual (children) branches this.addResp({ @@ -227,37 +227,40 @@ class FrocaImpl implements Froca { froca.notes[note.noteId].searchResultsLoaded = true; froca.notes[note.noteId].highlightedTokens = highlightedTokens; - return {error}; + return { error }; } getNotesFromCache(noteIds: string[], silentNotFoundError = false): FNote[] { - return noteIds.map(noteId => { - if (!this.notes[noteId] && !silentNotFoundError) { - console.trace(`Can't find note '${noteId}'`); + return noteIds + .map((noteId) => { + if (!this.notes[noteId] && !silentNotFoundError) { + console.trace(`Can't find note '${noteId}'`); - return null; - } - else { - return this.notes[noteId]; - } - }).filter(note => !!note) as FNote[]; + return null; + } else { + return this.notes[noteId]; + } + }) + .filter((note) => !!note) as FNote[]; } async getNotes(noteIds: string[] | JQuery, silentNotFoundError = false): Promise { noteIds = Array.from(new Set(noteIds)); // make unique - const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]); + const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]); await this.reloadNotes(missingNoteIds); - return noteIds.map(noteId => { - if (!this.notes[noteId] && !silentNotFoundError) { - console.trace(`Can't find note '${noteId}'`); + return noteIds + .map((noteId) => { + if (!this.notes[noteId] && !silentNotFoundError) { + console.trace(`Can't find note '${noteId}'`); - return null; - } else { - return this.notes[noteId]; - } - }).filter(note => !!note) as FNote[]; + return null; + } else { + return this.notes[noteId]; + } + }) + .filter((note) => !!note) as FNote[]; } async noteExists(noteId: string): Promise { @@ -267,11 +270,10 @@ class FrocaImpl implements Froca { } async getNote(noteId: string, silentNotFoundError = false): Promise { - if (noteId === 'none') { + if (noteId === "none") { console.trace(`No 'none' note.`); return null; - } - else if (!noteId) { + } else if (!noteId) { console.trace(`Falsy noteId '${noteId}', returning null.`); return null; } @@ -288,9 +290,7 @@ class FrocaImpl implements Froca { } getBranches(branchIds: string[], silentNotFoundError = false): FBranch[] { - return branchIds - .map(branchId => this.getBranch(branchId, silentNotFoundError)) - .filter(b => !!b) as FBranch[]; + return branchIds.map((branchId) => this.getBranch(branchId, silentNotFoundError)).filter((b) => !!b) as FBranch[]; } getBranch(branchId: string, silentNotFoundError = false) { @@ -298,15 +298,14 @@ class FrocaImpl implements Froca { if (!silentNotFoundError) { logError(`Not existing branch '${branchId}'`); } - } - else { + } else { return this.branches[branchId]; } } async getBranchId(parentNoteId: string, childNoteId: string) { - if (childNoteId === 'root') { - return 'none_root'; + if (childNoteId === "root") { + return "none_root"; } const child = await this.getNote(childNoteId); @@ -354,7 +353,7 @@ class FrocaImpl implements Froca { } processAttachmentRows(attachmentRows: FAttachmentRow[]): FAttachment[] { - return attachmentRows.map(attachmentRow => { + return attachmentRows.map((attachmentRow) => { let attachment; if (attachmentRow.attachmentId in this.attachments) { @@ -376,16 +375,15 @@ class FrocaImpl implements Froca { const key = `${entityType}-${entityId}`; if (!this.blobPromises[key]) { - this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob`) - .then(row => new FBlob(row)) - .catch(e => console.error(`Cannot get blob for ${entityType} '${entityId}'`, e)); + this.blobPromises[key] = server + .get(`${entityType}/${entityId}/blob`) + .then((row) => new FBlob(row)) + .catch((e) => console.error(`Cannot get blob for ${entityType} '${entityId}'`, e)); // we don't want to keep large payloads forever in memory, so we clean that up quite quickly // this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components) // if the blob is updated within the cache lifetime, it should be invalidated by froca_updater - this.blobPromises[key]?.then( - () => setTimeout(() => this.blobPromises[key] = null, 1000) - ); + this.blobPromises[key]?.then(() => setTimeout(() => (this.blobPromises[key] = null), 1000)); } return await this.blobPromises[key]; diff --git a/src/public/app/services/froca_updater.ts b/src/public/app/services/froca_updater.ts index d936a5495..684213336 100644 --- a/src/public/app/services/froca_updater.ts +++ b/src/public/app/services/froca_updater.ts @@ -7,42 +7,40 @@ import FBranch, { FBranchRow } from "../entities/fbranch.js"; import FAttribute, { FAttributeRow } from "../entities/fattribute.js"; import FAttachment, { FAttachmentRow } from "../entities/fattachment.js"; import FNote, { FNoteRow } from "../entities/fnote.js"; -import type { EntityChange } from "../server_types.js" +import type { EntityChange } from "../server_types.js"; async function processEntityChanges(entityChanges: EntityChange[]) { const loadResults = new LoadResults(entityChanges); for (const ec of entityChanges) { try { - if (ec.entityName === 'notes') { + if (ec.entityName === "notes") { processNoteChange(loadResults, ec); - } else if (ec.entityName === 'branches') { + } else if (ec.entityName === "branches") { await processBranchChange(loadResults, ec); - } else if (ec.entityName === 'attributes') { + } else if (ec.entityName === "attributes") { processAttributeChange(loadResults, ec); - } else if (ec.entityName === 'note_reordering') { + } else if (ec.entityName === "note_reordering") { processNoteReordering(loadResults, ec); - } else if (ec.entityName === 'revisions') { + } else if (ec.entityName === "revisions") { loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId); - } else if (ec.entityName === 'options') { + } else if (ec.entityName === "options") { const attributeEntity = ec.entity as FAttributeRow; - if (attributeEntity.name === 'openNoteContexts') { + if (attributeEntity.name === "openNoteContexts") { continue; // only noise } options.set(attributeEntity.name, attributeEntity.value); loadResults.addOption(attributeEntity.name); - } else if (ec.entityName === 'attachments') { + } else if (ec.entityName === "attachments") { processAttachment(loadResults, ec); - } else if (ec.entityName === 'blobs' || ec.entityName === 'etapi_tokens') { + } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") { // NOOP - } - else { + } else { throw new Error(`Unknown entityName '${ec.entityName}'`); } - } - catch (e: any) { + } catch (e: any) { throw new Error(`Can't process entity ${JSON.stringify(ec)} with error ${e.message} ${e.stack}`); } } @@ -54,19 +52,17 @@ async function processEntityChanges(entityChanges: EntityChange[]) { // mean we need to load the target of the relation (and then perhaps transitively the whole note path of this target). const missingNoteIds = []; - for (const {entityName, entity} of entityChanges) { - if (!entity) { // if erased + for (const { entityName, entity } of entityChanges) { + if (!entity) { + // if erased continue; } - if (entityName === 'branches' && !((entity as FBranchRow).parentNoteId in froca.notes)) { + if (entityName === "branches" && !((entity as FBranchRow).parentNoteId in froca.notes)) { missingNoteIds.push((entity as FBranchRow).parentNoteId); - } - else if (entityName === 'attributes') { + } else if (entityName === "attributes") { let attributeEntity = entity as FAttributeRow; - if (attributeEntity.type === 'relation' - && (attributeEntity.name === 'template' || attributeEntity.name === 'inherit') - && !(attributeEntity.value in froca.notes)) { + if (attributeEntity.type === "relation" && (attributeEntity.name === "template" || attributeEntity.name === "inherit") && !(attributeEntity.value in froca.notes)) { missingNoteIds.push(attributeEntity.value); } } @@ -84,7 +80,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) { // TODO: Remove after porting the file // @ts-ignore const appContext = (await import("../components/app_context.js")).default as any; - await appContext.triggerEvent('entitiesReloaded', {loadResults}); + await appContext.triggerEvent("entitiesReloaded", { loadResults }); } } @@ -106,8 +102,7 @@ function processNoteChange(loadResults: LoadResults, ec: EntityChange) { if (ec.isErased || ec.entity?.isDeleted) { delete froca.notes[ec.entityId]; - } - else { + } else { if (note.blobId !== (ec.entity as FNoteRow).blobId) { for (const key of Object.keys(froca.blobPromises)) { if (key.includes(note.noteId)) { @@ -138,12 +133,12 @@ async function processBranchChange(loadResults: LoadResults, ec: EntityChange) { const parentNote = froca.notes[branch.parentNoteId]; if (childNote) { - childNote.parents = childNote.parents.filter(parentNoteId => parentNoteId !== branch.parentNoteId); + childNote.parents = childNote.parents.filter((parentNoteId) => parentNoteId !== branch.parentNoteId); delete childNote.parentToBranch[branch.parentNoteId]; } if (parentNote) { - parentNote.children = parentNote.children.filter(childNoteId => childNoteId !== branch.noteId); + parentNote.children = parentNote.children.filter((childNoteId) => childNoteId !== branch.noteId); delete parentNote.childToBranch[branch.noteId]; } @@ -175,8 +170,7 @@ async function processBranchChange(loadResults: LoadResults, ec: EntityChange) { if (branch) { branch.update(ec.entity as FBranch); - } - else if (childNote || parentNote) { + } else if (childNote || parentNote) { froca.branches[ec.entityId] = branch = new FBranch(froca, branchEntity); } @@ -226,14 +220,14 @@ function processAttributeChange(loadResults: LoadResults, ec: EntityChange) { if (ec.isErased || ec.entity?.isDeleted) { if (attribute) { const sourceNote = froca.notes[attribute.noteId]; - const targetNote = attribute.type === 'relation' && froca.notes[attribute.value]; + const targetNote = attribute.type === "relation" && froca.notes[attribute.value]; if (sourceNote) { - sourceNote.attributes = sourceNote.attributes.filter(attributeId => attributeId !== attribute.attributeId); + sourceNote.attributes = sourceNote.attributes.filter((attributeId) => attributeId !== attribute.attributeId); } if (targetNote) { - targetNote.targetRelations = targetNote.targetRelations.filter(attributeId => attributeId !== attribute.attributeId); + targetNote.targetRelations = targetNote.targetRelations.filter((attributeId) => attributeId !== attribute.attributeId); } if (ec.componentId) { @@ -252,7 +246,7 @@ function processAttributeChange(loadResults: LoadResults, ec: EntityChange) { const attributeEntity = ec.entity as FAttributeRow; const sourceNote = froca.notes[attributeEntity.noteId]; - const targetNote = attributeEntity.type === 'relation' && froca.notes[attributeEntity.value]; + const targetNote = attributeEntity.type === "relation" && froca.notes[attributeEntity.value]; if (attribute) { attribute.update(ec.entity as FAttributeRow); @@ -285,7 +279,7 @@ function processAttachment(loadResults: LoadResults, ec: EntityChange) { const note = attachment.getNote(); if (note && note.attachments) { - note.attachments = note.attachments.filter(att => att.attachmentId !== attachment.attachmentId); + note.attachments = note.attachments.filter((att) => att.attachmentId !== attachment.attachmentId); } loadResults.addAttachmentRow(attachmentEntity); @@ -312,4 +306,4 @@ function processAttachment(loadResults: LoadResults, ec: EntityChange) { export default { processEntityChanges -} +}; diff --git a/src/public/app/services/frontend_script_api.ts b/src/public/app/services/frontend_script_api.ts index 1cbab4a0f..acdff7444 100644 --- a/src/public/app/services/frontend_script_api.ts +++ b/src/public/app/services/frontend_script_api.ts @@ -1,13 +1,13 @@ -import server from './server.js'; -import utils from './utils.js'; -import toastService from './toast.js'; -import linkService from './link.js'; -import froca from './froca.js'; -import noteTooltipService from './note_tooltip.js'; -import protectedSessionService from './protected_session.js'; -import dateNotesService from './date_notes.js'; -import searchService from './search.js'; -import RightPanelWidget from '../widgets/right_panel_widget.js'; +import server from "./server.js"; +import utils from "./utils.js"; +import toastService from "./toast.js"; +import linkService from "./link.js"; +import froca from "./froca.js"; +import noteTooltipService from "./note_tooltip.js"; +import protectedSessionService from "./protected_session.js"; +import dateNotesService from "./date_notes.js"; +import searchService from "./search.js"; +import RightPanelWidget from "../widgets/right_panel_widget.js"; import ws from "./ws.js"; import appContext from "../components/app_context.js"; import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js"; @@ -15,12 +15,11 @@ import BasicWidget from "../widgets/basic_widget.js"; import SpacedUpdate from "./spaced_update.js"; import shortcutService from "./shortcuts.js"; import dialogService from "./dialog.js"; -import FNote from '../entities/fnote.js'; -import { t } from './i18n.js'; -import NoteContext from '../components/note_context.js'; -import NoteDetailWidget from '../widgets/note_detail.js'; -import Component from '../components/component.js'; - +import FNote from "../entities/fnote.js"; +import { t } from "./i18n.js"; +import NoteContext from "../components/note_context.js"; +import NoteDetailWidget from "../widgets/note_detail.js"; +import Component from "../components/component.js"; /** * A whole number @@ -39,7 +38,7 @@ interface AddToToolbarOpts { action: () => void; /** id of the button, used to identify the old instances of this button to be replaced * ID is optional because of BC, but not specifying it is deprecated. ID can be alphanumeric only. */ - id: string; + id: string; /** name of the boxicon to be used (e.g. "time" for "bx-time" icon) */ icon: string; /** keyboard shortcut for the button, e.g. "alt+t" */ @@ -105,7 +104,7 @@ interface Api { */ activateNewNote(notePath: string): Promise; - /** + /** * Open a note in a new tab. * * @method @@ -131,7 +130,7 @@ interface Api { */ addButtonToToolbar(opts: AddToToolbarOpts): void; - /** + /** * @private */ __runOnBackendInner(func: unknown, params: unknown[], transactional: boolean): unknown; @@ -248,7 +247,7 @@ interface Api { */ triggerEvent: typeof appContext.triggerEvent; - /** + /** * Create a note link (jQuery object) for given note. * * @param {string} notePath (or noteId) @@ -388,7 +387,7 @@ interface Api { */ setHoistedNoteId(noteId: string): void; - /** + /** * @param keyboardShortcut - e.g. "ctrl+shift+a" * @param [namespace] specify namespace of the handler for the cases where call for bind may be repeated. * If a handler with this ID exists, it's replaced by the new handler. @@ -447,57 +446,55 @@ interface Api { * available in the JS frontend notes. You can use e.g. api.showMessage(api.startNote.title);

    */ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, originEntity: Entity | null = null, $container: JQuery | null = null) { - this.$container = $container; this.startNote = startNote; - this.currentNote = currentNote; + this.currentNote = currentNote; this.originEntity = originEntity; this.dayjs = dayjs; this.RightPanelWidget = RightPanelWidget; this.NoteContextAwareWidget = NoteContextAwareWidget; this.BasicWidget = BasicWidget; - - this.activateNote = async notePath => { + + this.activateNote = async (notePath) => { await appContext.tabManager.getActiveContext().setNote(notePath); }; - this.activateNewNote = async notePath => { + this.activateNewNote = async (notePath) => { await ws.waitForMaxKnownEntityChangeId(); await appContext.tabManager.getActiveContext().setNote(notePath); - await appContext.triggerEvent('focusAndSelectTitle'); + await appContext.triggerEvent("focusAndSelectTitle"); }; - this.openTabWithNote = async (notePath, activate) => { await ws.waitForMaxKnownEntityChangeId(); await appContext.tabManager.openTabWithNoteWithHoisting(notePath, { activate }); if (activate) { - await appContext.triggerEvent('focusAndSelectTitle'); + await appContext.triggerEvent("focusAndSelectTitle"); } }; - + this.openSplitWithNote = async (notePath, activate) => { await ws.waitForMaxKnownEntityChangeId(); const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); - const {ntxId} = subContexts[subContexts.length - 1]; + const { ntxId } = subContexts[subContexts.length - 1]; - await appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath}); + await appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath }); if (activate) { - await appContext.triggerEvent('focusAndSelectTitle'); + await appContext.triggerEvent("focusAndSelectTitle"); } }; - - this.addButtonToToolbar = async opts => { + + this.addButtonToToolbar = async (opts) => { console.warn("api.addButtonToToolbar() has been deprecated since v0.58 and may be removed in the future. Use Menu -> Configure Launchbar to create/update launchers instead."); - const {action, ...reqBody} = opts; - - await server.put('special-notes/api-script-launcher', { + const { action, ...reqBody } = opts; + + await server.put("special-notes/api-script-launcher", { action: action.toString(), ...reqBody }); @@ -508,31 +505,33 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig return params; } - return params.map(p => { + return params.map((p) => { if (typeof p === "function") { return `!@#Function: ${p.toString()}`; - } - else { + } else { return p; } }); } - this.__runOnBackendInner = async (func, params, transactional) => { if (typeof func === "function") { func = func.toString(); } - const ret = await server.post('script/exec', { - script: func, - params: prepareParams(params), - startNoteId: startNote.noteId, - currentNoteId: currentNote.noteId, - originEntityName: "notes", // currently there's no other entity on the frontend which can trigger event - originEntityId: originEntity ? originEntity.noteId : null, - transactional - }, "script"); + const ret = await server.post( + "script/exec", + { + script: func, + params: prepareParams(params), + startNoteId: startNote.noteId, + currentNoteId: currentNote.noteId, + originEntityName: "notes", // currently there's no other entity on the frontend which can trigger event + originEntityId: originEntity ? originEntity.noteId : null, + transactional + }, + "script" + ); if (ret.success) { await ws.waitForMaxKnownEntityChangeId(); @@ -541,7 +540,7 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig } else { throw new Error(`server error: ${ret.error}`); } - } + }; this.runOnBackend = async (func, params = []) => { if (func?.constructor.name === "AsyncFunction" || (typeof func === "string" && func?.startsWith?.("async "))) { @@ -551,7 +550,6 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig return await this.__runOnBackendInner(func, params, true); }; - this.runAsyncOnBackendWithManualTransactionHandling = async (func, params = []) => { if (func?.constructor.name === "Function" || (typeof func === "string" && func?.startsWith?.("function"))) { toastService.showError(t("frontend_script_api.sync_warning")); @@ -559,72 +557,67 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig return await this.__runOnBackendInner(func, params, false); }; - - this.searchForNotes = async searchString => { + + this.searchForNotes = async (searchString) => { return await searchService.searchForNotes(searchString); }; - - this.searchForNote = async searchString => { + this.searchForNote = async (searchString) => { const notes = await this.searchForNotes(searchString); return notes.length > 0 ? notes[0] : null; }; - - this.getNote = async noteId => await froca.getNote(noteId); - this.getNotes = async (noteIds, silentNotFoundError = false) => await froca.getNotes(noteIds, silentNotFoundError); - this.reloadNotes = async noteIds => await froca.reloadNotes(noteIds); - this.getInstanceName = () => window.glob.instanceName; + this.getNote = async (noteId) => await froca.getNote(noteId); + this.getNotes = async (noteIds, silentNotFoundError = false) => await froca.getNotes(noteIds, silentNotFoundError); + this.reloadNotes = async (noteIds) => await froca.reloadNotes(noteIds); + this.getInstanceName = () => window.glob.instanceName; this.formatDateISO = utils.formatDateISO; this.parseDate = utils.parseDate; this.showMessage = toastService.showMessage; this.showError = toastService.showError; - this.showInfoDialog = dialogService.info; + this.showInfoDialog = dialogService.info; this.showConfirmDialog = dialogService.confirm; - this.showPromptDialog = dialogService.prompt; - - this.triggerCommand = (name, data) => appContext.triggerCommand(name, data); + + this.triggerCommand = (name, data) => appContext.triggerCommand(name, data); this.triggerEvent = (name, data) => appContext.triggerEvent(name, data); - this.createLink = linkService.createLink; this.createNoteLink = linkService.createLink; - - this.addTextToActiveContextEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); + + this.addTextToActiveContextEditor = (text) => appContext.triggerCommand("addTextToActiveEditor", { text }); this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); this.getActiveContext = () => appContext.tabManager.getActiveContext(); this.getActiveMainContext = () => appContext.tabManager.getActiveMainContext(); - - this.getNoteContexts = () => appContext.tabManager.getNoteContexts(); + + this.getNoteContexts = () => appContext.tabManager.getNoteContexts(); this.getMainNoteContexts = () => appContext.tabManager.getMainNoteContexts(); - - this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor(); + this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor(); this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContext()?.getCodeEditor(); - - this.getActiveNoteDetailWidget = () => new Promise(resolve => appContext.triggerCommand('executeInActiveNoteDetailWidget', {callback: resolve})); + + this.getActiveNoteDetailWidget = () => new Promise((resolve) => appContext.triggerCommand("executeInActiveNoteDetailWidget", { callback: resolve })); this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath(); - - this.getComponentByEl = el => appContext.getComponentByEl(el); - + + this.getComponentByEl = (el) => appContext.getComponentByEl(el); + this.setupElementTooltip = noteTooltipService.setupElementTooltip; - + this.protectNote = async (noteId, protect) => { await protectedSessionService.protectNote(noteId, protect, false); }; - + this.protectSubTree = async (noteId, protect) => { await protectedSessionService.protectNote(noteId, protect, true); }; - - this.getTodayNote = dateNotesService.getTodayNote; + + this.getTodayNote = dateNotesService.getTodayNote; this.getDayNote = dateNotesService.getDayNote; - this.getWeekNote = dateNotesService.getWeekNote; + this.getWeekNote = dateNotesService.getWeekNote; this.getMonthNote = dateNotesService.getMonthNote; this.getYearNote = dateNotesService.getYearNote; @@ -637,32 +630,33 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig }; this.bindGlobalShortcut = shortcutService.bindGlobalShortcut; - - this.waitUntilSynced = ws.waitForMaxKnownEntityChangeId; - - this.refreshIncludedNote = includedNoteId => appContext.triggerEvent('refreshIncludedNote', {noteId: includedNoteId}); - + this.waitUntilSynced = ws.waitForMaxKnownEntityChangeId; + + this.refreshIncludedNote = (includedNoteId) => appContext.triggerEvent("refreshIncludedNote", { noteId: includedNoteId }); + this.randomString = utils.randomString; this.formatSize = utils.formatSize; this.formatNoteSize = utils.formatSize; this.logMessages = {}; - this.logSpacedUpdates = {}; - this.log = message => { - const {noteId} = this.startNote; + this.logSpacedUpdates = {}; + this.log = (message) => { + const { noteId } = this.startNote; message = `${utils.now()}: ${message}`; console.log(`Script ${noteId}: ${message}`); this.logMessages[noteId] = this.logMessages[noteId] || []; - this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => { - const messages = this.logMessages[noteId]; - this.logMessages[noteId] = []; + this.logSpacedUpdates[noteId] = + this.logSpacedUpdates[noteId] || + new SpacedUpdate(() => { + const messages = this.logMessages[noteId]; + this.logMessages[noteId] = []; - appContext.triggerEvent("apiLogMessages", {noteId, messages}); - }, 100); + appContext.triggerEvent("apiLogMessages", { noteId, messages }); + }, 100); this.logMessages[noteId].push(message); this.logSpacedUpdates[noteId].scheduleUpdate(); @@ -670,5 +664,5 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig } export default FrontendScriptApi as any as { - new (startNote: FNote, currentNote: FNote, originEntity: Entity | null, $container: JQuery | null): Api + new (startNote: FNote, currentNote: FNote, originEntity: Entity | null, $container: JQuery | null): Api; }; diff --git a/src/public/app/services/glob.ts b/src/public/app/services/glob.ts index e5f9e744d..69c33e4c7 100644 --- a/src/public/app/services/glob.ts +++ b/src/public/app/services/glob.ts @@ -10,10 +10,10 @@ function setupGlobs() { window.glob.isDesktop = utils.isDesktop; window.glob.isMobile = utils.isMobile; - window.glob.getComponentByEl = el => appContext.getComponentByEl(el); + window.glob.getComponentByEl = (el) => appContext.getComponentByEl(el); window.glob.getHeaders = server.getHeaders; - window.glob.getReferenceLinkTitle = href => linkService.getReferenceLinkTitle(href); - window.glob.getReferenceLinkTitleSync = href => linkService.getReferenceLinkTitleSync(href); + window.glob.getReferenceLinkTitle = (href) => linkService.getReferenceLinkTitle(href); + window.glob.getReferenceLinkTitleSync = (href) => linkService.getReferenceLinkTitleSync(href); // required for ESLint plugin and CKEditor window.glob.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); @@ -32,16 +32,9 @@ function setupGlobs() { let message = "Uncaught error: "; if (string.includes("script error")) { - message += 'No details available'; + message += "No details available"; } else { - message += [ - `Message: ${msg}`, - `URL: ${url}`, - `Line: ${lineNo}`, - `Column: ${columnNo}`, - `Error object: ${JSON.stringify(error)}`, - `Stack: ${error && error.stack}` - ].join(', '); + message += [`Message: ${msg}`, `URL: ${url}`, `Line: ${lineNo}`, `Column: ${columnNo}`, `Error object: ${JSON.stringify(error)}`, `Stack: ${error && error.stack}`].join(", "); } ws.logError(message); @@ -55,7 +48,7 @@ function setupGlobs() { let message = "Uncaught error: "; if (string?.includes("script error")) { - message += 'No details available'; + message += "No details available"; } else { message += [ `Message: ${e.reason.message}`, @@ -63,7 +56,7 @@ function setupGlobs() { `Column: ${e.reason.columnNumber}`, `Error object: ${JSON.stringify(e.reason)}`, `Stack: ${e.reason && e.reason.stack}` - ].join(', '); + ].join(", "); } ws.logError(message); @@ -78,7 +71,7 @@ function setupGlobs() { utils.initHelpButtons($(window)); $("body").on("click", "a.external", function () { - window.open($(this).attr("href"), '_blank'); + window.open($(this).attr("href"), "_blank"); return false; }); @@ -86,4 +79,4 @@ function setupGlobs() { export default { setupGlobs -} +}; diff --git a/src/public/app/services/hoisted_note.ts b/src/public/app/services/hoisted_note.ts index 86097da78..d05a2c3c8 100644 --- a/src/public/app/services/hoisted_note.ts +++ b/src/public/app/services/hoisted_note.ts @@ -8,7 +8,7 @@ import { t } from "./i18n.js"; function getHoistedNoteId() { const activeNoteContext = appContext.tabManager.getActiveContext(); - return activeNoteContext ? activeNoteContext.hoistedNoteId : 'root'; + return activeNoteContext ? activeNoteContext.hoistedNoteId : "root"; } async function unhoist() { @@ -25,14 +25,13 @@ function isTopLevelNode(node: Node) { function isHoistedNode(node: Node) { // even though check for 'root' should not be necessary, we keep it just in case - return node.data.noteId === "root" - || node.data.noteId === getHoistedNoteId(); + return node.data.noteId === "root" || node.data.noteId === getHoistedNoteId(); } async function isHoistedInHiddenSubtree() { const hoistedNoteId = getHoistedNoteId(); - if (hoistedNoteId === 'root') { + if (hoistedNoteId === "root") { return false; } @@ -50,7 +49,7 @@ async function checkNoteAccess(notePath: string, noteContext: NoteContext) { const hoistedNoteId = noteContext.hoistedNoteId; - if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes('_hidden') || resolvedNotePath.includes('_lbBookmarks'))) { + if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes("_hidden") || resolvedNotePath.includes("_lbBookmarks"))) { const noteId = treeService.getNoteIdFromUrl(resolvedNotePath); if (!noteId) { return false; @@ -58,8 +57,10 @@ async function checkNoteAccess(notePath: string, noteContext: NoteContext) { const requestedNote = await froca.getNote(noteId); const hoistedNote = await froca.getNote(hoistedNoteId); - if ((!hoistedNote?.hasAncestor('_hidden') || resolvedNotePath.includes('_lbBookmarks')) - && !await dialogService.confirm(t("hoisted_note.confirm_unhoisting", { requestedNote: requestedNote?.title, hoistedNote: hoistedNote?.title }))) { + if ( + (!hoistedNote?.hasAncestor("_hidden") || resolvedNotePath.includes("_lbBookmarks")) && + !(await dialogService.confirm(t("hoisted_note.confirm_unhoisting", { requestedNote: requestedNote?.title, hoistedNote: hoistedNote?.title }))) + ) { return false; } @@ -77,4 +78,4 @@ export default { isHoistedNode, checkNoteAccess, isHoistedInHiddenSubtree -} +}; diff --git a/src/public/app/services/i18n.ts b/src/public/app/services/i18n.ts index 3d82c0e1d..48b3be72e 100644 --- a/src/public/app/services/i18n.ts +++ b/src/public/app/services/i18n.ts @@ -6,16 +6,14 @@ await library_loader.requireLibrary(library_loader.I18NEXT); export async function initLocale() { const locale = (options.get("locale") as string) || "en"; - await i18next - .use(i18nextHttpBackend) - .init({ - lng: locale, - fallbackLng: "en", - backend: { - loadPath: `${window.glob.assetPath}/translations/{{lng}}/{{ns}}.json` - }, - returnEmptyString: false - }); + await i18next.use(i18nextHttpBackend).init({ + lng: locale, + fallbackLng: "en", + backend: { + loadPath: `${window.glob.assetPath}/translations/{{lng}}/{{ns}}.json` + }, + returnEmptyString: false + }); } export const t = i18next.t; diff --git a/src/public/app/services/image.ts b/src/public/app/services/image.ts index deedc7c0e..3cf1424d5 100644 --- a/src/public/app/services/image.ts +++ b/src/public/app/services/image.ts @@ -3,20 +3,19 @@ import toastService from "./toast.js"; function copyImageReferenceToClipboard($imageWrapper: JQuery) { try { - $imageWrapper.attr('contenteditable', 'true'); + $imageWrapper.attr("contenteditable", "true"); selectImage($imageWrapper.get(0)); - const success = document.execCommand('copy'); + const success = document.execCommand("copy"); if (success) { toastService.showMessage(t("image.copied-to-clipboard")); } else { toastService.showAndLogError(t("image.cannot-copy")); } - } - finally { + } finally { window.getSelection()?.removeAllRanges(); - $imageWrapper.removeAttr('contenteditable'); + $imageWrapper.removeAttr("contenteditable"); } } @@ -24,7 +23,7 @@ function selectImage(element: HTMLElement | undefined) { if (!element) { return; } - + const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(element); diff --git a/src/public/app/services/import.ts b/src/public/app/services/import.ts index b63fde8de..77b6d489e 100644 --- a/src/public/app/services/import.ts +++ b/src/public/app/services/import.ts @@ -6,7 +6,7 @@ import appContext from "../components/app_context.js"; import { t } from "./i18n.js"; export async function uploadFiles(entityType: string, parentNoteId: string, files: string[], options: Record) { - if (!['notes', 'attachments'].includes(entityType)) { + if (!["notes", "attachments"].includes(entityType)) { throw new Error(`Unrecognized import entity type '${entityType}'.`); } @@ -21,9 +21,9 @@ export async function uploadFiles(entityType: string, parentNoteId: string, file counter++; const formData = new FormData(); - formData.append('upload', file); - formData.append('taskId', taskId); - formData.append('last', counter === files.length ? "true" : "false"); + formData.append("upload", file); + formData.append("taskId", taskId); + formData.append("last", counter === files.length ? "true" : "false"); for (const key in options) { formData.append(key, options[key]); @@ -33,14 +33,14 @@ export async function uploadFiles(entityType: string, parentNoteId: string, file url: `${window.glob.baseApiUrl}notes/${parentNoteId}/${entityType}-import`, headers: await server.getHeaders(), data: formData, - dataType: 'json', - type: 'POST', + dataType: "json", + type: "POST", timeout: 60 * 60 * 1000, error: function (xhr) { toastService.showError(t("import.failed", { message: xhr.responseText })); }, contentType: false, // NEEDED, DON'T REMOVE THIS - processData: false, // NEEDED, DON'T REMOVE THIS + processData: false // NEEDED, DON'T REMOVE THIS }); } } @@ -54,17 +54,17 @@ function makeToast(id: string, message: string): ToastOptions { }; } -ws.subscribeToMessages(async message => { - if (message.taskType !== 'importNotes') { +ws.subscribeToMessages(async (message) => { + if (message.taskType !== "importNotes") { return; } - if (message.type === 'taskError') { + if (message.type === "taskError") { toastService.closePersistent(message.taskId); toastService.showError(message.message); - } else if (message.type === 'taskProgressCount') { + } else if (message.type === "taskProgressCount") { toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount }))); - } else if (message.type === 'taskSucceeded') { + } else if (message.type === "taskSucceeded") { const toast = makeToast(message.taskId, t("import.successful")); toast.closeAfter = 5000; @@ -76,17 +76,17 @@ ws.subscribeToMessages(async message => { } }); -ws.subscribeToMessages(async message => { - if (message.taskType !== 'importAttachments') { +ws.subscribeToMessages(async (message) => { + if (message.taskType !== "importAttachments") { return; } - if (message.type === 'taskError') { + if (message.type === "taskError") { toastService.closePersistent(message.taskId); toastService.showError(message.message); - } else if (message.type === 'taskProgressCount') { + } else if (message.type === "taskProgressCount") { toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount }))); - } else if (message.type === 'taskSucceeded') { + } else if (message.type === "taskSucceeded") { const toast = makeToast(message.taskId, t("import.successful")); toast.closeAfter = 5000; @@ -95,7 +95,7 @@ ws.subscribeToMessages(async message => { if (message.result.parentNoteId) { await appContext.tabManager.getActiveContext().setNote(message.result.importedNoteId, { viewScope: { - viewMode: 'attachments' + viewMode: "attachments" } }); } diff --git a/src/public/app/services/keyboard_actions.ts b/src/public/app/services/keyboard_actions.ts index bf5403706..5cadd2219 100644 --- a/src/public/app/services/keyboard_actions.ts +++ b/src/public/app/services/keyboard_actions.ts @@ -7,115 +7,114 @@ const keyboardActionRepo: Record = {}; // TODO: Deduplicate with server. export interface Action { - actionName: CommandNames; - effectiveShortcuts: string[]; - scope: string; + actionName: CommandNames; + effectiveShortcuts: string[]; + scope: string; } -const keyboardActionsLoaded = server.get('keyboard-actions').then(actions => { - actions = actions.filter(a => !!a.actionName); // filter out separators +const keyboardActionsLoaded = server.get("keyboard-actions").then((actions) => { + actions = actions.filter((a) => !!a.actionName); // filter out separators - for (const action of actions) { - action.effectiveShortcuts = action.effectiveShortcuts.filter(shortcut => !shortcut.startsWith("global:")); + for (const action of actions) { + action.effectiveShortcuts = action.effectiveShortcuts.filter((shortcut) => !shortcut.startsWith("global:")); - keyboardActionRepo[action.actionName] = action; - } + keyboardActionRepo[action.actionName] = action; + } - return actions; + return actions; }); async function getActions() { - return await keyboardActionsLoaded; + return await keyboardActionsLoaded; } async function getActionsForScope(scope: string) { - const actions = await keyboardActionsLoaded; + const actions = await keyboardActionsLoaded; - return actions.filter(action => action.scope === scope); + return actions.filter((action) => action.scope === scope); } async function setupActionsForElement(scope: string, $el: JQuery, component: Component) { - const actions = await getActionsForScope(scope); + const actions = await getActionsForScope(scope); - for (const action of actions) { - for (const shortcut of action.effectiveShortcuts) { - shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, {ntxId: appContext.tabManager.activeNtxId})); - } - } + for (const action of actions) { + for (const shortcut of action.effectiveShortcuts) { + shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId })); + } + } } -getActionsForScope("window").then(actions => { - for (const action of actions) { - for (const shortcut of action.effectiveShortcuts) { - shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, {ntxId: appContext.tabManager.activeNtxId})); - } - } +getActionsForScope("window").then((actions) => { + for (const action of actions) { + for (const shortcut of action.effectiveShortcuts) { + shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId })); + } + } }); async function getAction(actionName: string, silent = false) { - await keyboardActionsLoaded; + await keyboardActionsLoaded; - const action = keyboardActionRepo[actionName]; + const action = keyboardActionRepo[actionName]; - if (!action) { - if (silent) { - console.debug(`Cannot find action '${actionName}'`); - } - else { - throw new Error(`Cannot find action '${actionName}'`); - } - } + if (!action) { + if (silent) { + console.debug(`Cannot find action '${actionName}'`); + } else { + throw new Error(`Cannot find action '${actionName}'`); + } + } - return action; + return action; } function updateDisplayedShortcuts($container: JQuery) { - //@ts-ignore - //TODO: each() does not support async callbacks. - $container.find('kbd[data-command]').each(async (i, el) => { - const actionName = $(el).attr('data-command'); - if (!actionName) { - return; - } + //@ts-ignore + //TODO: each() does not support async callbacks. + $container.find("kbd[data-command]").each(async (i, el) => { + const actionName = $(el).attr("data-command"); + if (!actionName) { + return; + } - const action = await getAction(actionName, true); + const action = await getAction(actionName, true); - if (action) { - const keyboardActions = action.effectiveShortcuts.join(', '); + if (action) { + const keyboardActions = action.effectiveShortcuts.join(", "); - if (keyboardActions || $(el).text() !== "not set") { - $(el).text(keyboardActions); - } - } - }); + if (keyboardActions || $(el).text() !== "not set") { + $(el).text(keyboardActions); + } + } + }); - //@ts-ignore - //TODO: each() does not support async callbacks. - $container.find('[data-trigger-command]').each(async (i, el) => { - const actionName = $(el).attr('data-trigger-command'); - if (!actionName) { - return; - } - const action = await getAction(actionName, true); + //@ts-ignore + //TODO: each() does not support async callbacks. + $container.find("[data-trigger-command]").each(async (i, el) => { + const actionName = $(el).attr("data-trigger-command"); + if (!actionName) { + return; + } + const action = await getAction(actionName, true); - if (action) { - const title = $(el).attr('title'); - const shortcuts = action.effectiveShortcuts.join(', '); + if (action) { + const title = $(el).attr("title"); + const shortcuts = action.effectiveShortcuts.join(", "); - if (title?.includes(shortcuts)) { - return; - } + if (title?.includes(shortcuts)) { + return; + } - const newTitle = !title?.trim() ? shortcuts : `${title} (${shortcuts})`; + const newTitle = !title?.trim() ? shortcuts : `${title} (${shortcuts})`; - $(el).attr('title', newTitle); - } - }); + $(el).attr("title", newTitle); + } + }); } export default { - updateDisplayedShortcuts, - setupActionsForElement, - getActions, - getActionsForScope + updateDisplayedShortcuts, + setupActionsForElement, + getActions, + getActionsForScope }; diff --git a/src/public/app/services/library_loader.ts b/src/public/app/services/library_loader.ts index e7d448820..133a0e645 100644 --- a/src/public/app/services/library_loader.ts +++ b/src/public/app/services/library_loader.ts @@ -38,26 +38,16 @@ const CODE_MIRROR: Library = { return scriptsToLoad; }, - css: [ - "node_modules/codemirror/lib/codemirror.css", - "node_modules/codemirror/addon/lint/lint.css" - ] + css: ["node_modules/codemirror/lib/codemirror.css", "node_modules/codemirror/addon/lint/lint.css"] }; const ESLINT: Library = { - js: [ - "node_modules/eslint/bin/eslint.js" - ] + js: ["node_modules/eslint/bin/eslint.js"] }; const RELATION_MAP: Library = { - js: [ - "node_modules/jsplumb/dist/js/jsplumb.min.js", - "node_modules/panzoom/dist/panzoom.min.js" - ], - css: [ - "stylesheets/relation_map.css" - ] + js: ["node_modules/jsplumb/dist/js/jsplumb.min.js", "node_modules/panzoom/dist/panzoom.min.js"], + css: ["stylesheets/relation_map.css"] }; const PRINT_THIS: Library = { @@ -69,62 +59,44 @@ const CALENDAR_WIDGET: Library = { }; const KATEX: Library = { - js: [ "node_modules/katex/dist/katex.min.js", - "node_modules/katex/dist/contrib/mhchem.min.js", - "node_modules/katex/dist/contrib/auto-render.min.js" ], - css: [ "node_modules/katex/dist/katex.min.css" ] + js: ["node_modules/katex/dist/katex.min.js", "node_modules/katex/dist/contrib/mhchem.min.js", "node_modules/katex/dist/contrib/auto-render.min.js"], + css: ["node_modules/katex/dist/katex.min.css"] }; const WHEEL_ZOOM: Library = { - js: [ "node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"] + js: ["node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"] }; const FORCE_GRAPH: Library = { - js: [ "node_modules/force-graph/dist/force-graph.min.js"] + js: ["node_modules/force-graph/dist/force-graph.min.js"] }; const MERMAID: Library = { - js: [ - "node_modules/mermaid/dist/mermaid.min.js" - ] -} + js: ["node_modules/mermaid/dist/mermaid.min.js"] +}; /** * The ELK extension of Mermaid.js, which supports more advanced layouts. * See https://www.npmjs.com/package/@mermaid-js/layout-elk for more information. */ const MERMAID_ELK: Library = { - js: [ - "libraries/mermaid-elk/elk.min.js" - ] -} + js: ["libraries/mermaid-elk/elk.min.js"] +}; const EXCALIDRAW: Library = { - js: [ - "node_modules/react/umd/react.production.min.js", - "node_modules/react-dom/umd/react-dom.production.min.js", - "node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js", - ] + js: ["node_modules/react/umd/react.production.min.js", "node_modules/react-dom/umd/react-dom.production.min.js", "node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"] }; const MARKJS: Library = { - js: [ - "node_modules/mark.js/dist/jquery.mark.es6.min.js" - ] + js: ["node_modules/mark.js/dist/jquery.mark.es6.min.js"] }; const I18NEXT: Library = { - js: [ - "node_modules/i18next/i18next.min.js", - "node_modules/i18next-http-backend/i18nextHttpBackend.min.js" - ] + js: ["node_modules/i18next/i18next.min.js", "node_modules/i18next-http-backend/i18nextHttpBackend.min.js"] }; const MIND_ELIXIR: Library = { - js: [ - "node_modules/mind-elixir/dist/MindElixir.iife.js", - "node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs" - ] + js: ["node_modules/mind-elixir/dist/MindElixir.iife.js", "node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs"] }; const HIGHLIGHT_JS: Library = { @@ -155,7 +127,7 @@ const HIGHLIGHT_JS: Library = { async function requireLibrary(library: Library) { if (library.css) { - library.css.map(cssUrl => requireCss(cssUrl)); + library.css.map((cssUrl) => requireCss(cssUrl)); } if (library.js) { @@ -191,16 +163,14 @@ async function requireScript(url: string) { } async function requireCss(url: string, prependAssetPath = true) { - const cssLinks = Array - .from(document.querySelectorAll('link')) - .map(el => el.href); + const cssLinks = Array.from(document.querySelectorAll("link")).map((el) => el.href); - if (!cssLinks.some(l => l.endsWith(url))) { + if (!cssLinks.some((l) => l.endsWith(url))) { if (prependAssetPath) { url = `${window.glob.assetPath}/${url}`; } - $('head').append($('').attr('href', url)); + $("head").append($('').attr("href", url)); } } @@ -250,4 +220,4 @@ export default { I18NEXT, MIND_ELIXIR, HIGHLIGHT_JS -} +}; diff --git a/src/public/app/services/link.ts b/src/public/app/services/link.ts index 3cd5b1a70..b24107f5e 100644 --- a/src/public/app/services/link.ts +++ b/src/public/app/services/link.ts @@ -1,4 +1,4 @@ -import treeService from './tree.js'; +import treeService from "./tree.js"; import linkContextMenuService from "../menus/link_context_menu.js"; import appContext, { NoteCommandData } from "../components/app_context.js"; import froca from "./froca.js"; @@ -13,14 +13,14 @@ function getNotePathFromUrl(url: string) { async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) { let icon; - if (!viewMode || viewMode === 'default') { + if (!viewMode || viewMode === "default") { const note = await froca.getNote(noteId); icon = note?.getIcon(); - } else if (viewMode === 'source') { - icon = 'bx bx-code-curly'; - } else if (viewMode === 'attachments') { - icon = 'bx bx-file'; + } else if (viewMode === "source") { + icon = "bx bx-code-curly"; + } else if (viewMode === "attachments") { + icon = "bx bx-file"; } return icon; } @@ -72,14 +72,14 @@ async function createLink(notePath: string, options: CreateLinkOptions = {}) { } const viewScope = options.viewScope || {}; - const viewMode = viewScope.viewMode || 'default'; + const viewMode = viewScope.viewMode || "default"; let linkTitle = options.title; if (!linkTitle) { - if (viewMode === 'attachments' && viewScope.attachmentId) { + if (viewMode === "attachments" && viewScope.attachmentId) { const attachment = await froca.getAttachment(viewScope.attachmentId); - linkTitle = attachment ? attachment.title : '[missing attachment]'; + linkTitle = attachment ? attachment.title : "[missing attachment]"; } else if (noteId) { linkTitle = await treeService.getNoteTitle(noteId, parentNoteId); } @@ -87,7 +87,7 @@ async function createLink(notePath: string, options: CreateLinkOptions = {}) { const note = await froca.getNote(noteId); - if (autoConvertToImage && (note?.type && ['image', 'canvas', 'mermaid'].includes(note.type)) && viewMode === 'default') { + if (autoConvertToImage && note?.type && ["image", "canvas", "mermaid"].includes(note.type) && viewMode === "default") { const encodedTitle = encodeURIComponent(linkTitle || ""); return $("") @@ -101,9 +101,7 @@ async function createLink(notePath: string, options: CreateLinkOptions = {}) { let icon = await getLinkIcon(noteId, viewMode); if (icon) { - $container - .append($("").addClass(`bx ${icon}`)) - .append(" "); + $container.append($("").addClass(`bx ${icon}`)).append(" "); } } @@ -128,7 +126,7 @@ async function createLink(notePath: string, options: CreateLinkOptions = {}) { $container.append($noteLink); if (showNotePath) { - const resolvedPathSegments = await treeService.resolveNotePathToSegments(notePath) || []; + const resolvedPathSegments = (await treeService.resolveNotePathToSegments(notePath)) || []; resolvedPathSegments.pop(); // Remove last element const resolvedPath = resolvedPathSegments.join("/"); @@ -144,21 +142,23 @@ async function createLink(notePath: string, options: CreateLinkOptions = {}) { return $container; } -function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}: NoteCommandData) { +function calculateHash({ notePath, ntxId, hoistedNoteId, viewScope = {} }: NoteCommandData) { notePath = notePath || ""; const params = [ ntxId ? { ntxId: ntxId } : null, - (hoistedNoteId && hoistedNoteId !== 'root') ? { hoistedNoteId: hoistedNoteId } : null, - viewScope.viewMode && viewScope.viewMode !== 'default' ? { viewMode: viewScope.viewMode } : null, + hoistedNoteId && hoistedNoteId !== "root" ? { hoistedNoteId: hoistedNoteId } : null, + viewScope.viewMode && viewScope.viewMode !== "default" ? { viewMode: viewScope.viewMode } : null, viewScope.attachmentId ? { attachmentId: viewScope.attachmentId } : null - ].filter(p => !!p); + ].filter((p) => !!p); - const paramStr = params.map(pair => { - const name = Object.keys(pair)[0]; - const value = (pair as Record)[name]; + const paramStr = params + .map((pair) => { + const name = Object.keys(pair)[0]; + const value = (pair as Record)[name]; - return `${encodeURIComponent(name)}=${encodeURIComponent(value || "")}`; - }).join("&"); + return `${encodeURIComponent(name)}=${encodeURIComponent(value || "")}`; + }) + .join("&"); if (!notePath && !paramStr) { return ""; @@ -178,7 +178,7 @@ function parseNavigationStateFromUrl(url: string | undefined) { return {}; } - const hashIdx = url.indexOf('#'); + const hashIdx = url.indexOf("#"); if (hashIdx === -1) { return {}; } @@ -191,7 +191,7 @@ function parseNavigationStateFromUrl(url: string | undefined) { } const viewScope: ViewScope = { - viewMode: 'default' + viewMode: "default" }; let ntxId = null; let hoistedNoteId = null; @@ -203,13 +203,13 @@ function parseNavigationStateFromUrl(url: string | undefined) { name = decodeURIComponent(name); value = decodeURIComponent(value); - if (name === 'ntxId') { + if (name === "ntxId") { ntxId = value; - } else if (name === 'hoistedNoteId') { + } else if (name === "hoistedNoteId") { hoistedNoteId = value; - } else if (name === 'searchString') { + } else if (name === "searchString") { searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla - } else if (['viewMode', 'attachmentId'].includes(name)) { + } else if (["viewMode", "attachmentId"].includes(name)) { (viewScope as any)[name] = value; } else { console.warn(`Unrecognized hash parameter '${name}'.`); @@ -229,7 +229,7 @@ function parseNavigationStateFromUrl(url: string | undefined) { function goToLink(evt: MouseEvent | JQuery.ClickEvent) { const $link = $(evt.target as any).closest("a,.block-link"); - const hrefLink = $link.attr('href') || $link.attr('data-href'); + const hrefLink = $link.attr("href") || $link.attr("data-href"); return goToLinkExt(evt, hrefLink, $link); } @@ -246,7 +246,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und return handleFootnote(hrefLink, $link); } - const {notePath, viewScope} = parseNavigationStateFromUrl(hrefLink); + const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink); const ctrlKey = utils.isCtrlKey(evt); const isLeftClick = evt.which === 1; @@ -258,15 +258,15 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und if (notePath) { if (openInNewTab) { - appContext.tabManager.openTabWithNoteWithHoisting(notePath, {viewScope}); + appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope }); } else if (isLeftClick) { - const ntxId = $(evt.target as any).closest("[data-ntx-id]").attr("data-ntx-id"); + const ntxId = $(evt.target as any) + .closest("[data-ntx-id]") + .attr("data-ntx-id"); - const noteContext = ntxId - ? appContext.tabManager.getNoteContextById(ntxId) - : appContext.tabManager.getActiveContext(); + const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext(); - noteContext.setNote(notePath, {viewScope}).then(() => { + noteContext.setNote(notePath, { viewScope }).then(() => { if (noteContext !== appContext.tabManager.getActiveContext()) { appContext.tabManager.activateNoteContext(noteContext.ntxId); } @@ -276,27 +276,67 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und const withinEditLink = $link?.hasClass("ck-link-actions__preview"); const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0; - if (openInNewTab - || (withinEditLink && (leftClick || middleClick)) - || (outsideOfCKEditor && (leftClick || middleClick)) - ) { - if (hrefLink.toLowerCase().startsWith('http') || hrefLink.startsWith("api/")) { - window.open(hrefLink, '_blank'); - } else if ((hrefLink.toLowerCase().startsWith('file:') || hrefLink.toLowerCase().startsWith('geo:')) && utils.isElectron()) { - const electron = utils.dynamicRequire('electron'); + if (openInNewTab || (withinEditLink && (leftClick || middleClick)) || (outsideOfCKEditor && (leftClick || middleClick))) { + if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) { + window.open(hrefLink, "_blank"); + } else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) { + const electron = utils.dynamicRequire("electron"); electron.shell.openPath(hrefLink); } else { // Enable protocols supported by CKEditor 5 to be clickable. // Refer to `allowedProtocols` in https://github.com/TriliumNext/trilium-ckeditor5/blob/main/packages/ckeditor5-build-balloon-block/src/ckeditor.ts. // And be consistent with `allowedSchemes` in `src\services\html_sanitizer.ts` const allowedSchemes = [ - 'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'gemini', 'git', - 'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message', - 'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp', - 'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo' + "http", + "https", + "ftp", + "ftps", + "mailto", + "data", + "evernote", + "file", + "facetime", + "gemini", + "git", + "gopher", + "imap", + "irc", + "irc6", + "jabber", + "jar", + "lastfm", + "ldap", + "ldaps", + "magnet", + "message", + "mumble", + "nfs", + "onenote", + "pop", + "rmi", + "s3", + "sftp", + "skype", + "sms", + "spotify", + "steam", + "svn", + "udp", + "view-source", + "vlc", + "vnc", + "ws", + "wss", + "xmpp", + "jdbc", + "slack", + "tel", + "smb", + "zotero", + "geo" ]; - if (allowedSchemes.some(protocol => hrefLink.toLowerCase().startsWith(protocol+':'))){ - window.open(hrefLink, '_blank'); + if (allowedSchemes.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) { + window.open(hrefLink, "_blank"); } } } @@ -313,10 +353,9 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und * @returns whether the event should be consumed or not. */ function handleFootnote(hrefLink: string, $link: JQuery) { - const el = $link.closest(".ck-content") - .find(hrefLink)[0]; + const el = $link.closest(".ck-content").find(hrefLink)[0]; if (el) { - el.scrollIntoView({ behavior: "smooth", block: "center" }) + el.scrollIntoView({ behavior: "smooth", block: "center" }); } return true; } @@ -337,7 +376,7 @@ function linkContextMenu(e: PointerEvent) { } async function loadReferenceLinkTitle($el: JQuery, href: string | null | undefined = null) { - const $link = $el[0].tagName === 'A' ? $el : $el.find("a"); + const $link = $el[0].tagName === "A" ? $el : $el.find("a"); href = href || $link.attr("href"); if (!href) { @@ -345,7 +384,7 @@ async function loadReferenceLinkTitle($el: JQuery, href: string | n return; } - const {noteId, viewScope} = parseNavigationStateFromUrl(href); + const { noteId, viewScope } = parseNavigationStateFromUrl(href); if (!noteId) { console.warn("Missing note ID."); return; @@ -370,7 +409,7 @@ async function loadReferenceLinkTitle($el: JQuery, href: string | n } async function getReferenceLinkTitle(href: string) { - const {noteId, viewScope} = parseNavigationStateFromUrl(href); + const { noteId, viewScope } = parseNavigationStateFromUrl(href); if (!noteId) { return "[missing note]"; } @@ -380,7 +419,7 @@ async function getReferenceLinkTitle(href: string) { return "[missing note]"; } - if (viewScope?.viewMode === 'attachments' && viewScope?.attachmentId) { + if (viewScope?.viewMode === "attachments" && viewScope?.attachmentId) { const attachment = await note.getAttachmentById(viewScope.attachmentId); return attachment ? attachment.title : "[missing attachment]"; @@ -390,7 +429,7 @@ async function getReferenceLinkTitle(href: string) { } function getReferenceLinkTitleSync(href: string) { - const {noteId, viewScope} = parseNavigationStateFromUrl(href); + const { noteId, viewScope } = parseNavigationStateFromUrl(href); if (!noteId) { return "[missing note]"; } @@ -400,12 +439,12 @@ function getReferenceLinkTitleSync(href: string) { return "[missing note]"; } - if (viewScope?.viewMode === 'attachments' && viewScope?.attachmentId) { + if (viewScope?.viewMode === "attachments" && viewScope?.attachmentId) { if (!note.attachments) { return "[loading title...]"; } - const attachment = note.attachments.find(att => att.attachmentId === viewScope.attachmentId); + const attachment = note.attachments.find((att) => att.attachmentId === viewScope.attachmentId); return attachment ? attachment.title : "[missing attachment]"; } else { @@ -415,27 +454,27 @@ function getReferenceLinkTitleSync(href: string) { // TODO: Check why the event is not supported. //@ts-ignore -$(document).on('click', "a", goToLink); +$(document).on("click", "a", goToLink); // TODO: Check why the event is not supported. //@ts-ignore -$(document).on('auxclick', "a", goToLink); // to handle the middle button +$(document).on("auxclick", "a", goToLink); // to handle the middle button // TODO: Check why the event is not supported. //@ts-ignore -$(document).on('contextmenu', 'a', linkContextMenu); -$(document).on('dblclick', "a", e => { +$(document).on("contextmenu", "a", linkContextMenu); +$(document).on("dblclick", "a", (e) => { e.preventDefault(); e.stopPropagation(); const $link = $(e.target).closest("a"); - const address = $link.attr('href'); + const address = $link.attr("href"); - if (address && address.startsWith('http')) { - window.open(address, '_blank'); + if (address && address.startsWith("http")) { + window.open(address, "_blank"); } }); -$(document).on('mousedown', 'a', e => { +$(document).on("mousedown", "a", (e) => { if (e.which === 2) { // prevent paste on middle click // https://github.com/zadam/trilium/issues/2995 diff --git a/src/public/app/services/load_results.ts b/src/public/app/services/load_results.ts index 12fa2dbd4..b0a5b2eeb 100644 --- a/src/public/app/services/load_results.ts +++ b/src/public/app/services/load_results.ts @@ -47,13 +47,13 @@ interface ContentNoteIdToComponentIdRow { } type EntityRowMappings = { - "notes": NoteRow, - "branches": BranchRow, - "attributes": AttributeRow, - "options": OptionRow, - "revisions": RevisionRow, - "note_reordering": NoteReorderingRow -} + notes: NoteRow; + branches: BranchRow; + attributes: AttributeRow; + options: OptionRow; + revisions: RevisionRow; + note_reordering: NoteReorderingRow; +}; export type EntityRowNames = keyof EntityRowMappings; @@ -72,7 +72,7 @@ export default class LoadResults { constructor(entityChanges: EntityChange[]) { const entities: Record> = {}; - for (const {entityId, entityName, entity} of entityChanges) { + for (const { entityId, entityName, entity } of entityChanges) { if (entity) { entities[entityName] = entities[entityName] || []; entities[entityName][entityId] = entity; @@ -99,7 +99,7 @@ export default class LoadResults { } getEntityRow(entityName: T, entityId: string): EntityRowMappings[T] { - return (this.entities[entityName]?.[entityId]); + return this.entities[entityName]?.[entityId]; } addNote(noteId: string, componentId?: string | null) { @@ -119,13 +119,11 @@ export default class LoadResults { } addBranch(branchId: string, componentId: string) { - this.branchRows.push({branchId, componentId}); + this.branchRows.push({ branchId, componentId }); } getBranchRows() { - return this.branchRows - .map(row => this.getEntityRow("branches", row.branchId)) - .filter(branch => !!branch); + return this.branchRows.map((row) => this.getEntityRow("branches", row.branchId)).filter((branch) => !!branch); } addNoteReordering(parentNoteId: string, componentId: string) { @@ -137,22 +135,22 @@ export default class LoadResults { } addAttribute(attributeId: string, componentId: string) { - this.attributeRows.push({attributeId, componentId}); + this.attributeRows.push({ attributeId, componentId }); } - getAttributeRows(componentId = 'none'): AttributeRow[] { + getAttributeRows(componentId = "none"): AttributeRow[] { return this.attributeRows - .filter(row => row.componentId !== componentId) - .map(row => this.getEntityRow("attributes", row.attributeId)) - .filter(attr => !!attr) as AttributeRow[]; + .filter((row) => row.componentId !== componentId) + .map((row) => this.getEntityRow("attributes", row.attributeId)) + .filter((attr) => !!attr) as AttributeRow[]; } addRevision(revisionId: string, noteId?: string, componentId?: string | null) { - this.revisionRows.push({revisionId, noteId, componentId}); + this.revisionRows.push({ revisionId, noteId, componentId }); } hasRevisionForNote(noteId: string) { - return !!this.revisionRows.find(row => row.noteId === noteId); + return !!this.revisionRows.find((row) => row.noteId === noteId); } getNoteIds() { @@ -165,11 +163,11 @@ export default class LoadResults { } const componentIds = this.noteIdToComponentId[noteId]; - return componentIds && componentIds.find(sId => sId !== componentId) !== undefined; + return componentIds && componentIds.find((sId) => sId !== componentId) !== undefined; } addNoteContent(noteId: string, componentId: string) { - this.contentNoteIdToComponentId.push({noteId, componentId}); + this.contentNoteIdToComponentId.push({ noteId, componentId }); } isNoteContentReloaded(noteId: string, componentId?: string) { @@ -177,7 +175,7 @@ export default class LoadResults { return false; } - return this.contentNoteIdToComponentId.find(l => l.noteId === noteId && l.componentId !== componentId); + return this.contentNoteIdToComponentId.find((l) => l.noteId === noteId && l.componentId !== componentId); } addOption(name: string) { @@ -205,25 +203,23 @@ export default class LoadResults { * notably changes in note itself should not have any effect on attributes */ hasAttributeRelatedChanges() { - return this.branchRows.length > 0 - || this.attributeRows.length > 0; + return this.branchRows.length > 0 || this.attributeRows.length > 0; } isEmpty() { - return Object.keys(this.noteIdToComponentId).length === 0 - && this.branchRows.length === 0 - && this.attributeRows.length === 0 - && this.noteReorderings.length === 0 - && this.revisionRows.length === 0 - && this.contentNoteIdToComponentId.length === 0 - && this.optionNames.length === 0 - && this.attachmentRows.length === 0; + return ( + Object.keys(this.noteIdToComponentId).length === 0 && + this.branchRows.length === 0 && + this.attributeRows.length === 0 && + this.noteReorderings.length === 0 && + this.revisionRows.length === 0 && + this.contentNoteIdToComponentId.length === 0 && + this.optionNames.length === 0 && + this.attachmentRows.length === 0 + ); } isEmptyForTree() { - return Object.keys(this.noteIdToComponentId).length === 0 - && this.branchRows.length === 0 - && this.attributeRows.length === 0 - && this.noteReorderings.length === 0; + return Object.keys(this.noteIdToComponentId).length === 0 && this.branchRows.length === 0 && this.attributeRows.length === 0 && this.noteReorderings.length === 0; } } diff --git a/src/public/app/services/mac_init.ts b/src/public/app/services/mac_init.ts index 259b412f1..c864b3cad 100644 --- a/src/public/app/services/mac_init.ts +++ b/src/public/app/services/mac_init.ts @@ -6,12 +6,12 @@ import shortcutService from "./shortcuts.js"; function init() { if (utils.isElectron() && utils.isMac()) { - shortcutService.bindGlobalShortcut('meta+c', () => exec("copy")); - shortcutService.bindGlobalShortcut('meta+v', () => exec('paste')); - shortcutService.bindGlobalShortcut('meta+x', () => exec('cut')); - shortcutService.bindGlobalShortcut('meta+a', () => exec('selectAll')); - shortcutService.bindGlobalShortcut('meta+z', () => exec('undo')); - shortcutService.bindGlobalShortcut('meta+y', () => exec('redo')); + shortcutService.bindGlobalShortcut("meta+c", () => exec("copy")); + shortcutService.bindGlobalShortcut("meta+v", () => exec("paste")); + shortcutService.bindGlobalShortcut("meta+x", () => exec("cut")); + shortcutService.bindGlobalShortcut("meta+a", () => exec("selectAll")); + shortcutService.bindGlobalShortcut("meta+z", () => exec("undo")); + shortcutService.bindGlobalShortcut("meta+y", () => exec("redo")); } } @@ -23,4 +23,4 @@ function exec(cmd: string) { export default { init -} +}; diff --git a/src/public/app/services/mermaid.ts b/src/public/app/services/mermaid.ts index e553e89e2..16761bbb5 100644 --- a/src/public/app/services/mermaid.ts +++ b/src/public/app/services/mermaid.ts @@ -5,10 +5,10 @@ let elkLoaded = false; /** * Determines whether the ELK extension of Mermaid.js needs to be loaded (which is a relatively large library), based on the * front-matter of the diagram and loads the library if needed. - * + * *

    * If the library has already been loaded or the diagram does not require it, the method will exit immediately. - * + * * @param mermaidContent the plain text of the mermaid diagram, potentially including a frontmatter. */ export async function loadElkIfNeeded(mermaidContent: string) { @@ -18,11 +18,11 @@ export async function loadElkIfNeeded(mermaidContent: string) { } const parsedContent = await mermaid.parse(mermaidContent, { - suppressErrors: true + suppressErrors: true }); if (parsedContent?.config?.layout === "elk") { elkLoaded = true; await library_loader.requireLibrary(library_loader.MERMAID_ELK); mermaid.registerLayoutLoaders(MERMAID_ELK); - } -} \ No newline at end of file + } +} diff --git a/src/public/app/services/mime_types.ts b/src/public/app/services/mime_types.ts index 8f8a0e5f4..09e3a0f20 100644 --- a/src/public/app/services/mime_types.ts +++ b/src/public/app/services/mime_types.ts @@ -22,7 +22,7 @@ interface MimeTypeDefinition { } interface MimeType extends MimeTypeDefinition { - enabled: boolean + enabled: boolean; } const MIME_TYPES_DICT: MimeTypeDefinition[] = [ @@ -192,11 +192,10 @@ let mimeTypes: MimeType[] | null = null; function loadMimeTypes() { mimeTypes = JSON.parse(JSON.stringify(MIME_TYPES_DICT)) as MimeType[]; // clone - const enabledMimeTypes = options.getJson('codeNotesMimeTypes') - || MIME_TYPES_DICT.filter(mt => mt.default).map(mt => mt.mime); + const enabledMimeTypes = options.getJson("codeNotesMimeTypes") || MIME_TYPES_DICT.filter((mt) => mt.default).map((mt) => mt.mime); for (const mt of mimeTypes) { - mt.enabled = enabledMimeTypes.includes(mt.mime) || mt.mime === 'text/plain'; // text/plain is always enabled + mt.enabled = enabledMimeTypes.includes(mt.mime) || mt.mime === "text/plain"; // text/plain is always enabled } } @@ -242,8 +241,7 @@ function getHighlightJsNameForMime(mimeType: string) { * @returns the normalized MIME type (e.g. `text-c-src`). */ function normalizeMimeTypeForCKEditor(mimeType: string) { - return mimeType.toLowerCase() - .replace(/[\W_]+/g,"-"); + return mimeType.toLowerCase().replace(/[\W_]+/g, "-"); } export default { @@ -252,4 +250,4 @@ export default { loadMimeTypes, getHighlightJsNameForMime, normalizeMimeTypeForCKEditor -} +}; diff --git a/src/public/app/services/note_attribute_cache.ts b/src/public/app/services/note_attribute_cache.ts index 955744d71..ae3993cfb 100644 --- a/src/public/app/services/note_attribute_cache.ts +++ b/src/public/app/services/note_attribute_cache.ts @@ -9,7 +9,7 @@ import FAttribute from "../entities/fattribute.js"; */ class NoteAttributeCache { attributes: Record; - + constructor() { this.attributes = {}; } diff --git a/src/public/app/services/note_autocomplete.ts b/src/public/app/services/note_autocomplete.ts index 67a01969f..7b3defdb2 100644 --- a/src/public/app/services/note_autocomplete.ts +++ b/src/public/app/services/note_autocomplete.ts @@ -1,7 +1,7 @@ import server from "./server.js"; import appContext from "../components/app_context.js"; -import utils from './utils.js'; -import noteCreateService from './note_create.js'; +import utils from "./utils.js"; +import noteCreateService from "./note_create.js"; import froca from "./froca.js"; import { t } from "./i18n.js"; @@ -31,36 +31,42 @@ interface Options { async function autocompleteSourceForCKEditor(queryText: string) { return await new Promise((res, rej) => { - autocompleteSource(queryText, rows => { - res(rows.map(row => { - return { - action: row.action, - noteTitle: row.noteTitle, - id: `@${row.notePathTitle}`, - name: row.notePathTitle || "", - link: `#${row.notePath}`, - notePath: row.notePath, - highlightedNotePathTitle: row.highlightedNotePathTitle - } - })); - }, { - allowCreatingNotes: true - }); + autocompleteSource( + queryText, + (rows) => { + res( + rows.map((row) => { + return { + action: row.action, + noteTitle: row.noteTitle, + id: `@${row.notePathTitle}`, + name: row.notePathTitle || "", + link: `#${row.notePath}`, + notePath: row.notePath, + highlightedNotePathTitle: row.highlightedNotePathTitle + }; + }) + ); + }, + { + allowCreatingNotes: true + } + ); }); } async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void, options: Options = {}) { const fastSearch = options.fastSearch === false ? false : true; if (fastSearch === false) { - if (term.trim().length === 0){ + if (term.trim().length === 0) { return; } - cb( - [{ + cb([ + { noteTitle: term, highlightedNotePathTitle: t("quick-search.searching") - }] - ); + } + ]); } const activeNoteId = appContext.tabManager.getActiveContextNoteId(); @@ -69,9 +75,9 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void if (term.trim().length >= 1 && options.allowCreatingNotes) { results = [ { - action: 'create-note', + action: "create-note", noteTitle: term, - parentNoteId: activeNoteId || 'root', + parentNoteId: activeNoteId || "root", highlightedNotePathTitle: t("note_autocomplete.create-note", { term }) } as Suggestion ].concat(results); @@ -80,7 +86,7 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void if (term.trim().length >= 1 && options.allowJumpToSearchNotes) { results = results.concat([ { - action: 'search-notes', + action: "search-notes", noteTitle: term, highlightedNotePathTitle: `${t("note_autocomplete.search-for", { term })} Ctrl+Enter` } @@ -90,7 +96,7 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) { results = [ { - action: 'external-link', + action: "external-link", externalLink: term, highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term }) } as Suggestion @@ -102,42 +108,42 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void function clearText($el: JQuery) { $el.setSelectedNotePath(""); - $el.autocomplete("val", "").trigger('change'); + $el.autocomplete("val", "").trigger("change"); } function setText($el: JQuery, text: string) { $el.setSelectedNotePath(""); - $el - .autocomplete("val", text.trim()) - .autocomplete("open"); + $el.autocomplete("val", text.trim()).autocomplete("open"); } -function showRecentNotes($el:JQuery) { +function showRecentNotes($el: JQuery) { $el.setSelectedNotePath(""); $el.autocomplete("val", ""); - $el.autocomplete('open'); - $el.trigger('focus'); + $el.autocomplete("open"); + $el.trigger("focus"); } -function fullTextSearch($el: JQuery, options: Options){ - const searchString = $el.autocomplete('val') as unknown as string; +function fullTextSearch($el: JQuery, options: Options) { + const searchString = $el.autocomplete("val") as unknown as string; if (options.fastSearch === false || searchString?.trim().length === 0) { return; } - $el.trigger('focus'); + $el.trigger("focus"); options.fastSearch = false; - $el.autocomplete('val', ''); - $el.autocomplete() + $el.autocomplete("val", ""); + $el.autocomplete(); $el.setSelectedNotePath(""); - $el.autocomplete('val', searchString); + $el.autocomplete("val", searchString); // Set a delay to avoid resetting to true before full text search (await server.get) is called. - setTimeout(() => { options.fastSearch = true; }, 100); + setTimeout(() => { + options.fastSearch = true; + }, 100); } function initNoteAutocomplete($el: JQuery, options?: Options) { if ($el.hasClass("note-autocomplete-input")) { // clear any event listener added in previous invocation of this function - $el.off('autocomplete:noteselected'); + $el.off("autocomplete:noteselected"); return $el; } @@ -146,20 +152,15 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { $el.addClass("note-autocomplete-input"); - const $clearTextButton = $("

    `; class NoteListRenderer { - private $noteList: JQuery; private parentNote: FNote; @@ -181,7 +180,7 @@ class NoteListRenderer { this.parentNote = parentNote; const includedNoteIds = this.getIncludedNoteIds(); - this.noteIds = noteIds.filter(noteId => !includedNoteIds.has(noteId) && noteId !== '_hidden'); + this.noteIds = noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); if (this.noteIds.length === 0) { return; @@ -190,17 +189,17 @@ class NoteListRenderer { $parent.append(this.$noteList); this.page = 1; - this.pageSize = parseInt(parentNote.getLabelValue('pageSize') || ""); + this.pageSize = parseInt(parentNote.getLabelValue("pageSize") || ""); if (!this.pageSize || this.pageSize < 1) { this.pageSize = 20; } - this.viewType = parentNote.getLabelValue('viewType'); + this.viewType = parentNote.getLabelValue("viewType"); - if (!['list', 'grid'].includes(this.viewType || "")) { + if (!["list", "grid"].includes(this.viewType || "")) { // when not explicitly set, decide based on the note type - this.viewType = parentNote.type === 'search' ? 'list' : 'grid'; + this.viewType = parentNote.type === "search" ? "list" : "grid"; } this.$noteList.addClass(`${this.viewType}-view`); @@ -211,11 +210,9 @@ class NoteListRenderer { /** @returns {Set} list of noteIds included (images, included notes) in the parent note and which * don't have to be shown in the note list. */ getIncludedNoteIds() { - const includedLinks = this.parentNote - ? this.parentNote.getRelations().filter(rel => rel.name === 'imageLink' || rel.name === 'includeNoteLink') - : []; + const includedLinks = this.parentNote ? this.parentNote.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : []; - return new Set(includedLinks.map(rel => rel.value)); + return new Set(includedLinks.map((rel) => rel.value)); } async renderList() { @@ -228,18 +225,16 @@ class NoteListRenderer { if (highlightedTokens.length > 0) { await libraryLoader.requireLibrary(libraryLoader.MARKJS); - const regex = highlightedTokens - .map(token => utils.escapeRegExp(token)) - .join("|"); + const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|"); - this.highlightRegex = new RegExp(regex, 'gi'); + this.highlightRegex = new RegExp(regex, "gi"); } else { this.highlightRegex = null; } this.$noteList.show(); - const $container = this.$noteList.find('.note-list-container').empty(); + const $container = this.$noteList.find(".note-list-container").empty(); const startIdx = (this.page - 1) * this.pageSize; const endIdx = startIdx + this.pageSize; @@ -248,7 +243,7 @@ class NoteListRenderer { const pageNotes = await froca.getNotes(pageNoteIds); for (const note of pageNotes) { - const $card = await this.renderNote(note, this.parentNote.isLabelTruthy('expanded')); + const $card = await this.renderNote(note, this.parentNote.isLabelTruthy("expanded")); $container.append($card); } @@ -259,7 +254,7 @@ class NoteListRenderer { } renderPager() { - const $pager = this.$noteList.find('.note-list-pager').empty(); + const $pager = this.$noteList.find(".note-list-pager").empty(); if (!this.page || !this.pageSize) { return; } @@ -279,18 +274,17 @@ class NoteListRenderer { $pager.append( i === this.page - ? $('').text(i).css('text-decoration', 'underline').css('font-weight', "bold") + ? $("").text(i).css("text-decoration", "underline").css("font-weight", "bold") : $('') - .text(i) - .attr("title", `Page of ${startIndex} - ${endIndex}`) - .on('click', () => { - this.page = i; - this.renderList(); - }), + .text(i) + .attr("title", `Page of ${startIndex} - ${endIndex}`) + .on("click", () => { + this.page = i; + this.renderList(); + }), "   " ); - } - else if (lastPrinted) { + } else if (lastPrinted) { $pager.append("...   "); lastPrinted = false; @@ -304,33 +298,34 @@ class NoteListRenderer { async renderNote(note: FNote, expand: boolean = false) { const $expander = $(''); - const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note); - const notePath = this.parentNote.type === 'search' - ? note.noteId // for search note parent, we want to display a non-search path - : `${this.parentNote.noteId}/${note.noteId}`; + const { $renderedAttributes } = await attributeRenderer.renderNormalAttributes(note); + const notePath = + this.parentNote.type === "search" + ? note.noteId // for search note parent, we want to display a non-search path + : `${this.parentNote.noteId}/${note.noteId}`; const $card = $('
    ') - .attr('data-note-id', note.noteId) + .attr("data-note-id", note.noteId) .append( $('
    ') .append($expander) .append($('').addClass(note.getIcon())) - .append(this.viewType === 'grid' - ? $('').text(await treeService.getNoteTitle(note.noteId, this.parentNote.noteId)) - : (await linkService.createLink(notePath, {showTooltip: false, showNotePath: this.showNotePath})) - .addClass("note-book-title") + .append( + this.viewType === "grid" + ? $('').text(await treeService.getNoteTitle(note.noteId, this.parentNote.noteId)) + : (await linkService.createLink(notePath, { showTooltip: false, showNotePath: this.showNotePath })).addClass("note-book-title") ) .append($renderedAttributes) ); - if (this.viewType === 'grid') { + if (this.viewType === "grid") { $card .addClass("block-link") .attr("data-href", `#${notePath}`) - .on('click', e => linkService.goToLink(e)); + .on("click", (e) => linkService.goToLink(e)); } - $expander.on('click', () => this.toggleContent($card, note, !$card.hasClass("expanded"))); + $expander.on("click", () => this.toggleContent($card, note, !$card.hasClass("expanded"))); if (this.highlightRegex) { $card.find(".note-book-title").markRegExp(this.highlightRegex, { @@ -347,22 +342,21 @@ class NoteListRenderer { } async toggleContent($card: JQuery, note: FNote, expand: boolean) { - if (this.viewType === 'list' && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) { + if (this.viewType === "list" && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) { return; } - const $expander = $card.find('> .note-book-header .note-expander'); + const $expander = $card.find("> .note-book-header .note-expander"); - if (expand || this.viewType === 'grid') { + if (expand || this.viewType === "grid") { $card.addClass("expanded"); $expander.addClass("bx-chevron-down").removeClass("bx-chevron-right"); - } - else { + } else { $card.removeClass("expanded"); $expander.addClass("bx-chevron-right").removeClass("bx-chevron-down"); } - if ((expand || this.viewType === 'grid') && $card.find('.note-book-content').length === 0) { + if ((expand || this.viewType === "grid") && $card.find(".note-book-content").length === 0) { $card.append(await this.renderNoteContent(note)); } } @@ -371,8 +365,8 @@ class NoteListRenderer { const $content = $('
    '); try { - const {$renderedContent, type} = await contentRenderer.getRenderedContent(note, { - trim: this.viewType === 'grid' // for grid only short content is needed + const { $renderedContent, type } = await contentRenderer.getRenderedContent(note, { + trim: this.viewType === "grid" // for grid only short content is needed }); if (this.highlightRegex) { @@ -393,11 +387,10 @@ class NoteListRenderer { $content.append("rendering error"); } - if (this.viewType === 'list') { - const imageLinks = note.getRelations('imageLink'); + if (this.viewType === "list") { + const imageLinks = note.getRelations("imageLink"); - const childNotes = (await note.getChildNotes()) - .filter(childNote => !imageLinks.find(rel => rel.value === childNote.noteId)); + const childNotes = (await note.getChildNotes()).filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId)); for (const childNote of childNotes) { $content.append(await this.renderNote(childNote)); diff --git a/src/public/app/services/note_tooltip.ts b/src/public/app/services/note_tooltip.ts index 2ae0f47b7..f456d7a63 100644 --- a/src/public/app/services/note_tooltip.ts +++ b/src/public/app/services/note_tooltip.ts @@ -12,7 +12,7 @@ function setupGlobalTooltip() { $(document).on("mouseenter", "a", mouseEnterHandler); // close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen - $(document).on("click", e => { + $(document).on("click", (e) => { if ($(e.target).closest(".note-tooltip").length) { // click within the tooltip shouldn't close it return; @@ -23,11 +23,11 @@ function setupGlobalTooltip() { } function cleanUpTooltips() { - $('.note-tooltip').remove(); + $(".note-tooltip").remove(); } function setupElementTooltip($el: JQuery) { - $el.on('mouseenter', mouseEnterHandler); + $el.on("mouseenter", mouseEnterHandler); } async function mouseEnterHandler(this: HTMLElement) { @@ -51,7 +51,7 @@ async function mouseEnterHandler(this: HTMLElement) { return; } - if (!notePath || !noteId || viewScope?.viewMode !== 'default') { + if (!notePath || !noteId || viewScope?.viewMode !== "default") { return; } @@ -67,13 +67,13 @@ async function mouseEnterHandler(this: HTMLElement) { if (url?.startsWith("#fn")) { renderPromise = renderFootnote($link, url); } else { - renderPromise = renderTooltip(await froca.getNote(noteId)) + renderPromise = renderTooltip(await froca.getNote(noteId)); } const [content] = await Promise.all([ renderPromise, // to reduce flicker due to accidental mouseover, cursor must stay for a bit over the link for tooltip to appear - new Promise(res => setTimeout(res, 500)) + new Promise((res) => setTimeout(res, 500)) ]); if (!content || utils.isHtmlEmpty(content)) { @@ -81,18 +81,18 @@ async function mouseEnterHandler(this: HTMLElement) { } const html = `
    ${content}
    `; - const tooltipClass = 'tooltip-' + Math.floor(Math.random() * 999_999_999); + const tooltipClass = "tooltip-" + Math.floor(Math.random() * 999_999_999); // we need to check if we're still hovering over the element // since the operation to get tooltip content was async, it is possible that // we now create tooltip which won't close because it won't receive mouseleave event if ($(this).filter(":hover").length > 0) { $(this).tooltip({ - container: 'body', + container: "body", // https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988 // with bottom this flickering happens a bit less - placement: 'bottom', - trigger: 'manual', + placement: "bottom", + trigger: "manual", //TODO: boundary No longer applicable? //boundary: 'window', title: html, @@ -103,7 +103,7 @@ async function mouseEnterHandler(this: HTMLElement) { }); cleanUpTooltips(); - $(this).tooltip('show'); + $(this).tooltip("show"); // Dismiss the tooltip immediately if a link was clicked inside the tooltip. $(`.${tooltipClass} a`).on("click", (e) => { @@ -121,7 +121,7 @@ async function mouseEnterHandler(this: HTMLElement) { } else { setTimeout(checkTooltip, 1000); } - } + }; setTimeout(checkTooltip, 1000); } @@ -142,12 +142,12 @@ async function renderTooltip(note: FNote | null) { const noteTitleWithPathAsSuffix = await treeService.getNoteTitleWithPathAsSuffix(bestNotePath); let content = ""; if (noteTitleWithPathAsSuffix) { - content = `
    ${noteTitleWithPathAsSuffix.prop('outerHTML')}
    `; + content = `
    ${noteTitleWithPathAsSuffix.prop("outerHTML")}
    `; } - const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note); + const { $renderedAttributes } = await attributeRenderer.renderNormalAttributes(note); - const {$renderedContent} = await contentRenderer.getRenderedContent(note, { + const { $renderedContent } = await contentRenderer.getRenderedContent(note, { tooltip: true, trim: true }); @@ -161,11 +161,11 @@ function renderFootnote($link: JQuery, url: string) { // A footnote text reference const footnoteRef = url.substring(3); const $footnoteContent = $link - .closest(".ck-content") // find the parent CK content - .find("> .footnote-section") // find the footnote section - .find(`a[href="#fnref${footnoteRef}"]`) // find the footnote link - .closest(".footnote-item") // find the parent container of the footnote - .find(".footnote-content"); // find the actual text content of the footnote + .closest(".ck-content") // find the parent CK content + .find("> .footnote-section") // find the footnote section + .find(`a[href="#fnref${footnoteRef}"]`) // find the footnote link + .closest(".footnote-item") // find the parent container of the footnote + .find(".footnote-content"); // find the actual text content of the footnote return $footnoteContent.html() || ""; } @@ -173,4 +173,4 @@ function renderFootnote($link: JQuery, url: string) { export default { setupGlobalTooltip, setupElementTooltip -} +}; diff --git a/src/public/app/services/note_types.ts b/src/public/app/services/note_types.ts index 0174c78a0..61b71775b 100644 --- a/src/public/app/services/note_types.ts +++ b/src/public/app/services/note_types.ts @@ -43,4 +43,4 @@ async function getNoteTypeItems(command?: NoteTypeCommandNames) { export default { getNoteTypeItems -} +}; diff --git a/src/public/app/services/open.ts b/src/public/app/services/open.ts index da41da3b1..7dab08a1a 100644 --- a/src/public/app/services/open.ts +++ b/src/public/app/services/open.ts @@ -1,14 +1,14 @@ import utils from "./utils.js"; import server from "./server.js"; -type ExecFunction = (command: string, cb: ((err: string, stdout: string, stderror: string) => void)) => void; +type ExecFunction = (command: string, cb: (err: string, stdout: string, stderror: string) => void) => void; interface TmpResponse { tmpFilePath: string; } function checkType(type: string) { - if (type !== 'notes' && type !== 'attachments') { + if (type !== "notes" && type !== "attachments") { throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`); } } @@ -27,7 +27,7 @@ function getOpenFileUrl(type: string, noteId: string) { function download(url: string) { if (utils.isElectron()) { - const remote = utils.dynamicRequire('@electron/remote'); + const remote = utils.dynamicRequire("@electron/remote"); remote.getCurrentWebContents().downloadURL(url); } else { @@ -36,13 +36,13 @@ function download(url: string) { } function downloadFileNote(noteId: string) { - const url = `${getFileUrl('notes', noteId)}?${Date.now()}`; // don't use cache + const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache download(url); } function downloadAttachment(attachmentId: string) { - const url = `${getFileUrl('attachments', attachmentId)}?${Date.now()}`; // don't use cache + const url = `${getFileUrl("attachments", attachmentId)}?${Date.now()}`; // don't use cache download(url); } @@ -55,12 +55,12 @@ async function openCustom(type: string, entityId: string, mime: string) { const resp = await server.post(`${type}/${entityId}/save-to-tmp-dir`); let filePath = resp.tmpFilePath; - const exec = utils.dynamicRequire('child_process').exec as ExecFunction; + const exec = utils.dynamicRequire("child_process").exec as ExecFunction; const platform = process.platform; - if (platform === 'linux') { + if (platform === "linux") { // we don't know which terminal is available, try in succession - const terminals = ['x-terminal-emulator', 'gnome-terminal', 'konsole', 'xterm', 'xfce4-terminal', 'mate-terminal', 'rxvt', 'terminator', 'terminology']; + const terminals = ["x-terminal-emulator", "gnome-terminal", "konsole", "xterm", "xfce4-terminal", "mate-terminal", "rxvt", "terminator", "terminology"]; const openFileWithTerminal = (terminal: string) => { const command = `${terminal} -e 'mimeopen -d "${filePath}"'`; console.log(`Open Note custom: ${command} `); @@ -77,9 +77,9 @@ async function openCustom(type: string, entityId: string, mime: string) { const searchTerminal = (index: number) => { const terminal = terminals[index]; if (!terminal) { - console.error('Open Note custom: No terminal found!'); + console.error("Open Note custom: No terminal found!"); // TODO: Remove {url: true} if not needed. - (open as any)(getFileUrl(type, entityId), {url: true}); + (open as any)(getFileUrl(type, entityId), { url: true }); return; } exec(`which ${terminal}`, (error, stdout, stderr) => { @@ -91,7 +91,7 @@ async function openCustom(type: string, entityId: string, mime: string) { }); }; searchTerminal(0); - } else if (platform === 'win32') { + } else if (platform === "win32") { if (filePath.indexOf("/") !== -1) { // Note that the path separator must be \ instead of / filePath = filePath.replace(/\//g, "\\"); @@ -102,7 +102,7 @@ async function openCustom(type: string, entityId: string, mime: string) { console.error("Open Note custom: ", err); // TODO: This appears to be broken, since getFileUrl expects two arguments, with the first one being the type. // Also don't know why {url: true} is passed. - (open as any)(getFileUrl(entityId), {url: true}); + (open as any)(getFileUrl(entityId), { url: true }); return; } }); @@ -110,15 +110,12 @@ async function openCustom(type: string, entityId: string, mime: string) { console.log('Currently "Open Note custom" only supports linux and windows systems'); // TODO: This appears to be broken, since getFileUrl expects two arguments, with the first one being the type. // Also don't know why {url: true} is passed. - (open as any)(getFileUrl(entityId), {url: true}); + (open as any)(getFileUrl(entityId), { url: true }); } } -const openNoteCustom = - async (noteId: string, mime: string) => await openCustom('notes', noteId, mime); -const openAttachmentCustom = - async (attachmentId: string, mime: string) => await openCustom('attachments', attachmentId, mime); - +const openNoteCustom = async (noteId: string, mime: string) => await openCustom("notes", noteId, mime); +const openAttachmentCustom = async (attachmentId: string, mime: string) => await openCustom("attachments", attachmentId, mime); function downloadRevision(noteId: string, revisionId: string) { const url = getUrlForDownload(`api/revisions/${revisionId}/download`); @@ -133,18 +130,14 @@ function getUrlForDownload(url: string) { if (utils.isElectron()) { // electron needs absolute URL, so we extract current host, port, protocol return `${getHost()}/${url}`; - } - else { + } else { // web server can be deployed on subdomain, so we need to use a relative path return url; } } function canOpenInBrowser(mime: string) { - return mime === "application/pdf" - || mime.startsWith("image") - || mime.startsWith("audio") - || mime.startsWith("video"); + return mime === "application/pdf" || mime.startsWith("image") || mime.startsWith("audio") || mime.startsWith("video"); } async function openExternally(type: string, entityId: string, mime: string) { @@ -153,15 +146,14 @@ async function openExternally(type: string, entityId: string, mime: string) { if (utils.isElectron()) { const resp = await server.post(`${type}/${entityId}/save-to-tmp-dir`); - const electron = utils.dynamicRequire('electron'); + const electron = utils.dynamicRequire("electron"); const res = await electron.shell.openPath(resp.tmpFilePath); if (res) { // fallback in case there's no default application for this file window.open(getFileUrl(type, entityId)); } - } - else { + } else { // allow browser to handle opening common file if (canOpenInBrowser(mime)) { window.open(getOpenFileUrl(type, entityId)); @@ -171,10 +163,8 @@ async function openExternally(type: string, entityId: string, mime: string) { } } -const openNoteExternally = - async (noteId: string, mime: string) => await openExternally('notes', noteId, mime); -const openAttachmentExternally = - async (attachmentId: string, mime: string) => await openExternally('attachments', attachmentId, mime); +const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime); +const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime); function getHost() { const url = new URL(window.location.href); @@ -184,17 +174,17 @@ function getHost() { async function openDirectory(directory: string) { try { if (utils.isElectron()) { - const electron = utils.dynamicRequire('electron'); + const electron = utils.dynamicRequire("electron"); const res = await electron.shell.openPath(directory); if (res) { - console.error('Failed to open directory:', res); + console.error("Failed to open directory:", res); } } else { - console.error('Not running in an Electron environment.'); + console.error("Not running in an Electron environment."); } } catch (err: any) { // Handle file system errors (e.g. path does not exist or is inaccessible) - console.error('Error:', err.message); + console.error("Error:", err.message); } } @@ -209,4 +199,4 @@ export default { openNoteCustom, openAttachmentCustom, openDirectory -} +}; diff --git a/src/public/app/services/options.ts b/src/public/app/services/options.ts index b6162ea1d..660861d17 100644 --- a/src/public/app/services/options.ts +++ b/src/public/app/services/options.ts @@ -1,4 +1,3 @@ - import server from "./server.js"; type OptionValue = number | string; @@ -8,7 +7,7 @@ class Options { private arr!: Record; constructor() { - this.initializedPromise = server.get>('options').then(data => this.load(data)); + this.initializedPromise = server.get>("options").then((data) => this.load(data)); } load(arr: Record) { @@ -30,8 +29,7 @@ class Options { } try { return JSON.parse(value); - } - catch (e) { + } catch (e) { return null; } } @@ -53,7 +51,7 @@ class Options { } is(key: string) { - return this.arr[key] === 'true'; + return this.arr[key] === "true"; } set(key: string, value: OptionValue) { diff --git a/src/public/app/services/promoted_attribute_definition_parser.ts b/src/public/app/services/promoted_attribute_definition_parser.ts index 35d0f5ad0..ca0095f60 100644 --- a/src/public/app/services/promoted_attribute_definition_parser.ts +++ b/src/public/app/services/promoted_attribute_definition_parser.ts @@ -11,35 +11,29 @@ interface DefinitionObject { } function parse(value: string) { - const tokens = value.split(',').map(t => t.trim()); + const tokens = value.split(",").map((t) => t.trim()); const defObj: DefinitionObject = {}; for (const token of tokens) { - if (token === 'promoted') { + if (token === "promoted") { defObj.isPromoted = true; - } - else if (['text', 'number', 'boolean', 'date', 'datetime', 'time', 'url'].includes(token)) { + } else if (["text", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) { defObj.labelType = token as LabelType; - } - else if (['single', 'multi'].includes(token)) { + } else if (["single", "multi"].includes(token)) { defObj.multiplicity = token as Multiplicity; - } - else if (token.startsWith('precision')) { - const chunks = token.split('='); + } else if (token.startsWith("precision")) { + const chunks = token.split("="); defObj.numberPrecision = parseInt(chunks[1]); - } - else if (token.startsWith('alias')) { - const chunks = token.split('='); + } else if (token.startsWith("alias")) { + const chunks = token.split("="); defObj.promotedAlias = chunks[1]; - } - else if (token.startsWith('inverse')) { - const chunks = token.split('='); + } else if (token.startsWith("inverse")) { + const chunks = token.split("="); defObj.inverseRelation = chunks[1]; - } - else { + } else { console.log("Unrecognized attribute definition token:", token); } } diff --git a/src/public/app/services/protected_session.ts b/src/public/app/services/protected_session.ts index 0b6b03c9d..1bf995d93 100644 --- a/src/public/app/services/protected_session.ts +++ b/src/public/app/services/protected_session.ts @@ -1,5 +1,5 @@ -import server from './server.js'; -import protectedSessionHolder from './protected_session_holder.js'; +import server from "./server.js"; +import protectedSessionHolder from "./protected_session_holder.js"; import toastService from "./toast.js"; import type { ToastOptions } from "./toast.js"; import ws from "./ws.js"; @@ -7,7 +7,7 @@ import appContext from "../components/app_context.js"; import froca from "./froca.js"; import utils from "./utils.js"; import options from "./options.js"; -import { t } from './i18n.js'; +import { t } from "./i18n.js"; let protectedSessionDeferred: JQuery.Deferred | null = null; @@ -19,8 +19,8 @@ interface Response { interface Message { taskId: string; data: { - protect: boolean - } + protect: boolean; + }; } async function leaveProtectedSession() { @@ -40,8 +40,7 @@ function enterProtectedSession() { if (protectedSessionHolder.isProtectedSessionAvailable()) { dfd.resolve(false); - } - else { + } else { // using deferred instead of promise because it allows resolving from the outside protectedSessionDeferred = dfd; @@ -61,7 +60,7 @@ async function reloadData() { } async function setupProtectedSession(password: string) { - const response = await server.post('login/protected', { password: password }); + const response = await server.post("login/protected", { password: password }); if (!response.success) { toastService.showError(t("protected_session.wrong_password"), 3000); @@ -71,13 +70,13 @@ async function setupProtectedSession(password: string) { protectedSessionHolder.enableProtectedSession(); } -ws.subscribeToMessages(async message => { - if (message.type === 'protectedSessionLogin') { +ws.subscribeToMessages(async (message) => { + if (message.type === "protectedSessionLogin") { await reloadData(); - await appContext.triggerEvent('frocaReloaded'); + await appContext.triggerEvent("frocaReloaded"); - appContext.triggerEvent('protectedSessionStarted'); + appContext.triggerEvent("protectedSessionStarted"); appContext.triggerCommand("closeProtectedSessionPasswordDialog"); @@ -87,8 +86,7 @@ ws.subscribeToMessages(async message => { } toastService.showMessage(t("protected_session.started")); - } - else if (message.type === 'protectedSessionLogout') { + } else if (message.type === "protectedSessionLogout") { utils.reloadFrontendApp(`Protected session logout`); } }); @@ -108,23 +106,23 @@ function makeToast(message: Message, title: string, text: string): ToastOptions }; } -ws.subscribeToMessages(async message => { - if (message.taskType !== 'protectNotes') { +ws.subscribeToMessages(async (message) => { + if (message.taskType !== "protectNotes") { return; } const isProtecting = message.data.protect; const title = isProtecting ? t("protected_session.protecting-title") : t("protected_session.unprotecting-title"); - - if (message.type === 'taskError') { + + if (message.type === "taskError") { toastService.closePersistent(message.taskId); toastService.showError(message.message); - } else if (message.type === 'taskProgressCount') { + } else if (message.type === "taskProgressCount") { const count = message.progressCount; - const text = ( isProtecting ? t("protected_session.protecting-in-progress", { count }) : t("protected_session.unprotecting-in-progress-count", { count })); + const text = isProtecting ? t("protected_session.protecting-in-progress", { count }) : t("protected_session.unprotecting-in-progress-count", { count }); toastService.showPersistent(makeToast(message, title, text)); - } else if (message.type === 'taskSucceeded') { - const text = (isProtecting ? t("protected_session.protecting-finished-successfully") : t("protected_session.unprotecting-finished-successfully")) + } else if (message.type === "taskSucceeded") { + const text = isProtecting ? t("protected_session.protecting-finished-successfully") : t("protected_session.unprotecting-finished-successfully"); const toast = makeToast(message, title, text); toast.closeAfter = 3000; diff --git a/src/public/app/services/render.ts b/src/public/app/services/render.ts index 9a998b573..5c48fd3f0 100644 --- a/src/public/app/services/render.ts +++ b/src/public/app/services/render.ts @@ -3,17 +3,15 @@ import bundleService, { Bundle } from "./bundle.js"; import FNote from "../entities/fnote.js"; async function render(note: FNote, $el: JQuery) { - const relations = note.getRelations('renderNote'); - const renderNoteIds = relations - .map(rel => rel.value) - .filter(noteId => noteId); + const relations = note.getRelations("renderNote"); + const renderNoteIds = relations.map((rel) => rel.value).filter((noteId) => noteId); $el.empty().toggle(renderNoteIds.length > 0); for (const renderNoteId of renderNoteIds) { const bundle = await server.post(`script/bundle/${renderNoteId}`); - const $scriptContainer = $('
    '); + const $scriptContainer = $("
    "); $el.append($scriptContainer); $scriptContainer.append(bundle.html); @@ -27,4 +25,4 @@ async function render(note: FNote, $el: JQuery) { export default { render -} +}; diff --git a/src/public/app/services/resizer.ts b/src/public/app/services/resizer.ts index 6acea693a..d6e828047 100644 --- a/src/public/app/services/resizer.ts +++ b/src/public/app/services/resizer.ts @@ -12,21 +12,21 @@ function setupLeftPaneResizer(leftPaneVisible: boolean) { $("#left-pane").toggle(leftPaneVisible); if (!leftPaneVisible) { - $("#rest-pane").css('width', '100%'); + $("#rest-pane").css("width", "100%"); return; } - let leftPaneWidth = options.getInt('leftPaneWidth'); + let leftPaneWidth = options.getInt("leftPaneWidth"); if (!leftPaneWidth || leftPaneWidth < 5) { leftPaneWidth = 5; } if (leftPaneVisible) { - leftInstance = Split(['#left-pane', '#rest-pane'], { + leftInstance = Split(["#left-pane", "#rest-pane"], { sizes: [leftPaneWidth, 100 - leftPaneWidth], gutterSize: 5, - onDragEnd: sizes => options.save('leftPaneWidth', Math.round(sizes[0])) + onDragEnd: (sizes) => options.save("leftPaneWidth", Math.round(sizes[0])) }); } } @@ -40,26 +40,26 @@ function setupRightPaneResizer() { const rightPaneVisible = $("#right-pane").is(":visible"); if (!rightPaneVisible) { - $("#center-pane").css('width', '100%'); + $("#center-pane").css("width", "100%"); return; } - let rightPaneWidth = options.getInt('rightPaneWidth'); + let rightPaneWidth = options.getInt("rightPaneWidth"); if (!rightPaneWidth || rightPaneWidth < 5) { rightPaneWidth = 5; } if (rightPaneVisible) { - rightInstance = Split(['#center-pane', '#right-pane'], { + rightInstance = Split(["#center-pane", "#right-pane"], { sizes: [100 - rightPaneWidth, rightPaneWidth], gutterSize: 5, - onDragEnd: sizes => options.save('rightPaneWidth', Math.round(sizes[1])) + onDragEnd: (sizes) => options.save("rightPaneWidth", Math.round(sizes[1])) }); } } export default { setupLeftPaneResizer, - setupRightPaneResizer, + setupRightPaneResizer }; diff --git a/src/public/app/services/script_context.ts b/src/public/app/services/script_context.ts index 7d76c12a7..a654fa3d9 100644 --- a/src/public/app/services/script_context.ts +++ b/src/public/app/services/script_context.ts @@ -1,6 +1,6 @@ -import FrontendScriptApi, { Entity } from './frontend_script_api.js'; -import utils from './utils.js'; -import froca from './froca.js'; +import FrontendScriptApi, { Entity } from "./frontend_script_api.js"; +import utils from "./utils.js"; +import froca from "./froca.js"; async function ScriptContext(startNoteId: string, allNoteIds: string[], originEntity: Entity | null = null, $container: JQuery | null = null) { const modules: Record = {}; @@ -16,21 +16,21 @@ async function ScriptContext(startNoteId: string, allNoteIds: string[], originEn return { modules: modules, - notes: utils.toObject(allNotes, note => [note.noteId, note]), - apis: utils.toObject(allNotes, note => [note.noteId, new FrontendScriptApi(startNote, note, originEntity, $container)]), + notes: utils.toObject(allNotes, (note) => [note.noteId, note]), + apis: utils.toObject(allNotes, (note) => [note.noteId, new FrontendScriptApi(startNote, note, originEntity, $container)]), require: (moduleNoteIds: string) => { return (moduleName: string) => { - const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId)); - const note = candidates.find(c => c.title === moduleName); + const candidates = allNotes.filter((note) => moduleNoteIds.includes(note.noteId)); + const note = candidates.find((c) => c.title === moduleName); if (!note) { throw new Error(`Could not find module note ${moduleName}`); } return modules[note.noteId].exports; - } + }; } }; } -export default ScriptContext; \ No newline at end of file +export default ScriptContext; diff --git a/src/public/app/services/search.ts b/src/public/app/services/search.ts index f3ca50d75..bc516ecb4 100644 --- a/src/public/app/services/search.ts +++ b/src/public/app/services/search.ts @@ -14,4 +14,4 @@ async function searchForNotes(searchString: string) { export default { searchForNoteIds, searchForNotes -} +}; diff --git a/src/public/app/services/server.ts b/src/public/app/services/server.ts index f1499c660..e15e3ba88 100644 --- a/src/public/app/services/server.ts +++ b/src/public/app/services/server.ts @@ -1,4 +1,4 @@ -import utils from './utils.js'; +import utils from "./utils.js"; import ValidationError from "./validation_error.js"; type Headers = Record; @@ -28,16 +28,16 @@ export interface StandardResponse { } async function getHeaders(headers?: Headers) { - const appContext = (await import('../components/app_context.js')).default; + const appContext = (await import("../components/app_context.js")).default; const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null; // headers need to be lowercase because node.js automatically converts them to lower case // also avoiding using underscores instead of dashes since nginx filters them out by default const allHeaders: Headers = { - 'trilium-component-id': glob.componentId, - 'trilium-local-now-datetime': utils.localNowDateTime(), - 'trilium-hoisted-note-id': activeNoteContext ? activeNoteContext.hoistedNoteId : null, - 'x-csrf-token': glob.csrfToken + "trilium-component-id": glob.componentId, + "trilium-local-now-datetime": utils.localNowDateTime(), + "trilium-hoisted-note-id": activeNoteContext ? activeNoteContext.hoistedNoteId : null, + "x-csrf-token": glob.csrfToken }; for (const headerName in headers) { @@ -55,41 +55,41 @@ async function getHeaders(headers?: Headers) { } async function getWithSilentNotFound(url: string, componentId?: string) { - return await call('GET', url, componentId, { silentNotFound: true }); + return await call("GET", url, componentId, { silentNotFound: true }); } async function get(url: string, componentId?: string) { - return await call('GET', url, componentId); + return await call("GET", url, componentId); } async function post(url: string, data?: unknown, componentId?: string) { - return await call('POST', url, componentId, { data }); + return await call("POST", url, componentId, { data }); } async function put(url: string, data?: unknown, componentId?: string) { - return await call('PUT', url, componentId, { data }); + return await call("PUT", url, componentId, { data }); } async function patch(url: string, data: unknown, componentId?: string) { - return await call('PATCH', url, componentId, { data }); + return await call("PATCH", url, componentId, { data }); } async function remove(url: string, componentId?: string) { - return await call('DELETE', url, componentId); + return await call("DELETE", url, componentId); } async function upload(url: string, fileToUpload: File) { const formData = new FormData(); - formData.append('upload', fileToUpload); + formData.append("upload", fileToUpload); return await $.ajax({ url: window.glob.baseApiUrl + url, headers: await getHeaders(), data: formData, - type: 'PUT', + type: "PUT", timeout: 60 * 60 * 1000, contentType: false, // NEEDED, DON'T REMOVE THIS - processData: false, // NEEDED, DON'T REMOVE THIS + processData: false // NEEDED, DON'T REMOVE THIS }); } @@ -108,35 +108,34 @@ async function call(method: string, url: string, componentId?: string, option let resp; const headers = await getHeaders({ - 'trilium-component-id': componentId + "trilium-component-id": componentId }); - const {data} = options; + const { data } = options; if (utils.isElectron()) { - const ipc = utils.dynamicRequire('electron').ipcRenderer; + const ipc = utils.dynamicRequire("electron").ipcRenderer; const requestId = idCounter++; - resp = await new Promise((resolve, reject) => { + resp = (await new Promise((resolve, reject) => { idToRequestMap[requestId] = { resolve, reject, silentNotFound: !!options.silentNotFound }; - ipc.send('server-request', { + ipc.send("server-request", { requestId: requestId, headers: headers, method: method, url: `/${window.glob.baseApiUrl}${url}`, data: data }); - }) as any; - } - else { + })) as any; + } else { resp = await ajax(url, method, data, headers, !!options.silentNotFound); } - const maxEntityChangeIdStr = resp.headers['trilium-max-entity-change-id']; + const maxEntityChangeIdStr = resp.headers["trilium-max-entity-change-id"]; if (maxEntityChangeIdStr && maxEntityChangeIdStr.trim()) { maxKnownEntityChangeId = Math.max(maxKnownEntityChangeId, parseInt(maxEntityChangeIdStr)); @@ -155,20 +154,24 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile success: (body, textStatus, jqXhr) => { const respHeaders: Headers = {}; - jqXhr.getAllResponseHeaders().trim().split(/[\r\n]+/).forEach(line => { - const parts = line.split(': '); - const header = parts.shift(); - if (header) { - respHeaders[header] = parts.join(': '); - } - }); + jqXhr + .getAllResponseHeaders() + .trim() + .split(/[\r\n]+/) + .forEach((line) => { + const parts = line.split(": "); + const header = parts.shift(); + if (header) { + respHeaders[header] = parts.join(": "); + } + }); res({ body, headers: respHeaders }); }, - error: async jqXhr => { + error: async (jqXhr) => { if (jqXhr.status === 0) { // don't report requests that are rejected by the browser, usually when the user is refreshing or going to a different page. rej("rejected by browser"); @@ -187,7 +190,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile try { options.data = JSON.stringify(data); } catch (e) { - console.log("Can't stringify data: ", data, " because of error: ", e) + console.log("Can't stringify data: ", data, " because of error: ", e); } options.contentType = "application/json"; } @@ -197,13 +200,12 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile } if (utils.isElectron()) { - const ipc = utils.dynamicRequire('electron').ipcRenderer; + const ipc = utils.dynamicRequire("electron").ipcRenderer; - ipc.on('server-response', async (event: string, arg: Arg) => { + ipc.on("server-response", async (event: string, arg: Arg) => { if (arg.statusCode >= 200 && arg.statusCode < 300) { handleSuccessfulResponse(arg); - } - else { + } else { if (arg.statusCode === 404 && idToRequestMap[arg.requestId]?.silentNotFound) { // report nothing } else { @@ -217,7 +219,7 @@ if (utils.isElectron()) { }); function handleSuccessfulResponse(arg: Arg) { - if (arg.headers['Content-Type'] === 'application/json' && typeof arg.body === "string") { + if (arg.headers["Content-Type"] === "application/json" && typeof arg.body === "string") { arg.body = JSON.parse(arg.body); } @@ -236,19 +238,18 @@ if (utils.isElectron()) { async function reportError(method: string, url: string, statusCode: number, response: unknown) { let message = response; - if (typeof response === 'string') { + if (typeof response === "string") { try { response = JSON.parse(response); message = (response as any).message; - } - catch (e) {} + } catch (e) {} } const toastService = (await import("./toast.js")).default; - const messageStr = (typeof message === "string" ? message : JSON.stringify(message)); + const messageStr = typeof message === "string" ? message : JSON.stringify(message); - if ([400, 404].includes(statusCode) && response && typeof response === 'object') { + if ([400, 404].includes(statusCode) && response && typeof response === "object") { toastService.showError(messageStr); throw new ValidationError({ requestUrl: url, diff --git a/src/public/app/services/shortcuts.ts b/src/public/app/services/shortcuts.ts index 05886bac2..65da81a0d 100644 --- a/src/public/app/services/shortcuts.ts +++ b/src/public/app/services/shortcuts.ts @@ -4,7 +4,7 @@ type ElementType = HTMLElement | Document; type Handler = (e: JQuery.TriggeredEvent) => void; function removeGlobalShortcut(namespace: string) { - bindGlobalShortcut('', null, namespace); + bindGlobalShortcut("", null, namespace); } function bindGlobalShortcut(keyboardShortcut: string, handler: Handler | null, namespace: string | null = null) { @@ -15,7 +15,7 @@ function bindElShortcut($el: JQuery, keyboardShortcut: string, hand if (utils.isDesktop()) { keyboardShortcut = normalizeShortcut(keyboardShortcut); - let eventName = 'keydown'; + let eventName = "keydown"; if (namespace) { eventName += `.${namespace}`; @@ -26,7 +26,7 @@ function bindElShortcut($el: JQuery, keyboardShortcut: string, hand // method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted) if (keyboardShortcut) { - $el.bind(eventName, keyboardShortcut, e => { + $el.bind(eventName, keyboardShortcut, (e) => { if (handler) { handler(e); } @@ -46,12 +46,7 @@ function normalizeShortcut(shortcut: string): string { return shortcut; } - return shortcut - .toLowerCase() - .replace("enter", "return") - .replace("delete", "del") - .replace("ctrl+alt", "alt+ctrl") - .replace("meta+alt", "alt+meta"); // alt needs to be first; + return shortcut.toLowerCase().replace("enter", "return").replace("delete", "del").replace("ctrl+alt", "alt+ctrl").replace("meta+alt", "alt+meta"); // alt needs to be first; } export default { @@ -59,4 +54,4 @@ export default { bindElShortcut, removeGlobalShortcut, normalizeShortcut -} +}; diff --git a/src/public/app/services/spaced_update.ts b/src/public/app/services/spaced_update.ts index 991320423..e56d6ffa2 100644 --- a/src/public/app/services/spaced_update.ts +++ b/src/public/app/services/spaced_update.ts @@ -27,8 +27,7 @@ export default class SpacedUpdate { try { await this.updater(); - } - catch (e) { + } catch (e) { this.changed = true; throw e; @@ -53,8 +52,7 @@ export default class SpacedUpdate { this.updater(); this.lastUpdated = Date.now(); this.changed = false; - } - else { + } else { // update isn't triggered but changes are still pending, so we need to schedule another check this.scheduleUpdate(); } @@ -65,8 +63,7 @@ export default class SpacedUpdate { try { await callback(); - } - finally { + } finally { this.changeForbidden = false; } } diff --git a/src/public/app/services/sync.ts b/src/public/app/services/sync.ts index 7c953fb84..c06a2077d 100644 --- a/src/public/app/services/sync.ts +++ b/src/public/app/services/sync.ts @@ -1,5 +1,5 @@ -import { t } from './i18n.js'; -import server from './server.js'; +import { t } from "./i18n.js"; +import server from "./server.js"; import toastService from "./toast.js"; // TODO: De-duplicate with server once we have a commons. @@ -10,17 +10,16 @@ interface SyncResult { } async function syncNow(ignoreNotConfigured = false) { - const result = await server.post('sync/now'); + const result = await server.post("sync/now"); if (result.success) { toastService.showMessage(t("sync.finished-successfully")); - } - else { + } else { if (result.message.length > 200) { result.message = `${result.message.substr(0, 200)}...`; } - if (!ignoreNotConfigured || result.errorCode !== 'NOT_CONFIGURED') { + if (!ignoreNotConfigured || result.errorCode !== "NOT_CONFIGURED") { toastService.showError(t("sync.failed", { message: result.message })); } } diff --git a/src/public/app/services/syntax_highlight.ts b/src/public/app/services/syntax_highlight.ts index 68ca07ced..ae2fbde72 100644 --- a/src/public/app/services/syntax_highlight.ts +++ b/src/public/app/services/syntax_highlight.ts @@ -8,7 +8,7 @@ export function getStylesheetUrl(theme: string) { } const defaultPrefix = "default:"; - if (theme.startsWith(defaultPrefix)) { + if (theme.startsWith(defaultPrefix)) { return `${window.glob.assetPath}/node_modules/@highlightjs/cdn-assets/styles/${theme.substr(defaultPrefix.length)}.min.css`; } @@ -17,13 +17,13 @@ export function getStylesheetUrl(theme: string) { /** * Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks. - * + * * @param $container the container under which to look for code blocks and to apply syntax highlighting to them. */ export async function applySyntaxHighlight($container: JQuery) { if (!isSyntaxHighlightEnabled()) { return; - } + } const codeBlocks = $container.find("pre code"); for (const codeBlock of codeBlocks) { @@ -31,7 +31,7 @@ export async function applySyntaxHighlight($container: JQuery) { if (!normalizedMimeType) { continue; } - + applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); } } @@ -58,8 +58,8 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery` ); - $toast.find('.toast-title').text(options.title); - $toast.find('.toast-body').html(options.message); + $toast.find(".toast-title").text(options.title); + $toast.find(".toast-body").html(options.message); if (options.id) { $toast.attr("id", `toast-${options.id}`); @@ -39,7 +39,7 @@ function toast(options: ToastOptions) { autohide: !!options.autohide }); - $toast.on('hidden.bs.toast', e => e.target.remove()); + $toast.on("hidden.bs.toast", (e) => e.target.remove()); $toast.toast("show"); @@ -50,9 +50,8 @@ function showPersistent(options: ToastOptions) { let $toast = $(`#toast-${options.id}`); if ($toast.length > 0) { - $toast.find('.toast-body').html(options.message); - } - else { + $toast.find(".toast-body").html(options.message); + } else { options.autohide = false; $toast = toast(options); @@ -90,7 +89,7 @@ function showError(message: string, delay = 10000) { toast({ title: "Error", - icon: 'alert', + icon: "alert", message: message, autohide: true, delay @@ -102,7 +101,7 @@ function showErrorTitleAndMessage(title: string, message: string, delay = 10000) toast({ title: title, - icon: 'alert', + icon: "alert", message: message, autohide: true, delay @@ -123,4 +122,4 @@ export default { throwError, showPersistent, closePersistent -} +}; diff --git a/src/public/app/services/tree.ts b/src/public/app/services/tree.ts index 090b1b362..0a56771a4 100644 --- a/src/public/app/services/tree.ts +++ b/src/public/app/services/tree.ts @@ -1,7 +1,7 @@ -import ws from './ws.js'; -import utils from './utils.js'; -import froca from './froca.js'; -import hoistedNoteService from '../services/hoisted_note.js'; +import ws from "./ws.js"; +import utils from "./utils.js"; +import froca from "./froca.js"; +import hoistedNoteService from "../services/hoisted_note.js"; import appContext from "../components/app_context.js"; export interface Node { @@ -9,19 +9,19 @@ export interface Node { getParent(): Node; getChildren(): Node[]; folder: boolean; - renderTitle(): void, + renderTitle(): void; data: { noteId?: string; isProtected?: boolean; branchId: string; noteType: string; - } + }; } /** * @returns {string|null} */ -async function resolveNotePath(notePath: string, hoistedNoteId = 'root') { +async function resolveNotePath(notePath: string, hoistedNoteId = "root") { const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId); return runPath ? runPath.join("/") : null; @@ -32,7 +32,7 @@ async function resolveNotePath(notePath: string, hoistedNoteId = 'root') { * notePath as possible. Part of the path might not be valid because of note moving (which causes * path change) or other corruption, in that case, this will try to get some other valid path to the correct note. */ -async function resolveNotePathToSegments(notePath: string, hoistedNoteId = 'root', logErrors = true) { +async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root", logErrors = true) { utils.assertArguments(notePath); // we might get notePath with the params suffix, remove it if present @@ -45,7 +45,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = 'root const path = notePath.split("/").reverse(); if (!path.includes("root")) { - path.push('root'); + path.push("root"); } const effectivePathSegments = []; @@ -82,13 +82,16 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = 'root return null; } - if (!parents.some(p => p.noteId === parentNoteId)) { + if (!parents.some((p) => p.noteId === parentNoteId)) { if (logErrors) { const parent = froca.getNoteFromCache(parentNoteId); - console.debug(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'}) - for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}. - You can ignore this message as it is mostly harmless.`); + console.debug( + utils.now(), + `Did not find parent ${parentNoteId} (${parent ? parent.title : "n/a"}) + for child ${childNoteId} (${child.title}), available parents: ${parents.map((p) => `${p.noteId} (${p.title})`)}. + You can ignore this message as it is mostly harmless.` + ); } const bestNotePath = child.getBestNotePath(hoistedNoteId); @@ -113,8 +116,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = 'root if (effectivePathSegments.includes(hoistedNoteId)) { return effectivePathSegments; - } - else { + } else { const noteId = getNoteIdFromUrl(notePath); if (!noteId) { throw new Error(`Unable to find note with ID: ${noteId}.`); @@ -134,16 +136,16 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = 'root } } -ws.subscribeToMessages(message => { - if (message.type === 'openNote') { - appContext.tabManager.activateOrOpenNote(message.noteId); +ws.subscribeToMessages((message) => { + if (message.type === "openNote") { + appContext.tabManager.activateOrOpenNote(message.noteId); - if (utils.isElectron()) { - const currentWindow = utils.dynamicRequire('@electron/remote').getCurrentWindow(); + if (utils.isElectron()) { + const currentWindow = utils.dynamicRequire("@electron/remote").getCurrentWindow(); - currentWindow.show(); - } - } + currentWindow.show(); + } + } }); function getParentProtectedStatus(node: Node) { @@ -162,7 +164,7 @@ function getNoteIdFromUrl(urlOrNotePath: string | undefined) { } async function getBranchIdFromUrl(urlOrNotePath: string) { - const {noteId, parentNoteId} = getNoteIdAndParentIdFromUrl(urlOrNotePath); + const { noteId, parentNoteId } = getNoteIdAndParentIdFromUrl(urlOrNotePath); if (!parentNoteId) { return null; } @@ -177,15 +179,15 @@ function getNoteIdAndParentIdFromUrl(urlOrNotePath: string) { const [notePath] = urlOrNotePath.split("?"); - if (notePath === 'root') { + if (notePath === "root") { return { - noteId: 'root', - parentNoteId: 'none' + noteId: "root", + parentNoteId: "none" }; } - let parentNoteId = 'root'; - let noteId = ''; + let parentNoteId = "root"; + let noteId = ""; if (notePath) { const segments = notePath.split("/"); @@ -230,7 +232,7 @@ async function getNoteTitle(noteId: string, parentNoteId: string | null = null) return "[not found]"; } - let {title} = note; + let { title } = note; if (parentNoteId !== null) { const branchId = note.parentToBranch[parentNoteId]; @@ -250,17 +252,17 @@ async function getNoteTitle(noteId: string, parentNoteId: string | null = null) async function getNotePathTitleComponents(notePath: string) { const titleComponents = []; - if (notePath.startsWith('root/')) { + if (notePath.startsWith("root/")) { notePath = notePath.substr(5); } // special case when we want just root's title - if (notePath === 'root') { + if (notePath === "root") { titleComponents.push(await getNoteTitle(notePath)); } else { - let parentNoteId = 'root'; + let parentNoteId = "root"; - for (const noteId of notePath.split('/')) { + for (const noteId of notePath.split("/")) { titleComponents.push(await getNoteTitle(noteId, parentNoteId)); parentNoteId = noteId; @@ -275,7 +277,7 @@ async function getNotePathTitle(notePath: string) { const titlePath = await getNotePathTitleComponents(notePath); - return titlePath.join(' / '); + return titlePath.join(" / "); } async function getNoteTitleWithPathAsSuffix(notePath: string) { @@ -290,9 +292,7 @@ async function getNoteTitleWithPathAsSuffix(notePath: string) { const title = titleComponents[titleComponents.length - 1]; const path = titleComponents.slice(0, titleComponents.length - 1); - const $titleWithPath = $('') - .append($('').text(title)); - + const $titleWithPath = $('').append($('').text(title)); $titleWithPath.append(formatNotePath(path)); @@ -303,7 +303,6 @@ function formatNotePath(path: string[]) { const $notePath = $(''); if (path.length > 0) { - $notePath.append($(` ()`)); for (let segmentIndex = 0; segmentIndex < path.length; segmentIndex++) { @@ -324,8 +323,6 @@ function isNotePathInHiddenSubtree(notePath: string) { return notePath?.includes("root/_hidden"); } - - export default { resolveNotePath, resolveNotePathToSegments, diff --git a/src/public/app/services/utils.ts b/src/public/app/services/utils.ts index fa93e47dd..3d1d72969 100644 --- a/src/public/app/services/utils.ts +++ b/src/public/app/services/utils.ts @@ -11,8 +11,7 @@ function reloadFrontendApp(reason?: string) { function parseDate(str: string) { try { return new Date(Date.parse(str)); - } - catch (e: any) { + } catch (e: any) { throw new Error(`Can't parse date from '${str}': ${e.message} ${e.stack}`); } } @@ -34,26 +33,26 @@ function formatTimeInterval(ms: number) { const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); - const plural = (count: number, name: string) => `${count} ${name}${count > 1 ? 's' : ''}`; + const plural = (count: number, name: string) => `${count} ${name}${count > 1 ? "s" : ""}`; const segments = []; if (days > 0) { - segments.push(plural(days, 'day')); + segments.push(plural(days, "day")); } if (days < 2) { if (hours % 24 > 0) { - segments.push(plural(hours % 24, 'hour')); + segments.push(plural(hours % 24, "hour")); } if (hours < 4) { if (minutes % 60 > 0) { - segments.push(plural(minutes % 60, 'minute')); + segments.push(plural(minutes % 60, "minute")); } if (minutes < 5) { if (seconds % 60 > 0) { - segments.push(plural(seconds % 60, 'second')); + segments.push(plural(seconds % 60, "second")); } } } @@ -80,7 +79,7 @@ function formatDateTime(date: Date) { } function localNowDateTime() { - return dayjs().format('YYYY-MM-DD HH:mm:ss.SSSZZ'); + return dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZZ"); } function now() { @@ -95,12 +94,11 @@ function isElectron() { } function isMac() { - return navigator.platform.indexOf('Mac') > -1; + return navigator.platform.indexOf("Mac") > -1; } function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent) { - return (!isMac() && evt.ctrlKey) - || (isMac() && evt.metaKey); + return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey); } function assertArguments(...args: string[]) { @@ -112,18 +110,18 @@ function assertArguments(...args: string[]) { } const entityMap: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=' + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "/": "/", + "`": "`", + "=": "=" }; function escapeHtml(str: string) { - return str.replace(/[&<>"'`=\/]/g, s => entityMap[s]); + return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]); } function formatSize(size: number) { @@ -131,8 +129,7 @@ function formatSize(size: number) { if (size < 1024) { return `${size} KiB`; - } - else { + } else { return `${Math.round(size / 102.4) / 10} MiB`; } } @@ -161,15 +158,19 @@ function randomString(len: number) { } function isMobile() { - return window.glob?.device === "mobile" + return ( + window.glob?.device === "mobile" || // window.glob.device is not available in setup - || (!window.glob?.device && /Mobi/.test(navigator.userAgent)); + (!window.glob?.device && /Mobi/.test(navigator.userAgent)) + ); } function isDesktop() { - return window.glob?.device === "desktop" + return ( + window.glob?.device === "desktop" || // window.glob.device is not available in setup - || (!window.glob?.device && !/Mobi/.test(navigator.userAgent)); + (!window.glob?.device && !/Mobi/.test(navigator.userAgent)) + ); } /** @@ -192,7 +193,7 @@ function getMimeTypeClass(mime: string) { return ""; } - const semicolonIdx = mime.indexOf(';'); + const semicolonIdx = mime.indexOf(";"); if (semicolonIdx !== -1) { // stripping everything following the semicolon @@ -227,9 +228,7 @@ function focusSavedElement() { // must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607 // the bug manifests itself in resetting the cursor position to the first character - jumping above - const editor = $lastFocusedElement - .closest('.ck-editor__editable') - .prop('ckeditorInstance'); + const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance"); if (editor) { editor.editing.view.focus(); @@ -254,7 +253,7 @@ async function openDialog($dialog: JQuery, closeActDialog = true) { //@ts-ignore bootstrap.Modal.getOrCreateInstance($dialog[0]).show(); - $dialog.on('hidden.bs.modal', () => { + $dialog.on("hidden.bs.modal", () => { const $autocompleteEl = $(".aa-input"); if ("autocomplete" in $autocompleteEl) { $autocompleteEl.autocomplete("close"); @@ -276,22 +275,24 @@ async function openDialog($dialog: JQuery, closeActDialog = true) { function isHtmlEmpty(html: string) { if (!html) { return true; - } else if (typeof html !== 'string') { + } else if (typeof html !== "string") { logError(`Got object of type '${typeof html}' where string was expected.`); return false; } html = html.toLowerCase(); - return !html.includes('").html(html).text().trim().length === 0; + $("
    ").html(html).text().trim().length === 0 + ); } async function clearBrowserCache() { if (isElectron()) { - const win = dynamicRequire('@electron/remote').getCurrentWindow(); + const win = dynamicRequire("@electron/remote").getCurrentWindow(); await win.webContents.session.clearCache(); } } @@ -304,16 +305,16 @@ function copySelectionToClipboard() { } function dynamicRequire(moduleName: string) { - if (typeof __non_webpack_require__ !== 'undefined') { + if (typeof __non_webpack_require__ !== "undefined") { return __non_webpack_require__(moduleName); - } - else { + } else { return require(moduleName); } } function timeLimit(promise: Promise, limitMs: number, errorMessage?: string) { - if (!promise || !promise.then) { // it's not actually a promise + if (!promise || !promise.then) { + // it's not actually a promise return promise; } @@ -323,7 +324,7 @@ function timeLimit(promise: Promise, limitMs: number, errorMessage?: strin return new Promise((res, rej) => { let resolved = false; - promise.then(result => { + promise.then((result) => { resolved = true; res(result); @@ -339,8 +340,8 @@ function timeLimit(promise: Promise, limitMs: number, errorMessage?: strin function initHelpDropdown($el: JQuery) { // stop inside clicks from closing the menu - const $dropdownMenu = $el.find('.help-dropdown .dropdown-menu'); - $dropdownMenu.on('click', e => e.stopPropagation()); + const $dropdownMenu = $el.find(".help-dropdown .dropdown-menu"); + $dropdownMenu.on("click", (e) => e.stopPropagation()); // previous propagation stop will also block help buttons from being opened, so we need to re-init for this element initHelpButtons($dropdownMenu); @@ -354,21 +355,21 @@ function openHelp($button: JQuery) { if (helpPage) { const url = wikiBaseUrl + helpPage; - window.open(url, '_blank'); + window.open(url, "_blank"); } } function initHelpButtons($el: JQuery | JQuery) { // for some reason, the .on(event, listener, handler) does not work here (e.g. Options -> Sync -> Help button) // so we do it manually - $el.on("click", e => { + $el.on("click", (e) => { const $helpButton = $(e.target).closest("[data-help-page]"); openHelp($helpButton); }); } function filterAttributeName(name: string) { - return name.replace(/[^\p{L}\p{N}_:]/ug, ""); + return name.replace(/[^\p{L}\p{N}_:]/gu, ""); } const ATTR_NAME_MATCHER = new RegExp("^[\\p{L}\\p{N}_:]+$", "u"); @@ -393,12 +394,12 @@ function areObjectsEqual(...args: unknown[]) { let leftChain: Object[]; let rightChain: Object[]; - function compare2Objects (x: unknown, y: unknown) { + function compare2Objects(x: unknown, y: unknown) { let p; // remember that NaN === NaN returns false // and isNaN(undefined) returns true - if (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y)) { + if (typeof x === "number" && typeof y === "number" && isNaN(x) && isNaN(y)) { return true; } @@ -412,11 +413,13 @@ function areObjectsEqual(...args: unknown[]) { // Works in case when functions are created in constructor. // Comparing dates is a common scenario. Another built-ins? // We can even handle functions passed across iframes - if ((typeof x === 'function' && typeof y === 'function') || + if ( + (typeof x === "function" && typeof y === "function") || (x instanceof Date && y instanceof Date) || (x instanceof RegExp && y instanceof RegExp) || (x instanceof String && y instanceof String) || - (x instanceof Number && y instanceof Number)) { + (x instanceof Number && y instanceof Number) + ) { return x.toString() === y.toString(); } @@ -447,8 +450,7 @@ function areObjectsEqual(...args: unknown[]) { for (p in y) { if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { return false; - } - else if (typeof (y as any)[p] !== typeof (x as any)[p]) { + } else if (typeof (y as any)[p] !== typeof (x as any)[p]) { return false; } } @@ -456,15 +458,13 @@ function areObjectsEqual(...args: unknown[]) { for (p in x) { if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { return false; - } - else if (typeof (y as any)[p] !== typeof (x as any)[p]) { + } else if (typeof (y as any)[p] !== typeof (x as any)[p]) { return false; } - switch (typeof ((x as any)[p])) { - case 'object': - case 'function': - + switch (typeof (x as any)[p]) { + case "object": + case "function": leftChain.push(x); rightChain.push(y); @@ -493,7 +493,6 @@ function areObjectsEqual(...args: unknown[]) { } for (i = 1, l = arguments.length; i < l; i++) { - leftChain = []; //Todo: this can be cached rightChain = []; @@ -531,11 +530,11 @@ function createImageSrcUrl(note: { noteId: string; title: string }) { */ function downloadSvg(nameWithoutExtension: string, svgContent: string) { const filename = `${nameWithoutExtension}.svg`; - const element = document.createElement('a'); - element.setAttribute('href', `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`); - element.setAttribute('download', filename); + const element = document.createElement("a"); + element.setAttribute("href", `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`); + element.setAttribute("download", filename); - element.style.display = 'none'; + element.style.display = "none"; document.body.appendChild(element); element.click(); @@ -555,13 +554,12 @@ function downloadSvg(nameWithoutExtension: string, svgContent: string) { * @returns */ function compareVersions(v1: string, v2: string): number { - // Remove 'v' prefix and everything after dash if present - v1 = v1.replace(/^v/, '').split('-')[0]; - v2 = v2.replace(/^v/, '').split('-')[0]; + v1 = v1.replace(/^v/, "").split("-")[0]; + v2 = v2.replace(/^v/, "").split("-")[0]; - const v1parts = v1.split('.').map(Number); - const v2parts = v2.split('.').map(Number); + const v1parts = v1.split(".").map(Number); + const v2parts = v2.split(".").map(Number); // Pad shorter version with zeros while (v1parts.length < 3) v1parts.push(0); @@ -593,10 +591,7 @@ function isUpdateAvailable(latestVersion: string, currentVersion: string): boole } function isLaunchBarConfig(noteId: string) { - return [ - "_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", - "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers" - ].includes(noteId); + return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId); } export default { diff --git a/src/public/app/services/validation_error.ts b/src/public/app/services/validation_error.ts index a37841a89..d04edc76f 100644 --- a/src/public/app/services/validation_error.ts +++ b/src/public/app/services/validation_error.ts @@ -4,4 +4,4 @@ export default class ValidationError { (this as any)[key] = resp[key]; } } -} \ No newline at end of file +} diff --git a/src/public/app/services/ws.ts b/src/public/app/services/ws.ts index 061673470..d6c392b5c 100644 --- a/src/public/app/services/ws.ts +++ b/src/public/app/services/ws.ts @@ -1,11 +1,11 @@ -import utils from './utils.js'; +import utils from "./utils.js"; import toastService from "./toast.js"; import server from "./server.js"; import options from "./options.js"; import frocaUpdater from "./froca_updater.js"; import appContext from "../components/app_context.js"; -import { t } from './i18n.js'; -import { EntityChange } from '../server_types.js'; +import { t } from "./i18n.js"; +import { EntityChange } from "../server_types.js"; type MessageHandler = (message: any) => void; const messageHandlers: MessageHandler[] = []; @@ -21,11 +21,13 @@ function logError(message: string) { console.error(utils.now(), message); // needs to be separate from .trace() if (ws && ws.readyState === 1) { - ws.send(JSON.stringify({ - type: 'log-error', - error: message, - stack: new Error().stack - })); + ws.send( + JSON.stringify({ + type: "log-error", + error: message, + stack: new Error().stack + }) + ); } } @@ -33,10 +35,12 @@ function logInfo(message: string) { console.log(utils.now(), message); if (ws && ws.readyState === 1) { - ws.send(JSON.stringify({ - type: 'log-info', - info: message - })); + ws.send( + JSON.stringify({ + type: "log-info", + info: message + }) + ); } } @@ -54,9 +58,7 @@ let consumeQueuePromise: Promise | null = null; const processedEntityChangeIds = new Set(); function logRows(entityChanges: EntityChange[]) { - const filteredRows = entityChanges.filter(row => - !processedEntityChangeIds.has(row.id) - && (row.entityName !== 'options' || row.entityId !== 'openNoteContexts')); + const filteredRows = entityChanges.filter((row) => !processedEntityChangeIds.has(row.id) && (row.entityName !== "options" || row.entityId !== "openNoteContexts")); if (filteredRows.length > 0) { console.debug(utils.now(), "Frontend update data: ", filteredRows); @@ -111,28 +113,21 @@ async function handleMessage(event: MessageEvent) { messageHandler(message); } - if (message.type === 'ping') { + if (message.type === "ping") { lastPingTs = Date.now(); - } - else if (message.type === 'reload-frontend') { + } else if (message.type === "reload-frontend") { utils.reloadFrontendApp("received request from backend to reload frontend"); - } - else if (message.type === 'frontend-update') { + } else if (message.type === "frontend-update") { await executeFrontendUpdate(message.data.entityChanges); - } - else if (message.type === 'sync-hash-check-failed') { + } else if (message.type === "sync-hash-check-failed") { toastService.showError(t("ws.sync-check-failed"), 60000); - } - else if (message.type === 'consistency-checks-failed') { + } else if (message.type === "consistency-checks-failed") { toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000); - } - else if (message.type === 'api-log-messages') { - appContext.triggerEvent("apiLogMessages", {noteId: message.noteId, messages: message.messages}); - } - else if (message.type === 'toast') { + } else if (message.type === "api-log-messages") { + appContext.triggerEvent("apiLogMessages", { noteId: message.noteId, messages: message.messages }); + } else if (message.type === "toast") { toastService.showMessage(message.message); - } - else if (message.type === 'execute-script') { + } else if (message.type === "execute-script") { // TODO: Remove after porting the file // @ts-ignore const bundleService = (await import("../services/bundle.js")).default as any; @@ -163,7 +158,7 @@ function waitForEntityChangeId(desiredEntityChangeId: number) { desiredEntityChangeId: desiredEntityChangeId, resolvePromise: res, start: Date.now() - }) + }); }); } @@ -172,15 +167,17 @@ function waitForMaxKnownEntityChangeId() { } function checkEntityChangeIdListeners() { + entityChangeIdReachedListeners.filter((l) => l.desiredEntityChangeId <= lastProcessedEntityChangeId).forEach((l) => l.resolvePromise()); + + entityChangeIdReachedListeners = entityChangeIdReachedListeners.filter((l) => l.desiredEntityChangeId > lastProcessedEntityChangeId); + entityChangeIdReachedListeners - .filter(l => l.desiredEntityChangeId <= lastProcessedEntityChangeId) - .forEach(l => l.resolvePromise()); - - entityChangeIdReachedListeners = entityChangeIdReachedListeners - .filter(l => l.desiredEntityChangeId > lastProcessedEntityChangeId); - - entityChangeIdReachedListeners.filter(l => Date.now() > l.start - 60000) - .forEach(l => console.log(`Waiting for entityChangeId ${l.desiredEntityChangeId} while last processed is ${lastProcessedEntityChangeId} (last accepted ${lastAcceptedEntityChangeId}) for ${Math.floor((Date.now() - l.start) / 1000)}s`)); + .filter((l) => Date.now() > l.start - 60000) + .forEach((l) => + console.log( + `Waiting for entityChangeId ${l.desiredEntityChangeId} while last processed is ${lastProcessedEntityChangeId} (last accepted ${lastAcceptedEntityChangeId}) for ${Math.floor((Date.now() - l.start) / 1000)}s` + ) + ); } async function consumeFrontendUpdateData() { @@ -188,20 +185,18 @@ async function consumeFrontendUpdateData() { const allEntityChanges = frontendUpdateDataQueue; frontendUpdateDataQueue = []; - const nonProcessedEntityChanges = allEntityChanges.filter(ec => !processedEntityChangeIds.has(ec.id)); + const nonProcessedEntityChanges = allEntityChanges.filter((ec) => !processedEntityChangeIds.has(ec.id)); try { await utils.timeLimit(frocaUpdater.processEntityChanges(nonProcessedEntityChanges), 30000); - } - catch (e: any) { + } catch (e: any) { logError(`Encountered error ${e.message}: ${e.stack}, reloading frontend.`); - if (!glob.isDev && !options.is('debugModeEnabled')) { + if (!glob.isDev && !options.is("debugModeEnabled")) { // if there's an error in updating the frontend, then the easy option to recover is to reload the frontend completely utils.reloadFrontendApp(); - } - else { + } else { console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges); toastService.showError(t("ws.encountered-error", { message: e.message })); @@ -235,16 +230,20 @@ function connectWebSocket() { async function sendPing() { if (Date.now() - lastPingTs > 30000) { - console.log(utils.now(), "Lost websocket connection to the backend. If you keep having this issue repeatedly, you might want to check your reverse proxy (nginx, apache) configuration and allow/unblock WebSocket."); + console.log( + utils.now(), + "Lost websocket connection to the backend. If you keep having this issue repeatedly, you might want to check your reverse proxy (nginx, apache) configuration and allow/unblock WebSocket." + ); } if (ws.readyState === ws.OPEN) { - ws.send(JSON.stringify({ - type: 'ping', - lastEntityChangeId: lastAcceptedEntityChangeId - })); - } - else if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) { + ws.send( + JSON.stringify({ + type: "ping", + lastEntityChangeId: lastAcceptedEntityChangeId + }) + ); + } else if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) { console.log(utils.now(), "WS closed or closing, trying to reconnect"); ws = connectWebSocket(); @@ -265,4 +264,3 @@ export default { waitForMaxKnownEntityChangeId, getMaxKnownEntityChangeSyncId: () => lastAcceptedEntityChangeSyncId }; - diff --git a/src/public/app/setup.js b/src/public/app/setup.js index c40d0fb8e..0c12534a8 100644 --- a/src/public/app/setup.js +++ b/src/public/app/setup.js @@ -1,5 +1,5 @@ import utils from "./services/utils.js"; -import macInit from './services/mac_init.js'; +import macInit from "./services/mac_init.js"; macInit.init(); @@ -26,14 +26,13 @@ function SetupModel() { this.setupTypeSelected = () => !!this.setupType(); this.selectSetupType = () => { - if (this.setupType() === 'new-document') { - this.step('new-document-in-progress'); + if (this.setupType() === "new-document") { + this.step("new-document-in-progress"); - $.post('api/setup/new-document').then(() => { + $.post("api/setup/new-document").then(() => { window.location.replace("./setup"); }); - } - else { + } else { this.step(this.setupType()); } }; @@ -60,39 +59,36 @@ function SetupModel() { } // not using server.js because it loads too many dependencies - const resp = await $.post('api/setup/sync-from-server', { + const resp = await $.post("api/setup/sync-from-server", { syncServerHost: syncServerHost, syncProxy: syncProxy, password: password }); - if (resp.result === 'success') { - this.step('sync-in-progress'); + if (resp.result === "success") { + this.step("sync-in-progress"); setInterval(checkOutstandingSyncs, 1000); hideAlert(); - } - else { + } else { showAlert(`Sync setup failed: ${resp.error}`); } }; } async function checkOutstandingSyncs() { - const { outstandingPullCount, initialized } = await $.get('api/sync/stats'); + const { outstandingPullCount, initialized } = await $.get("api/sync/stats"); if (initialized) { if (utils.isElectron()) { - const remote = utils.dynamicRequire('@electron/remote'); + const remote = utils.dynamicRequire("@electron/remote"); remote.app.relaunch(); remote.app.exit(0); - } - else { + } else { utils.reloadFrontendApp(); } - } - else { + } else { $("#outstanding-syncs").html(outstandingPullCount); } } @@ -106,6 +102,6 @@ function hideAlert() { $("#alert").hide(); } -ko.applyBindings(new SetupModel(), document.getElementById('setup-dialog')); +ko.applyBindings(new SetupModel(), document.getElementById("setup-dialog")); $("#setup-dialog").show(); diff --git a/src/public/app/share.js b/src/public/app/share.js index 03ad92515..3c3282497 100644 --- a/src/public/app/share.js +++ b/src/public/app/share.js @@ -13,11 +13,15 @@ async function fetchNote(noteId = null) { return await resp.json(); } -document.addEventListener('DOMContentLoaded', () => { - const toggleMenuButton = document.getElementById('toggleMenuButton'); - const layout = document.getElementById('layout'); +document.addEventListener( + "DOMContentLoaded", + () => { + const toggleMenuButton = document.getElementById("toggleMenuButton"); + const layout = document.getElementById("layout"); - if (toggleMenuButton && layout) { - toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu')); - } -}, false); + if (toggleMenuButton && layout) { + toggleMenuButton.addEventListener("click", () => layout.classList.toggle("showMenu")); + } + }, + false +); diff --git a/src/public/app/utils/formatters.ts b/src/public/app/utils/formatters.ts index 398aa678b..755cc8153 100644 --- a/src/public/app/utils/formatters.ts +++ b/src/public/app/utils/formatters.ts @@ -16,20 +16,19 @@ export function formatDateTime(date: string | Date | number, dateStyle: DateTime } else { // Invalid type throw new TypeError(`Invalid type for the "date" argument.`); - }; + } if (timeStyle !== "none" && dateStyle !== "none") { // Format the date and time - const formatter = new Intl.DateTimeFormat(navigator.language, {dateStyle, timeStyle}); + const formatter = new Intl.DateTimeFormat(navigator.language, { dateStyle, timeStyle }); return formatter.format(parsedDate); } else if (timeStyle === "none" && dateStyle !== "none") { // Format only the date - return parsedDate.toLocaleDateString(locale, {dateStyle}); + return parsedDate.toLocaleDateString(locale, { dateStyle }); } else if (dateStyle === "none" && timeStyle !== "none") { // Format only the time - return parsedDate.toLocaleTimeString(locale, {timeStyle}); + return parsedDate.toLocaleTimeString(locale, { timeStyle }); } throw new Error("Incorrect state."); } - diff --git a/src/public/app/utils/mutex.ts b/src/public/app/utils/mutex.ts index c37e78003..76ea57b0d 100644 --- a/src/public/app/utils/mutex.ts +++ b/src/public/app/utils/mutex.ts @@ -7,22 +7,21 @@ export default class Mutex { lock() { let resolveFun: () => void; - const subPromise = new Promise(resolve => resolveFun = () => resolve()); + const subPromise = new Promise((resolve) => (resolveFun = () => resolve())); // Caller gets a promise that resolves when the current outstanding lock resolves const newPromise = this.current.then(() => resolveFun); // Don't allow the next request until the new promise is done this.current = subPromise; // Return the new promise return newPromise; - }; + } async runExclusively(cb: () => Promise) { const unlock = await this.lock(); try { return await cb(); - } - finally { + } finally { unlock(); } } diff --git a/src/public/app/widgets/api_log.js b/src/public/app/widgets/api_log.js index b1a85a486..a3c7e41d3 100644 --- a/src/public/app/widgets/api_log.js +++ b/src/public/app/widgets/api_log.js @@ -33,23 +33,21 @@ const TPL = ` } -
    +
    `; export default class ApiLogWidget extends NoteContextAwareWidget { isEnabled() { - return this.note - && this.note.mime.startsWith('application/javascript;env=') - && super.isEnabled(); + return this.note && this.note.mime.startsWith("application/javascript;env=") && super.isEnabled(); } doRender() { this.$widget = $(TPL); this.toggle(false); - this.$logContainer = this.$widget.find('.api-log-container'); + this.$logContainer = this.$widget.find(".api-log-container"); this.$closeButton = this.$widget.find(".close-api-log-button"); this.$closeButton.on("click", () => this.toggle(false)); } @@ -58,7 +56,7 @@ export default class ApiLogWidget extends NoteContextAwareWidget { this.$logContainer.empty(); } - apiLogMessagesEvent({messages, noteId}) { + apiLogMessagesEvent({ messages, noteId }) { if (!this.isNote(noteId)) { return; } diff --git a/src/public/app/widgets/attachment_detail.js b/src/public/app/widgets/attachment_detail.js index c0d27dd9e..263722018 100644 --- a/src/public/app/widgets/attachment_detail.js +++ b/src/public/app/widgets/attachment_detail.js @@ -114,86 +114,78 @@ export default class AttachmentDetailWidget extends BasicWidget { } async refresh() { - this.$widget.find('.attachment-detail-wrapper') - .empty() - .append( - $(TPL) - .find('.attachment-detail-wrapper') - .html() - ); - this.$wrapper = this.$widget.find('.attachment-detail-wrapper'); + this.$widget.find(".attachment-detail-wrapper").empty().append($(TPL).find(".attachment-detail-wrapper").html()); + this.$wrapper = this.$widget.find(".attachment-detail-wrapper"); this.$wrapper.addClass(this.isFullDetail ? "full-detail" : "list-view"); if (!this.isFullDetail) { - this.$wrapper.find('.attachment-title').append( + this.$wrapper.find(".attachment-title").append( await linkService.createLink(this.attachment.ownerId, { title: this.attachment.title, viewScope: { - viewMode: 'attachments', + viewMode: "attachments", attachmentId: this.attachment.attachmentId } }) ); } else { - this.$wrapper.find('.attachment-title') - .text(this.attachment.title); + this.$wrapper.find(".attachment-title").text(this.attachment.title); } - const $deletionWarning = this.$wrapper.find('.attachment-deletion-warning'); - const {utcDateScheduledForErasureSince} = this.attachment; + const $deletionWarning = this.$wrapper.find(".attachment-deletion-warning"); + const { utcDateScheduledForErasureSince } = this.attachment; if (utcDateScheduledForErasureSince) { this.$wrapper.addClass("scheduled-for-deletion"); const scheduledSinceTimestamp = utils.parseDate(utcDateScheduledForErasureSince)?.getTime(); - const intervalMs = options.getInt('eraseUnusedAttachmentsAfterSeconds') * 1000; + const intervalMs = options.getInt("eraseUnusedAttachmentsAfterSeconds") * 1000; const deletionTimestamp = scheduledSinceTimestamp + intervalMs; const willBeDeletedInMs = deletionTimestamp - Date.now(); $deletionWarning.show(); if (willBeDeletedInMs >= 60000) { - $deletionWarning.text(t('attachment_detail_2.will_be_deleted_in', { time: utils.formatTimeInterval(willBeDeletedInMs) })); + $deletionWarning.text(t("attachment_detail_2.will_be_deleted_in", { time: utils.formatTimeInterval(willBeDeletedInMs) })); } else { - $deletionWarning.text(t('attachment_detail_2.will_be_deleted_soon')); + $deletionWarning.text(t("attachment_detail_2.will_be_deleted_soon")); } - $deletionWarning.append(t('attachment_detail_2.deletion_reason')); + $deletionWarning.append(t("attachment_detail_2.deletion_reason")); } else { this.$wrapper.removeClass("scheduled-for-deletion"); $deletionWarning.hide(); } - this.$wrapper.find('.attachment-details') - .text(t('attachment_detail_2.role_and_size', { role: this.attachment.role, size: utils.formatSize(this.attachment.contentLength) })); - this.$wrapper.find('.attachment-actions-container').append(this.attachmentActionsWidget.render()); + this.$wrapper.find(".attachment-details").text(t("attachment_detail_2.role_and_size", { role: this.attachment.role, size: utils.formatSize(this.attachment.contentLength) })); + this.$wrapper.find(".attachment-actions-container").append(this.attachmentActionsWidget.render()); - const {$renderedContent} = await contentRenderer.getRenderedContent(this.attachment, { imageHasZoom: this.isFullDetail }); - this.$wrapper.find('.attachment-content-wrapper').append($renderedContent); + const { $renderedContent } = await contentRenderer.getRenderedContent(this.attachment, { imageHasZoom: this.isFullDetail }); + this.$wrapper.find(".attachment-content-wrapper").append($renderedContent); } async copyAttachmentLinkToClipboard() { - if (this.attachment.role === 'image') { - imageService.copyImageReferenceToClipboard(this.$wrapper.find('.attachment-content-wrapper')); - } else if (this.attachment.role === 'file') { + if (this.attachment.role === "image") { + imageService.copyImageReferenceToClipboard(this.$wrapper.find(".attachment-content-wrapper")); + } else if (this.attachment.role === "file") { const $link = await linkService.createLink(this.attachment.ownerId, { referenceLink: true, viewScope: { - viewMode: 'attachments', + viewMode: "attachments", attachmentId: this.attachment.attachmentId } }); utils.copyHtmlToClipboard($link[0].outerHTML); - toastService.showMessage(t('attachment_detail_2.link_copied')); + toastService.showMessage(t("attachment_detail_2.link_copied")); } else { - throw new Error(t('attachment_detail_2.unrecognized_role', { role: this.attachment.role })); + throw new Error(t("attachment_detail_2.unrecognized_role", { role: this.attachment.role })); } } - async entitiesReloadedEvent({loadResults}) { - const attachmentRow = loadResults.getAttachmentRows().find(att => att.attachmentId === this.attachment.attachmentId); + async entitiesReloadedEvent({ loadResults }) { + const attachmentRow = loadResults.getAttachmentRows().find((att) => att.attachmentId === this.attachment.attachmentId); if (attachmentRow) { if (attachmentRow.isDeleted) { diff --git a/src/public/app/widgets/attribute_widgets/attribute_detail.ts b/src/public/app/widgets/attribute_widgets/attribute_detail.ts index f05f41124..17d4d1d2f 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_detail.ts +++ b/src/public/app/widgets/attribute_widgets/attribute_detail.ts @@ -4,7 +4,7 @@ import froca from "../../services/froca.js"; import linkService from "../../services/link.js"; import attributeAutocompleteService from "../../services/attribute_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js"; -import promotedAttributeDefinitionParser from '../../services/promoted_attribute_definition_parser.js'; +import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js"; import NoteContextAwareWidget from "../note_context_aware_widget.js"; import SpacedUpdate from "../../services/spaced_update.js"; import utils from "../../services/utils.js"; @@ -72,25 +72,25 @@ const TPL = `
    -
    ${t('attribute_detail.attr_detail_title')}
    +
    ${t("attribute_detail.attr_detail_title")}
    - +
    -
    ${t('attribute_detail.attr_is_owned_by')}
    +
    ${t("attribute_detail.attr_is_owned_by")}
    - - + + - + - + - + title="${t("attribute_detail.promoted_title")}"> + - + - + - + - + - + - - + +
    ${t('attribute_detail.name')}
    ${t("attribute_detail.name")}
    ${t('attribute_detail.value')}${t("attribute_detail.value")}
    ${t('attribute_detail.target_note')}${t("attribute_detail.target_note")}
    @@ -98,12 +98,12 @@ const TPL = `
    ${t('attribute_detail.promoted')}${t("attribute_detail.promoted")}
    ${t('attribute_detail.promoted_alias')}${t("attribute_detail.promoted_alias")}
    @@ -111,47 +111,47 @@ const TPL = `
    ${t('attribute_detail.multiplicity')}${t("attribute_detail.multiplicity")}
    ${t('attribute_detail.label_type')}${t("attribute_detail.label_type")}
    ${t('attribute_detail.precision')}${t("attribute_detail.precision")}
    - ${t('attribute_detail.digits')} + ${t("attribute_detail.digits")}
    ${t('attribute_detail.inverse_relation')}${t("attribute_detail.inverse_relation")}
    ${t('attribute_detail.inheritable')}
    ${t("attribute_detail.inheritable")}
    @@ -159,113 +159,113 @@ const TPL = `
    + ${t("attribute_detail.save_and_close")} + ${t("attribute_detail.delete")}
    `; const DISPLAYED_NOTES = 10; const ATTR_TITLES: Record = { - "label": t('attribute_detail.label'), - "label-definition": t('attribute_detail.label_definition'), - "relation": t('attribute_detail.relation'), - "relation-definition": t('attribute_detail.relation_definition') + label: t("attribute_detail.label"), + "label-definition": t("attribute_detail.label_definition"), + relation: t("attribute_detail.relation"), + "relation-definition": t("attribute_detail.relation_definition") }; const ATTR_HELP: Record> = { - "label": { - "disableVersioning": t('attribute_detail.disable_versioning'), - "calendarRoot": t('attribute_detail.calendar_root'), - "archived": t('attribute_detail.archived'), - "excludeFromExport": t('attribute_detail.exclude_from_export'), - "run": t('attribute_detail.run'), - "runOnInstance": t('attribute_detail.run_on_instance'), - "runAtHour": t('attribute_detail.run_at_hour'), - "disableInclusion": t('attribute_detail.disable_inclusion'), - "sorted": t('attribute_detail.sorted'), - "sortDirection": t('attribute_detail.sort_direction'), - "sortFoldersFirst": t('attribute_detail.sort_folders_first'), - "top": t('attribute_detail.top'), - "hidePromotedAttributes": t('attribute_detail.hide_promoted_attributes'), - "readOnly": t('attribute_detail.read_only'), - "autoReadOnlyDisabled": t('attribute_detail.auto_read_only_disabled'), - "appCss": t('attribute_detail.app_css'), - "appTheme": t('attribute_detail.app_theme'), - "appThemeBase": t('attribute_detail.app_theme_base'), - "cssClass": t('attribute_detail.css_class'), - "iconClass": t('attribute_detail.icon_class'), - "pageSize": t('attribute_detail.page_size'), - "customRequestHandler": t('attribute_detail.custom_request_handler'), - "customResourceProvider": t('attribute_detail.custom_resource_provider'), - "widget": t('attribute_detail.widget'), - "workspace": t('attribute_detail.workspace'), - "workspaceIconClass": t('attribute_detail.workspace_icon_class'), - "workspaceTabBackgroundColor": t('attribute_detail.workspace_tab_background_color'), - "workspaceCalendarRoot": t('attribute_detail.workspace_calendar_root'), - "workspaceTemplate": t('attribute_detail.workspace_template'), - "searchHome": t('attribute_detail.search_home'), - "workspaceSearchHome": t('attribute_detail.workspace_search_home'), - "inbox": t('attribute_detail.inbox'), - "workspaceInbox": t('attribute_detail.workspace_inbox'), - "sqlConsoleHome": t('attribute_detail.sql_console_home'), - "bookmarkFolder": t('attribute_detail.bookmark_folder'), - "shareHiddenFromTree": t('attribute_detail.share_hidden_from_tree'), - "shareExternalLink": t('attribute_detail.share_external_link'), - "shareAlias": t('attribute_detail.share_alias'), - "shareOmitDefaultCss": t('attribute_detail.share_omit_default_css'), - "shareRoot": t('attribute_detail.share_root'), - "shareDescription": t('attribute_detail.share_description'), - "shareRaw": t('attribute_detail.share_raw'), - "shareDisallowRobotIndexing": t('attribute_detail.share_disallow_robot_indexing'), - "shareCredentials": t('attribute_detail.share_credentials'), - "shareIndex": t('attribute_detail.share_index'), - "displayRelations": t('attribute_detail.display_relations'), - "hideRelations": t('attribute_detail.hide_relations'), - "titleTemplate": t('attribute_detail.title_template'), - "template": t('attribute_detail.template'), - "toc": t('attribute_detail.toc'), - "color": t('attribute_detail.color'), - "keyboardShortcut": t('attribute_detail.keyboard_shortcut'), - "keepCurrentHoisting": t('attribute_detail.keep_current_hoisting'), - "executeButton": t('attribute_detail.execute_button'), - "executeDescription": t('attribute_detail.execute_description'), - "excludeFromNoteMap": t('attribute_detail.exclude_from_note_map'), - "newNotesOnTop": t('attribute_detail.new_notes_on_top'), - "hideHighlightWidget": t('attribute_detail.hide_highlight_widget') + label: { + disableVersioning: t("attribute_detail.disable_versioning"), + calendarRoot: t("attribute_detail.calendar_root"), + archived: t("attribute_detail.archived"), + excludeFromExport: t("attribute_detail.exclude_from_export"), + run: t("attribute_detail.run"), + runOnInstance: t("attribute_detail.run_on_instance"), + runAtHour: t("attribute_detail.run_at_hour"), + disableInclusion: t("attribute_detail.disable_inclusion"), + sorted: t("attribute_detail.sorted"), + sortDirection: t("attribute_detail.sort_direction"), + sortFoldersFirst: t("attribute_detail.sort_folders_first"), + top: t("attribute_detail.top"), + hidePromotedAttributes: t("attribute_detail.hide_promoted_attributes"), + readOnly: t("attribute_detail.read_only"), + autoReadOnlyDisabled: t("attribute_detail.auto_read_only_disabled"), + appCss: t("attribute_detail.app_css"), + appTheme: t("attribute_detail.app_theme"), + appThemeBase: t("attribute_detail.app_theme_base"), + cssClass: t("attribute_detail.css_class"), + iconClass: t("attribute_detail.icon_class"), + pageSize: t("attribute_detail.page_size"), + customRequestHandler: t("attribute_detail.custom_request_handler"), + customResourceProvider: t("attribute_detail.custom_resource_provider"), + widget: t("attribute_detail.widget"), + workspace: t("attribute_detail.workspace"), + workspaceIconClass: t("attribute_detail.workspace_icon_class"), + workspaceTabBackgroundColor: t("attribute_detail.workspace_tab_background_color"), + workspaceCalendarRoot: t("attribute_detail.workspace_calendar_root"), + workspaceTemplate: t("attribute_detail.workspace_template"), + searchHome: t("attribute_detail.search_home"), + workspaceSearchHome: t("attribute_detail.workspace_search_home"), + inbox: t("attribute_detail.inbox"), + workspaceInbox: t("attribute_detail.workspace_inbox"), + sqlConsoleHome: t("attribute_detail.sql_console_home"), + bookmarkFolder: t("attribute_detail.bookmark_folder"), + shareHiddenFromTree: t("attribute_detail.share_hidden_from_tree"), + shareExternalLink: t("attribute_detail.share_external_link"), + shareAlias: t("attribute_detail.share_alias"), + shareOmitDefaultCss: t("attribute_detail.share_omit_default_css"), + shareRoot: t("attribute_detail.share_root"), + shareDescription: t("attribute_detail.share_description"), + shareRaw: t("attribute_detail.share_raw"), + shareDisallowRobotIndexing: t("attribute_detail.share_disallow_robot_indexing"), + shareCredentials: t("attribute_detail.share_credentials"), + shareIndex: t("attribute_detail.share_index"), + displayRelations: t("attribute_detail.display_relations"), + hideRelations: t("attribute_detail.hide_relations"), + titleTemplate: t("attribute_detail.title_template"), + template: t("attribute_detail.template"), + toc: t("attribute_detail.toc"), + color: t("attribute_detail.color"), + keyboardShortcut: t("attribute_detail.keyboard_shortcut"), + keepCurrentHoisting: t("attribute_detail.keep_current_hoisting"), + executeButton: t("attribute_detail.execute_button"), + executeDescription: t("attribute_detail.execute_description"), + excludeFromNoteMap: t("attribute_detail.exclude_from_note_map"), + newNotesOnTop: t("attribute_detail.new_notes_on_top"), + hideHighlightWidget: t("attribute_detail.hide_highlight_widget") }, - "relation": { - "runOnNoteCreation": t('attribute_detail.run_on_note_creation'), - "runOnChildNoteCreation": t('attribute_detail.run_on_child_note_creation'), - "runOnNoteTitleChange": t('attribute_detail.run_on_note_title_change'), - "runOnNoteContentChange": t('attribute_detail.run_on_note_content_change'), - "runOnNoteChange": t('attribute_detail.run_on_note_change'), - "runOnNoteDeletion": t('attribute_detail.run_on_note_deletion'), - "runOnBranchCreation": t('attribute_detail.run_on_branch_creation'), - "runOnBranchChange": t('attribute_detail.run_on_branch_change'), - "runOnBranchDeletion": t('attribute_detail.run_on_branch_deletion'), - "runOnAttributeCreation": t('attribute_detail.run_on_attribute_creation'), - "runOnAttributeChange": t('attribute_detail.run_on_attribute_change'), - "template": t('attribute_detail.relation_template'), - "inherit": t('attribute_detail.inherit'), - "renderNote": t('attribute_detail.render_note'), - "widget": t('attribute_detail.widget_relation'), - "shareCss": t('attribute_detail.share_css'), - "shareJs": t('attribute_detail.share_js'), - "shareTemplate": t('attribute_detail.share_template'), - "shareFavicon": t('attribute_detail.share_favicon') + relation: { + runOnNoteCreation: t("attribute_detail.run_on_note_creation"), + runOnChildNoteCreation: t("attribute_detail.run_on_child_note_creation"), + runOnNoteTitleChange: t("attribute_detail.run_on_note_title_change"), + runOnNoteContentChange: t("attribute_detail.run_on_note_content_change"), + runOnNoteChange: t("attribute_detail.run_on_note_change"), + runOnNoteDeletion: t("attribute_detail.run_on_note_deletion"), + runOnBranchCreation: t("attribute_detail.run_on_branch_creation"), + runOnBranchChange: t("attribute_detail.run_on_branch_change"), + runOnBranchDeletion: t("attribute_detail.run_on_branch_deletion"), + runOnAttributeCreation: t("attribute_detail.run_on_attribute_creation"), + runOnAttributeChange: t("attribute_detail.run_on_attribute_change"), + template: t("attribute_detail.relation_template"), + inherit: t("attribute_detail.inherit"), + renderNote: t("attribute_detail.render_note"), + widget: t("attribute_detail.widget_relation"), + shareCss: t("attribute_detail.share_css"), + shareJs: t("attribute_detail.share_js"), + shareTemplate: t("attribute_detail.share_template"), + shareFavicon: t("attribute_detail.share_favicon") } }; @@ -288,7 +288,6 @@ interface SearchRelatedResponse { } export default class AttributeDetailWidget extends NoteContextAwareWidget { - private $title!: JQuery; private $inputName!: JQuery; private $inputValue!: JQuery; @@ -335,39 +334,40 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.$widget = $(TPL); - shortcutService.bindElShortcut(this.$widget, 'ctrl+return', () => this.saveAndClose()); - shortcutService.bindElShortcut(this.$widget, 'esc', () => this.cancelAndClose()); + shortcutService.bindElShortcut(this.$widget, "ctrl+return", () => this.saveAndClose()); + shortcutService.bindElShortcut(this.$widget, "esc", () => this.cancelAndClose()); + this.$title = this.$widget.find(".attr-detail-title"); - this.$title = this.$widget.find('.attr-detail-title'); - - this.$inputName = this.$widget.find('.attr-input-name'); - this.$inputName.on('input', ev => { - if (!(ev.originalEvent as KeyboardEvent)?.isComposing) { // https://github.com/zadam/trilium/pull/3812 + this.$inputName = this.$widget.find(".attr-input-name"); + this.$inputName.on("input", (ev) => { + if (!(ev.originalEvent as KeyboardEvent)?.isComposing) { + // https://github.com/zadam/trilium/pull/3812 this.userEditedAttribute(); } }); - this.$inputName.on('change', () => this.userEditedAttribute()); - this.$inputName.on('autocomplete:closed', () => this.userEditedAttribute()); + this.$inputName.on("change", () => this.userEditedAttribute()); + this.$inputName.on("autocomplete:closed", () => this.userEditedAttribute()); - this.$inputName.on('focus', () => { + this.$inputName.on("focus", () => { attributeAutocompleteService.initAttributeNameAutocomplete({ $el: this.$inputName, - attributeType: () => ['relation', 'relation-definition'].includes(this.attrType || "") ? 'relation' : 'label', + attributeType: () => (["relation", "relation-definition"].includes(this.attrType || "") ? "relation" : "label"), open: true }); }); - this.$rowValue = this.$widget.find('.attr-row-value'); - this.$inputValue = this.$widget.find('.attr-input-value'); - this.$inputValue.on('input', ev => { - if (!(ev.originalEvent as KeyboardEvent)?.isComposing) { // https://github.com/zadam/trilium/pull/3812 + this.$rowValue = this.$widget.find(".attr-row-value"); + this.$inputValue = this.$widget.find(".attr-input-value"); + this.$inputValue.on("input", (ev) => { + if (!(ev.originalEvent as KeyboardEvent)?.isComposing) { + // https://github.com/zadam/trilium/pull/3812 this.userEditedAttribute(); } }); - this.$inputValue.on('change', () => this.userEditedAttribute()); - this.$inputValue.on('autocomplete:closed', () => this.userEditedAttribute()); - this.$inputValue.on('focus', () => { + this.$inputValue.on("change", () => this.userEditedAttribute()); + this.$inputValue.on("autocomplete:closed", () => this.userEditedAttribute()); + this.$inputValue.on("focus", () => { attributeAutocompleteService.initLabelValueAutocomplete({ $el: this.$inputValue, open: true, @@ -375,86 +375,84 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { }); }); - this.$rowPromoted = this.$widget.find('.attr-row-promoted'); - this.$inputPromoted = this.$widget.find('.attr-input-promoted'); - this.$inputPromoted.on('change', () => this.userEditedAttribute()); + this.$rowPromoted = this.$widget.find(".attr-row-promoted"); + this.$inputPromoted = this.$widget.find(".attr-input-promoted"); + this.$inputPromoted.on("change", () => this.userEditedAttribute()); - this.$rowPromotedAlias = this.$widget.find('.attr-row-promoted-alias'); - this.$inputPromotedAlias = this.$widget.find('.attr-input-promoted-alias'); - this.$inputPromotedAlias.on('change', () => this.userEditedAttribute()); + this.$rowPromotedAlias = this.$widget.find(".attr-row-promoted-alias"); + this.$inputPromotedAlias = this.$widget.find(".attr-input-promoted-alias"); + this.$inputPromotedAlias.on("change", () => this.userEditedAttribute()); - this.$rowMultiplicity = this.$widget.find('.attr-row-multiplicity'); - this.$inputMultiplicity = this.$widget.find('.attr-input-multiplicity'); - this.$inputMultiplicity.on('change', () => this.userEditedAttribute()); + this.$rowMultiplicity = this.$widget.find(".attr-row-multiplicity"); + this.$inputMultiplicity = this.$widget.find(".attr-input-multiplicity"); + this.$inputMultiplicity.on("change", () => this.userEditedAttribute()); - this.$rowLabelType = this.$widget.find('.attr-row-label-type'); - this.$inputLabelType = this.$widget.find('.attr-input-label-type'); - this.$inputLabelType.on('change', () => this.userEditedAttribute()); + this.$rowLabelType = this.$widget.find(".attr-row-label-type"); + this.$inputLabelType = this.$widget.find(".attr-input-label-type"); + this.$inputLabelType.on("change", () => this.userEditedAttribute()); - this.$rowNumberPrecision = this.$widget.find('.attr-row-number-precision'); - this.$inputNumberPrecision = this.$widget.find('.attr-input-number-precision'); - this.$inputNumberPrecision.on('change', () => this.userEditedAttribute()); + this.$rowNumberPrecision = this.$widget.find(".attr-row-number-precision"); + this.$inputNumberPrecision = this.$widget.find(".attr-input-number-precision"); + this.$inputNumberPrecision.on("change", () => this.userEditedAttribute()); - this.$rowInverseRelation = this.$widget.find('.attr-row-inverse-relation'); - this.$inputInverseRelation = this.$widget.find('.attr-input-inverse-relation'); - this.$inputInverseRelation.on('input', ev => { - if (!(ev.originalEvent as KeyboardEvent)?.isComposing) { // https://github.com/zadam/trilium/pull/3812 + this.$rowInverseRelation = this.$widget.find(".attr-row-inverse-relation"); + this.$inputInverseRelation = this.$widget.find(".attr-input-inverse-relation"); + this.$inputInverseRelation.on("input", (ev) => { + if (!(ev.originalEvent as KeyboardEvent)?.isComposing) { + // https://github.com/zadam/trilium/pull/3812 this.userEditedAttribute(); } }); - this.$rowTargetNote = this.$widget.find('.attr-row-target-note'); - this.$inputTargetNote = this.$widget.find('.attr-input-target-note'); + this.$rowTargetNote = this.$widget.find(".attr-row-target-note"); + this.$inputTargetNote = this.$widget.find(".attr-input-target-note"); - noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, { allowCreatingNotes: true }) - .on('autocomplete:noteselected', (event, suggestion, dataset) => { - if (!suggestion.notePath) { - return false; - } + noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, { allowCreatingNotes: true }).on("autocomplete:noteselected", (event, suggestion, dataset) => { + if (!suggestion.notePath) { + return false; + } - const pathChunks = suggestion.notePath.split('/'); + const pathChunks = suggestion.notePath.split("/"); - this.attribute.value = pathChunks[pathChunks.length - 1]; // noteId + this.attribute.value = pathChunks[pathChunks.length - 1]; // noteId - this.triggerCommand('updateAttributeList', { attributes: this.allAttributes }); - this.updateRelatedNotes(); + this.triggerCommand("updateAttributeList", { attributes: this.allAttributes }); + this.updateRelatedNotes(); + }); + + this.$inputInheritable = this.$widget.find(".attr-input-inheritable"); + this.$inputInheritable.on("change", () => this.userEditedAttribute()); + + this.$closeAttrDetailButton = this.$widget.find(".close-attr-detail-button"); + this.$closeAttrDetailButton.on("click", () => this.cancelAndClose()); + + this.$attrIsOwnedBy = this.$widget.find(".attr-is-owned-by"); + + this.$attrSaveDeleteButtonContainer = this.$widget.find(".attr-save-delete-button-container"); + + this.$saveAndCloseButton = this.$widget.find(".attr-save-changes-and-close-button"); + this.$saveAndCloseButton.on("click", () => this.saveAndClose()); + + this.$deleteButton = this.$widget.find(".attr-delete-button"); + this.$deleteButton.on("click", async () => { + await this.triggerCommand("updateAttributeList", { + attributes: this.allAttributes.filter((attr) => attr !== this.attribute) }); - this.$inputInheritable = this.$widget.find('.attr-input-inheritable'); - this.$inputInheritable.on('change', () => this.userEditedAttribute()); - - this.$closeAttrDetailButton = this.$widget.find('.close-attr-detail-button'); - this.$closeAttrDetailButton.on('click', () => this.cancelAndClose()); - - this.$attrIsOwnedBy = this.$widget.find('.attr-is-owned-by'); - - this.$attrSaveDeleteButtonContainer = this.$widget.find('.attr-save-delete-button-container'); - - this.$saveAndCloseButton = this.$widget.find('.attr-save-changes-and-close-button'); - this.$saveAndCloseButton.on('click', () => this.saveAndClose()); - - this.$deleteButton = this.$widget.find('.attr-delete-button'); - this.$deleteButton.on('click', async () => { - await this.triggerCommand('updateAttributeList', { - attributes: this.allAttributes.filter(attr => attr !== this.attribute) - }); - - await this.triggerCommand('saveAttributes'); + await this.triggerCommand("saveAttributes"); this.hide(); }); - this.$attrHelp = this.$widget.find('.attr-help'); + this.$attrHelp = this.$widget.find(".attr-help"); - this.$relatedNotesContainer = this.$widget.find('.related-notes-container'); - this.$relatedNotesTitle = this.$relatedNotesContainer.find('.related-notes-tile'); - this.$relatedNotesList = this.$relatedNotesContainer.find('.related-notes-list'); - this.$relatedNotesMoreNotes = this.$relatedNotesContainer.find('.related-notes-more-notes'); + this.$relatedNotesContainer = this.$widget.find(".related-notes-container"); + this.$relatedNotesTitle = this.$relatedNotesContainer.find(".related-notes-tile"); + this.$relatedNotesList = this.$relatedNotesContainer.find(".related-notes-list"); + this.$relatedNotesMoreNotes = this.$relatedNotesContainer.find(".related-notes-more-notes"); - $(window).on('mousedown', e => { - if (!$(e.target).closest(this.$widget[0]).length - && !$(e.target).closest(".algolia-autocomplete").length - && !$(e.target).closest("#context-menu-container").length) { + $(window).on("mousedown", (e) => { + if (!$(e.target).closest(this.$widget[0]).length && !$(e.target).closest(".algolia-autocomplete").length && !$(e.target).closest("#context-menu-container").length) { this.hide(); } }); @@ -471,13 +469,9 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.attrType = this.getAttrType(attribute); - const attrName = - this.attrType === 'label-definition' ? attribute.name.substr(6) - : (this.attrType === 'relation-definition' ? attribute.name.substr(9) : attribute.name); + const attrName = this.attrType === "label-definition" ? attribute.name.substr(6) : this.attrType === "relation-definition" ? attribute.name.substr(9) : attribute.name; - const definition = this.attrType?.endsWith('-definition') - ? promotedAttributeDefinitionParser.parse(attribute.value || "") - : {}; + const definition = this.attrType?.endsWith("-definition") ? promotedAttributeDefinitionParser.parse(attribute.value || "") : {}; if (this.attrType) { this.$title.text(ATTR_TITLES[this.attrType]); @@ -497,75 +491,51 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.$attrIsOwnedBy .show() .empty() - .append(attribute.type === 'label' ? 'Label' : 'Relation') + .append(attribute.type === "label" ? "Label" : "Relation") .append(` ${t("attribute_detail.is_owned_by_note")} `) - .append(await linkService.createLink(attribute.noteId)) + .append(await linkService.createLink(attribute.noteId)); } - const disabledFn = (() => !isOwned ? "true" : undefined); + const disabledFn = () => (!isOwned ? "true" : undefined); - this.$inputName - .val(attrName) - .attr('readonly', disabledFn); + this.$inputName.val(attrName).attr("readonly", disabledFn); - this.$rowValue.toggle(this.attrType === 'label'); - this.$rowTargetNote.toggle(this.attrType === 'relation'); + this.$rowValue.toggle(this.attrType === "label"); + this.$rowTargetNote.toggle(this.attrType === "relation"); - this.$rowPromoted.toggle(['label-definition', 'relation-definition'].includes(this.attrType || "")); - this.$inputPromoted - .prop("checked", !!definition.isPromoted) - .attr('disabled', disabledFn); + this.$rowPromoted.toggle(["label-definition", "relation-definition"].includes(this.attrType || "")); + this.$inputPromoted.prop("checked", !!definition.isPromoted).attr("disabled", disabledFn); this.$rowPromotedAlias.toggle(!!definition.isPromoted); - this.$inputPromotedAlias - .val(definition.promotedAlias || "") - .attr('disabled', disabledFn); + this.$inputPromotedAlias.val(definition.promotedAlias || "").attr("disabled", disabledFn); - this.$rowMultiplicity.toggle(['label-definition', 'relation-definition'].includes(this.attrType || "")); - this.$inputMultiplicity - .val(definition.multiplicity || "") - .attr('disabled', disabledFn); + this.$rowMultiplicity.toggle(["label-definition", "relation-definition"].includes(this.attrType || "")); + this.$inputMultiplicity.val(definition.multiplicity || "").attr("disabled", disabledFn); - this.$rowLabelType.toggle(this.attrType === 'label-definition'); - this.$inputLabelType - .val(definition.labelType || "") - .attr('disabled', disabledFn); + this.$rowLabelType.toggle(this.attrType === "label-definition"); + this.$inputLabelType.val(definition.labelType || "").attr("disabled", disabledFn); - this.$rowNumberPrecision.toggle(this.attrType === 'label-definition' && definition.labelType === 'number'); - this.$inputNumberPrecision - .val(definition.numberPrecision || "") - .attr('disabled', disabledFn); + this.$rowNumberPrecision.toggle(this.attrType === "label-definition" && definition.labelType === "number"); + this.$inputNumberPrecision.val(definition.numberPrecision || "").attr("disabled", disabledFn); - this.$rowInverseRelation.toggle(this.attrType === 'relation-definition'); - this.$inputInverseRelation - .val(definition.inverseRelation || "") - .attr('disabled', disabledFn); + this.$rowInverseRelation.toggle(this.attrType === "relation-definition"); + this.$inputInverseRelation.val(definition.inverseRelation || "").attr("disabled", disabledFn); - if (attribute.type === 'label') { - this.$inputValue - .val(attribute.value || "") - .attr('readonly', disabledFn); - } - else if (attribute.type === 'relation') { - this.$inputTargetNote - .attr('readonly', disabledFn) - .val("") - .setSelectedNotePath(""); + if (attribute.type === "label") { + this.$inputValue.val(attribute.value || "").attr("readonly", disabledFn); + } else if (attribute.type === "relation") { + this.$inputTargetNote.attr("readonly", disabledFn).val("").setSelectedNotePath(""); if (attribute.value) { const targetNote = await froca.getNote(attribute.value); if (targetNote) { - this.$inputTargetNote - .val(targetNote ? targetNote.title : "") - .setSelectedNotePath(attribute.value); + this.$inputTargetNote.val(targetNote ? targetNote.title : "").setSelectedNotePath(attribute.value); } } } - this.$inputInheritable - .prop("checked", !!attribute.isInheritable) - .attr('disabled', disabledFn); + this.$inputInheritable.prop("checked", !!attribute.isInheritable).attr("disabled", disabledFn); this.updateHelp(); @@ -581,16 +551,11 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { .css("left", detPosition.left) .css("right", detPosition.right) .css("top", y - offset.top + 70) - .css("max-height", - outerHeight + y > height - 50 - ? height - y - 50 - : 10000); + .css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000); } - if (focus === 'name') { - this.$inputName - .trigger('focus') - .trigger('select'); + if (focus === "name") { + this.$inputName.trigger("focus").trigger("select"); } } @@ -618,7 +583,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { } async saveAndClose() { - await this.triggerCommand('saveAttributes'); + await this.triggerCommand("saveAttributes"); this.hide(); @@ -626,7 +591,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { } async cancelAndClose() { - await this.triggerCommand('reloadAttributes'); + await this.triggerCommand("reloadAttributes"); this.hide(); @@ -642,24 +607,18 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { updateHelp() { const attrName = String(this.$inputName.val()); - if (this.attrType && this.attrType in ATTR_HELP && - attrName && attrName in ATTR_HELP[this.attrType]) { + if (this.attrType && this.attrType in ATTR_HELP && attrName && attrName in ATTR_HELP[this.attrType]) { this.$attrHelp .empty() - .append($("") - .append($("").text(attrName)) - .append(" - ") - .append(ATTR_HELP[this.attrType][attrName]) - ) + .append($("").append($("").text(attrName)).append(" - ").append(ATTR_HELP[this.attrType][attrName])) .show(); - } - else { + } else { this.$attrHelp.empty().hide(); } } async updateRelatedNotes() { - let { results, count } = await server.post('search-related', this.attribute); + let { results, count } = await server.post("search-related", this.attribute); for (const res of results) { res.noteId = res.notePathArray[res.notePathArray.length - 1]; @@ -676,16 +635,14 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.$relatedNotesList.empty(); 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) { const notePath = note.getBestNotePathString(hoistedNoteId); const $noteLink = await linkService.createLink(notePath, { showNotePath: true }); - this.$relatedNotesList.append( - $("
  • ").append($noteLink) - ); + this.$relatedNotesList.append($("
  • ").append($noteLink)); } if (results.length > DISPLAYED_NOTES) { @@ -697,20 +654,18 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { } getAttrType(attribute: Attribute) { - if (attribute.type === 'label') { - if (attribute.name.startsWith('label:')) { + if (attribute.type === "label") { + if (attribute.name.startsWith("label:")) { return "label-definition"; - } else if (attribute.name.startsWith('relation:')) { + } else if (attribute.name.startsWith("relation:")) { return "relation-definition"; } else { return "label"; } - } - else if (attribute.type === 'relation') { + } else if (attribute.type === "relation") { return "relation"; - } - else { - this.$title.text(''); + } else { + this.$title.text(""); } } @@ -724,26 +679,24 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.$inputName.val(attrName); } - if (this.attrType === 'label-definition') { + if (this.attrType === "label-definition") { attrName = `label:${attrName}`; - } else if (this.attrType === 'relation-definition') { + } else if (this.attrType === "relation-definition") { attrName = `relation:${attrName}`; } this.attribute.name = attrName; this.attribute.isInheritable = this.$inputInheritable.is(":checked"); - if (this.attrType?.endsWith('-definition')) { + if (this.attrType?.endsWith("-definition")) { this.attribute.value = this.buildDefinitionValue(); - } - else if (this.attrType === 'relation') { + } else if (this.attrType === "relation") { this.attribute.value = this.$inputTargetNote.getSelectedNoteId() || ""; - } - else { + } else { this.attribute.value = String(this.$inputValue.val()); } - this.triggerCommand('updateAttributeList', { attributes: this.allAttributes }); + this.triggerCommand("updateAttributeList", { attributes: this.allAttributes }); } buildDefinitionValue() { @@ -752,28 +705,26 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { if (this.$inputPromoted.is(":checked")) { props.push("promoted"); - if (this.$inputPromotedAlias.val() !== '') { + if (this.$inputPromotedAlias.val() !== "") { props.push(`alias=${this.$inputPromotedAlias.val()}`); } } props.push(this.$inputMultiplicity.val()); - if (this.attrType === 'label-definition') { + if (this.attrType === "label-definition") { props.push(this.$inputLabelType.val()); - if (this.$inputLabelType.val() === 'number' && this.$inputNumberPrecision.val() !== '') { + if (this.$inputLabelType.val() === "number" && this.$inputNumberPrecision.val() !== "") { props.push(`precision=${this.$inputNumberPrecision.val()}`); } - } else if (this.attrType === 'relation-definition' && String(this.$inputInverseRelation.val())?.trim().length > 0) { + } else if (this.attrType === "relation-definition" && String(this.$inputInverseRelation.val())?.trim().length > 0) { const inverseRelationName = this.$inputInverseRelation.val(); props.push(`inverse=${utils.filterAttributeName(String(inverseRelationName))}`); } - this.$rowNumberPrecision.toggle( - this.attrType === 'label-definition' - && this.$inputLabelType.val() === 'number'); + this.$rowNumberPrecision.toggle(this.attrType === "label-definition" && this.$inputLabelType.val() === "number"); this.$rowPromotedAlias.toggle(this.$inputPromoted.is(":checked")); @@ -787,7 +738,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { createLink(noteId: string) { return $("", { href: `#root/${noteId}`, - class: 'reference-link' + class: "reference-link" }); } diff --git a/src/public/app/widgets/attribute_widgets/attribute_editor.ts b/src/public/app/widgets/attribute_widgets/attribute_editor.ts index d01825c23..68dafbcbc 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_editor.ts +++ b/src/public/app/widgets/attribute_widgets/attribute_editor.ts @@ -86,10 +86,10 @@ const TPL = ` const mentionSetup: MentionConfig = { feeds: [ { - marker: '@', - feed: queryText => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), - itemRenderer: item => { - const itemElement = document.createElement('button'); + marker: "@", + feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), + itemRenderer: (item) => { + const itemElement = document.createElement("button"); itemElement.innerHTML = `${item.highlightedNotePathTitle} `; @@ -98,29 +98,29 @@ const mentionSetup: MentionConfig = { minimumCharacters: 0 }, { - marker: '#', - feed: async queryText => { + marker: "#", + feed: async (queryText) => { const names = await server.get(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`); - return names.map(name => { + return names.map((name) => { return { id: `#${name}`, name: name - } + }; }); }, minimumCharacters: 0 }, { - marker: '~', - feed: async queryText => { + marker: "~", + feed: async (queryText) => { const names = await server.get(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`); - return names.map(name => { + return names.map((name) => { return { id: `~${name}`, name: name - } + }; }); }, minimumCharacters: 0 @@ -130,53 +130,53 @@ const mentionSetup: MentionConfig = { const editorConfig = { removePlugins: [ - 'Heading', - 'Link', - 'Autoformat', - 'Bold', - 'Italic', - 'Underline', - 'Strikethrough', - 'Code', - 'Superscript', - 'Subscript', - 'BlockQuote', - 'Image', - 'ImageCaption', - 'ImageStyle', - 'ImageToolbar', - 'ImageUpload', - 'ImageResize', - 'List', - 'TodoList', - 'PasteFromOffice', - 'Table', - 'TableToolbar', - 'TableProperties', - 'TableCellProperties', - 'Indent', - 'IndentBlock', - 'BlockToolbar', - 'ParagraphButtonUI', - 'HeadingButtonsUI', - 'UploadimagePlugin', - 'InternalLinkPlugin', - 'MarkdownImportPlugin', - 'CuttonotePlugin', - 'TextTransformation', - 'Font', - 'FontColor', - 'FontBackgroundColor', - 'CodeBlock', - 'SelectAll', - 'IncludeNote', - 'CutToNote', - 'Mathematics', - 'AutoformatMath', - 'indentBlockShortcutPlugin', - 'removeFormatLinksPlugin', - 'Footnotes', - 'Mermaid' + "Heading", + "Link", + "Autoformat", + "Bold", + "Italic", + "Underline", + "Strikethrough", + "Code", + "Superscript", + "Subscript", + "BlockQuote", + "Image", + "ImageCaption", + "ImageStyle", + "ImageToolbar", + "ImageUpload", + "ImageResize", + "List", + "TodoList", + "PasteFromOffice", + "Table", + "TableToolbar", + "TableProperties", + "TableCellProperties", + "Indent", + "IndentBlock", + "BlockToolbar", + "ParagraphButtonUI", + "HeadingButtonsUI", + "UploadimagePlugin", + "InternalLinkPlugin", + "MarkdownImportPlugin", + "CuttonotePlugin", + "TextTransformation", + "Font", + "FontColor", + "FontBackgroundColor", + "CodeBlock", + "SelectAll", + "IncludeNote", + "CutToNote", + "Mathematics", + "AutoformatMath", + "indentBlockShortcutPlugin", + "removeFormatLinksPlugin", + "Footnotes", + "Mermaid" ], toolbar: { items: [] @@ -187,11 +187,7 @@ const editorConfig = { type AttributeCommandNames = FilteredCommandNames; -export default class AttributeEditorWidget extends NoteContextAwareWidget implements - EventListener<"entitiesReloaded">, - EventListener<"addNewLabel">, - EventListener<"addNewRelation"> { - +export default class AttributeEditorWidget extends NoteContextAwareWidget implements EventListener<"entitiesReloaded">, EventListener<"addNewLabel">, EventListener<"addNewRelation"> { private attributeDetailWidget: AttributeDetailWidget; private $editor!: JQuery; private $addNewAttributeButton!: JQuery; @@ -210,11 +206,11 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem doRender() { this.$widget = $(TPL); - this.$editor = this.$widget.find('.attribute-list-editor'); + this.$editor = this.$widget.find(".attribute-list-editor"); this.initialized = this.initEditor(); - this.$editor.on('keydown', async e => { + this.$editor.on("keydown", async (e) => { if (e.which === 13) { // allow autocomplete to fill the result textarea setTimeout(() => this.save(), 100); @@ -223,48 +219,48 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem this.attributeDetailWidget.hide(); }); - this.$editor.on('blur', () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160 + this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160 - this.$addNewAttributeButton = this.$widget.find('.add-new-attribute-button'); - this.$addNewAttributeButton.on('click', e => this.addNewAttribute(e)); + this.$addNewAttributeButton = this.$widget.find(".add-new-attribute-button"); + this.$addNewAttributeButton.on("click", (e) => this.addNewAttribute(e)); - this.$saveAttributesButton = this.$widget.find('.save-attributes-button'); - this.$saveAttributesButton.on('click', () => this.save()); + this.$saveAttributesButton = this.$widget.find(".save-attributes-button"); + this.$saveAttributesButton.on("click", () => this.save()); - this.$errors = this.$widget.find('.attribute-errors'); + this.$errors = this.$widget.find(".attribute-errors"); } addNewAttribute(e: JQuery.ClickEvent) { contextMenuService.show({ x: e.pageX, y: e.pageY, - orientation: 'left', + orientation: "left", items: [ { title: t("attribute_editor.add_new_label"), command: "addNewLabel", uiIcon: "bx bx-hash" }, { title: t("attribute_editor.add_new_relation"), command: "addNewRelation", uiIcon: "bx bx-transfer" }, { title: "----" }, { title: t("attribute_editor.add_new_label_definition"), command: "addNewLabelDefinition", uiIcon: "bx bx-empty" }, - { title: t("attribute_editor.add_new_relation_definition"), command: "addNewRelationDefinition", uiIcon: "bx bx-empty" }, + { title: t("attribute_editor.add_new_relation_definition"), command: "addNewRelationDefinition", uiIcon: "bx bx-empty" } ], selectMenuItemHandler: ({ command }) => this.handleAddNewAttributeCommand(command) }); } // triggered from keyboard shortcut - async addNewLabelEvent({ntxId}: EventData<"addNewLabel">) { + async addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) { if (this.isNoteContext(ntxId)) { await this.refresh(); - this.handleAddNewAttributeCommand('addNewLabel'); + this.handleAddNewAttributeCommand("addNewLabel"); } } // triggered from keyboard shortcut - async addNewRelationEvent({ntxId}: EventData<"addNewRelation">) { + async addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) { if (this.isNoteContext(ntxId)) { await this.refresh(); - this.handleAddNewAttributeCommand('addNewRelation'); + this.handleAddNewAttributeCommand("addNewRelation"); } } @@ -280,22 +276,22 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem let name; let value; - if (command === 'addNewLabel') { - type = 'label'; - name = 'myLabel'; - value = ''; - } else if (command === 'addNewRelation') { - type = 'relation'; - name = 'myRelation'; - value = ''; - } else if (command === 'addNewLabelDefinition') { - type = 'label'; - name = 'label:myLabel'; - value = 'promoted,single,text'; - } else if (command === 'addNewRelationDefinition') { - type = 'label'; - name = 'relation:myRelation'; - value = 'promoted,single'; + if (command === "addNewLabel") { + type = "label"; + name = "myLabel"; + value = ""; + } else if (command === "addNewRelation") { + type = "relation"; + name = "myRelation"; + value = ""; + } else if (command === "addNewLabelDefinition") { + type = "label"; + name = "label:myLabel"; + value = "promoted,single,text"; + } else if (command === "addNewRelationDefinition") { + type = "label"; + name = "relation:myRelation"; + value = "promoted,single"; } else { return; } @@ -323,7 +319,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem isOwned: true, x: (rect.left + rect.right) / 2, y: rect.bottom, - focus: 'name' + focus: "name" }); }, 100); } @@ -343,10 +339,10 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem this.$saveAttributesButton.fadeOut(); // blink the attribute text to give a visual hint that save has been executed - this.$editor.css('opacity', 0); + this.$editor.css("opacity", 0); // revert back - setTimeout(() => this.$editor.css('opacity', 1), 100); + setTimeout(() => this.$editor.css("opacity", 1), 100); } } @@ -359,7 +355,8 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem } getPreprocessedData() { - const str = this.textEditor.getData() + const str = this.textEditor + .getData() .replace(/]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1") .replace(/ /g, " "); // otherwise .text() below outputs non-breaking space in unicode @@ -371,18 +368,22 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem this.$widget.show(); - this.$editor.on("click", e => this.handleEditorClick(e)); + this.$editor.on("click", (e) => this.handleEditorClick(e)); this.textEditor = await CKEditor.BalloonEditor.create(this.$editor[0], editorConfig); - this.textEditor.model.document.on('change:data', () => this.dataChanged()); - this.textEditor.editing.view.document.on('enter', (event, data) => { - // disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422 - data.preventDefault(); - event.stop(); - }, {priority: 'high'}); + this.textEditor.model.document.on("change:data", () => this.dataChanged()); + this.textEditor.editing.view.document.on( + "enter", + (event, data) => { + // disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422 + data.preventDefault(); + event.stop(); + }, + { priority: "high" } + ); // disable spellcheck for attribute editor - this.textEditor.editing.view.change(writer => writer.setAttribute('spellcheck', 'false', this.textEditor.editing.view.document.getRoot())); + this.textEditor.editing.view.change((writer) => writer.setAttribute("spellcheck", "false", this.textEditor.editing.view.document.getRoot())); } dataChanged() { @@ -390,8 +391,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem if (this.lastSavedContent === this.textEditor.getData()) { this.$saveAttributesButton.fadeOut(); - } - else { + } else { this.$saveAttributesButton.fadeIn(); } @@ -412,8 +412,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem try { parsedAttrs = attributeParser.lexAndParse(this.getPreprocessedData(), true); - } - catch (e) { + } catch (e) { // the input is incorrect because the user messed up with it and now needs to fix it manually return null; } @@ -421,8 +420,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem let matchedAttr = null; for (const attr of parsedAttrs) { - if (attr.startIndex && clickIndex > attr.startIndex && - attr.endIndex && clickIndex <= attr.endIndex) { + if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) { matchedAttr = attr; break; } @@ -430,7 +428,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem setTimeout(() => { if (matchedAttr) { - this.$editor.tooltip('hide'); + this.$editor.tooltip("hide"); this.attributeDetailWidget.showAttributeDetail({ allAttributes: parsedAttrs, @@ -439,13 +437,11 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem x: e.pageX, y: e.pageY }); - } - else { + } else { this.showHelpTooltip(); } }, 100); - } - else { + } else { this.showHelpTooltip(); } } @@ -454,14 +450,14 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem this.attributeDetailWidget.hide(); this.$editor.tooltip({ - trigger: 'focus', + trigger: "focus", html: true, title: HELP_TEXT, - placement: 'bottom', + placement: "bottom", offset: "0,30" }); - this.$editor.tooltip('show'); + this.$editor.tooltip("show"); } getClickIndex(pos: TextPosition) { @@ -472,8 +468,8 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem while (curNode.previousSibling) { curNode = curNode.previousSibling; - if (curNode.name === 'reference') { - clickIndex += curNode._attrs.get('notePath').length + 1; + if (curNode.name === "reference") { + clickIndex += curNode._attrs.get("notePath").length + 1; } else { clickIndex += curNode.data.length; } @@ -483,9 +479,9 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem } async loadReferenceLinkTitle($el: JQuery, href: string) { - const {noteId} = linkService.parseNavigationStateFromUrl(href); + const { noteId } = linkService.parseNavigationStateFromUrl(href); const note = noteId ? await froca.getNote(noteId, true) : null; - const title = note ? note.title : '[missing]'; + const title = note ? note.title : "[missing]"; $el.text(title); } @@ -530,19 +526,18 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem } focus() { - this.$editor.trigger('focus'); + this.$editor.trigger("focus"); - this.textEditor.model.change( writer => { - const positionAt = writer.createPositionAt(this.textEditor.model.document.getRoot(), 'end'); + this.textEditor.model.change((writer) => { + const positionAt = writer.createPositionAt(this.textEditor.model.document.getRoot(), "end"); writer.setSelection(positionAt); - } ); + }); } - entitiesReloadedEvent({loadResults}: EventData<"entitiesReloaded">) { - if (loadResults.getAttributeRows(this.componentId).find(attr => attributeService.isAffecting(attr, this.note))) { + entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { + if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) { this.refresh(); } } - } diff --git a/src/public/app/widgets/basic_widget.ts b/src/public/app/widgets/basic_widget.ts index bbf734775..ec10acde6 100644 --- a/src/public/app/widgets/basic_widget.ts +++ b/src/public/app/widgets/basic_widget.ts @@ -14,7 +14,7 @@ export class TypedBasicWidget> extends TypedCompon super(); this.attrs = { - style: '' + style: "" }; this.classes = []; @@ -101,13 +101,13 @@ export class TypedBasicWidget> extends TypedCompon } collapsible() { - this.css('min-height', '0'); - this.css('min-width', '0'); + this.css("min-height", "0"); + this.css("min-width", "0"); return this; } filling() { - this.css('flex-grow', '1'); + this.css("flex-grow", "1"); return this; } @@ -127,31 +127,28 @@ export class TypedBasicWidget> extends TypedCompon this.logRenderingError(e); } - this.$widget.attr('data-component-id', this.componentId); - this.$widget - .addClass('component') - .prop('component', this); + this.$widget.attr("data-component-id", this.componentId); + this.$widget.addClass("component").prop("component", this); if (!this.isEnabled()) { this.toggleInt(false); } if (this.cssEl) { - const css = this.cssEl.trim().startsWith('`; + const css = this.cssEl.trim().startsWith("`; this.$widget.append(css); } for (const key in this.attrs) { - if (key === 'style') { + if (key === "style") { if (this.attrs[key]) { - let style = this.$widget.attr('style'); + let style = this.$widget.attr("style"); style = style ? `${style}; ${this.attrs[key]}` : this.attrs[key]; this.$widget.attr(key, style); } - } - else { + } else { this.$widget.attr(key, this.attrs[key]); } } @@ -209,19 +206,19 @@ export class TypedBasicWidget> extends TypedCompon doRender() {} toggleInt(show: boolean) { - this.$widget.toggleClass('hidden-int', !show); + this.$widget.toggleClass("hidden-int", !show); } isHiddenInt() { - return this.$widget.hasClass('hidden-int'); + return this.$widget.hasClass("hidden-int"); } toggleExt(show: boolean) { - this.$widget.toggleClass('hidden-ext', !show); + this.$widget.toggleClass("hidden-ext", !show); } isHiddenExt() { - return this.$widget.hasClass('hidden-ext'); + return this.$widget.hasClass("hidden-ext"); } canBeShown() { @@ -245,8 +242,7 @@ export class TypedBasicWidget> extends TypedCompon getClosestNtxId() { if (this.$widget) { return this.$widget.closest("[data-ntx-id]").attr("data-ntx-id"); - } - else { + } else { return null; } } @@ -259,6 +255,4 @@ export class TypedBasicWidget> extends TypedCompon * * For information on using widgets, see the tutorial {@tutorial widget_basics}. */ -export default class BasicWidget extends TypedBasicWidget { - -} +export default class BasicWidget extends TypedBasicWidget {} diff --git a/src/public/app/widgets/bookmark_buttons.js b/src/public/app/widgets/bookmark_buttons.js index 1cb519e69..767522239 100644 --- a/src/public/app/widgets/bookmark_buttons.js +++ b/src/public/app/widgets/bookmark_buttons.js @@ -16,15 +16,12 @@ export default class BookmarkButtons extends FlexContainer { this.children = []; this.noteIds = []; - const bookmarkParentNote = await froca.getNote('_lbBookmarks'); + const bookmarkParentNote = await froca.getNote("_lbBookmarks"); for (const note of await bookmarkParentNote.getChildNotes()) { this.noteIds.push(note.noteId); - const buttonWidget = note.isLabelTruthy("bookmarkFolder") - ? new BookmarkFolderWidget(note) - : new OpenNoteButtonWidget(note) - .class("launcher-button"); + const buttonWidget = note.isLabelTruthy("bookmarkFolder") ? new BookmarkFolderWidget(note) : new OpenNoteButtonWidget(note).class("launcher-button"); if (this.settings.titlePlacement) { if (!buttonWidget.settings) { buttonWidget = {}; @@ -44,15 +41,12 @@ export default class BookmarkButtons extends FlexContainer { this.refresh(); } - entitiesReloadedEvent({loadResults}) { - if (loadResults.getBranchRows().find(branch => branch.parentNoteId === '_lbBookmarks')) { + entitiesReloadedEvent({ loadResults }) { + if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === "_lbBookmarks")) { this.refresh(); } - if (loadResults.getAttributeRows().find(attr => attr.type === 'label' - && ['iconClass', 'workspaceIconClass', 'bookmarkFolder'].includes(attr.name) - && this.noteIds.includes(attr.noteId)) - ) { + if (loadResults.getAttributeRows().find((attr) => attr.type === "label" && ["iconClass", "workspaceIconClass", "bookmarkFolder"].includes(attr.name) && this.noteIds.includes(attr.noteId))) { this.refresh(); } } diff --git a/src/public/app/widgets/bookmark_switch.js b/src/public/app/widgets/bookmark_switch.js index ab9055df1..acf940c7a 100644 --- a/src/public/app/widgets/bookmark_switch.js +++ b/src/public/app/widgets/bookmark_switch.js @@ -5,9 +5,11 @@ import { t } from "../services/i18n.js"; export default class BookmarkSwitchWidget extends SwitchWidget { isEnabled() { - return super.isEnabled() + return ( + super.isEnabled() && // it's not possible to bookmark root because that would clone it under bookmarks and thus create a cycle - && !['root', '_hidden'].includes(this.noteId); + !["root", "_hidden"].includes(this.noteId) + ); } doRender() { @@ -29,14 +31,14 @@ export default class BookmarkSwitchWidget extends SwitchWidget { } async refreshWithNote(note) { - const isBookmarked = !!note.getParentBranches().find(b => b.parentNoteId === '_lbBookmarks'); + const isBookmarked = !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks"); this.$switchOn.toggle(!isBookmarked); this.$switchOff.toggle(isBookmarked); } - entitiesReloadedEvent({loadResults}) { - if (loadResults.getBranchRows().find(b => b.noteId === this.noteId)) { + entitiesReloadedEvent({ loadResults }) { + if (loadResults.getBranchRows().find((b) => b.noteId === this.noteId)) { this.refresh(); } } diff --git a/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts b/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts index d1cf333b5..436f8fbd8 100644 --- a/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts +++ b/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts @@ -31,9 +31,10 @@ export default abstract class AbstractBulkAction { try { const $rendered = this.doRender(); - $rendered.find('.action-conf-del') - .on('click', () => this.deleteAction()) - .attr('title', t('abstract_bulk_action.remove_this_search_action')); + $rendered + .find(".action-conf-del") + .on("click", () => this.deleteAction()) + .attr("title", t("abstract_bulk_action.remove_this_search_action")); utils.initHelpDropdown($rendered); @@ -46,15 +47,17 @@ export default abstract class AbstractBulkAction { // to be overridden abstract doRender(): JQuery; - static get actionName() { return ""; } + static get actionName() { + return ""; + } async saveAction(data: {}) { const actionObject = Object.assign({ name: (this.constructor as typeof AbstractBulkAction).actionName }, data); await server.put(`notes/${this.attribute.noteId}/attribute`, { attributeId: this.attribute.attributeId, - type: 'label', - name: 'action', + type: "label", + name: "action", value: JSON.stringify(actionObject) }); diff --git a/src/public/app/widgets/bulk_actions/execute_script.ts b/src/public/app/widgets/bulk_actions/execute_script.ts index f28a5b348..6d363d8b7 100644 --- a/src/public/app/widgets/bulk_actions/execute_script.ts +++ b/src/public/app/widgets/bulk_actions/execute_script.ts @@ -5,7 +5,7 @@ import AbstractBulkAction from "./abstract_bulk_action.js"; const TPL = ` - ${t('execute_script.execute_script')} + ${t("execute_script.execute_script")} @@ -35,19 +35,23 @@ const TPL = ` `; export default class ExecuteScriptBulkAction extends AbstractBulkAction { - static get actionName() { return "executeScript"; } - static get actionTitle() { return t("execute_script.execute_script"); } + static get actionName() { + return "executeScript"; + } + static get actionTitle() { + return t("execute_script.execute_script"); + } doRender() { const $action = $(TPL); - const $script = $action.find('.script'); + const $script = $action.find(".script"); $script.val(this.actionDef.script || ""); const spacedUpdate = new SpacedUpdate(async () => { await this.saveAction({ script: $script.val() }); }, 1000); - $script.on('input', () => spacedUpdate.scheduleUpdate()); + $script.on("input", () => spacedUpdate.scheduleUpdate()); return $action; } diff --git a/src/public/app/widgets/bulk_actions/label/add_label.ts b/src/public/app/widgets/bulk_actions/label/add_label.ts index d5e4fa6fb..df0c1e006 100644 --- a/src/public/app/widgets/bulk_actions/label/add_label.ts +++ b/src/public/app/widgets/bulk_actions/label/add_label.ts @@ -39,16 +39,20 @@ const TPL = ` `; export default class AddLabelBulkAction extends AbstractBulkAction { - static get actionName() { return "addLabel"; } - static get actionTitle() { return t("add_label.add_label"); } + static get actionName() { + return "addLabel"; + } + static get actionTitle() { + return t("add_label.add_label"); + } doRender() { const $action = $(TPL); - const $labelName = $action.find('.label-name'); + const $labelName = $action.find(".label-name"); $labelName.val(this.actionDef.labelName || ""); - const $labelValue = $action.find('.label-value'); + const $labelValue = $action.find(".label-value"); $labelValue.val(this.actionDef.labelValue || ""); const spacedUpdate = new SpacedUpdate(async () => { @@ -58,8 +62,8 @@ export default class AddLabelBulkAction extends AbstractBulkAction { }); }, 1000); - $labelName.on('input', () => spacedUpdate.scheduleUpdate()); - $labelValue.on('input', () => spacedUpdate.scheduleUpdate()); + $labelName.on("input", () => spacedUpdate.scheduleUpdate()); + $labelValue.on("input", () => spacedUpdate.scheduleUpdate()); return $action; } diff --git a/src/public/app/widgets/bulk_actions/label/delete_label.ts b/src/public/app/widgets/bulk_actions/label/delete_label.ts index ba2c393a0..4fc92499a 100644 --- a/src/public/app/widgets/bulk_actions/label/delete_label.ts +++ b/src/public/app/widgets/bulk_actions/label/delete_label.ts @@ -20,19 +20,23 @@ const TPL = ` `; export default class DeleteLabelBulkAction extends AbstractBulkAction { - static get actionName() { return "deleteLabel"; } - static get actionTitle() { return t("delete_label.delete_label"); } + static get actionName() { + return "deleteLabel"; + } + static get actionTitle() { + return t("delete_label.delete_label"); + } doRender() { const $action = $(TPL); - const $labelName = $action.find('.label-name'); + const $labelName = $action.find(".label-name"); $labelName.val(this.actionDef.labelName || ""); const spacedUpdate = new SpacedUpdate(async () => { await this.saveAction({ labelName: $labelName.val() }); - }, 1000) + }, 1000); - $labelName.on('input', () => spacedUpdate.scheduleUpdate()); + $labelName.on("input", () => spacedUpdate.scheduleUpdate()); return $action; } diff --git a/src/public/app/widgets/bulk_actions/label/rename_label.ts b/src/public/app/widgets/bulk_actions/label/rename_label.ts index c250e795c..afda38a81 100644 --- a/src/public/app/widgets/bulk_actions/label/rename_label.ts +++ b/src/public/app/widgets/bulk_actions/label/rename_label.ts @@ -29,16 +29,20 @@ const TPL = ` `; export default class RenameLabelBulkAction extends AbstractBulkAction { - static get actionName() { return "renameLabel"; } - static get actionTitle() { return t("rename_label.rename_label"); } + static get actionName() { + return "renameLabel"; + } + static get actionTitle() { + return t("rename_label.rename_label"); + } doRender() { const $action = $(TPL); - const $oldLabelName = $action.find('.old-label-name'); + const $oldLabelName = $action.find(".old-label-name"); $oldLabelName.val(this.actionDef.oldLabelName || ""); - const $newLabelName = $action.find('.new-label-name'); + const $newLabelName = $action.find(".new-label-name"); $newLabelName.val(this.actionDef.newLabelName || ""); const spacedUpdate = new SpacedUpdate(async () => { @@ -48,8 +52,8 @@ export default class RenameLabelBulkAction extends AbstractBulkAction { }); }, 1000); - $oldLabelName.on('input', () => spacedUpdate.scheduleUpdate()); - $newLabelName.on('input', () => spacedUpdate.scheduleUpdate()); + $oldLabelName.on("input", () => spacedUpdate.scheduleUpdate()); + $newLabelName.on("input", () => spacedUpdate.scheduleUpdate()); return $action; } diff --git a/src/public/app/widgets/bulk_actions/label/update_label_value.ts b/src/public/app/widgets/bulk_actions/label/update_label_value.ts index 56137e5fd..dcd731bd5 100644 --- a/src/public/app/widgets/bulk_actions/label/update_label_value.ts +++ b/src/public/app/widgets/bulk_actions/label/update_label_value.ts @@ -34,16 +34,20 @@ const TPL = ` `; export default class UpdateLabelValueBulkAction extends AbstractBulkAction { - static get actionName() { return "updateLabelValue"; } - static get actionTitle() { return t("update_label_value.update_label_value"); } + static get actionName() { + return "updateLabelValue"; + } + static get actionTitle() { + return t("update_label_value.update_label_value"); + } doRender() { const $action = $(TPL); - const $labelName = $action.find('.label-name'); + const $labelName = $action.find(".label-name"); $labelName.val(this.actionDef.labelName || ""); - const $labelValue = $action.find('.label-value'); + const $labelValue = $action.find(".label-value"); $labelValue.val(this.actionDef.labelValue || ""); const spacedUpdate = new SpacedUpdate(async () => { @@ -51,10 +55,10 @@ export default class UpdateLabelValueBulkAction extends AbstractBulkAction { labelName: $labelName.val(), labelValue: $labelValue.val() }); - }, 1000) + }, 1000); - $labelName.on('input', () => spacedUpdate.scheduleUpdate()); - $labelValue.on('input', () => spacedUpdate.scheduleUpdate()); + $labelName.on("input", () => spacedUpdate.scheduleUpdate()); + $labelValue.on("input", () => spacedUpdate.scheduleUpdate()); return $action; } diff --git a/src/public/app/widgets/bulk_actions/note/delete_note.ts b/src/public/app/widgets/bulk_actions/note/delete_note.ts index d1708dd03..3899390c9 100644 --- a/src/public/app/widgets/bulk_actions/note/delete_note.ts +++ b/src/public/app/widgets/bulk_actions/note/delete_note.ts @@ -25,8 +25,12 @@ const TPL = ` `; export default class DeleteNoteBulkAction extends AbstractBulkAction { - static get actionName() { return "deleteNote"; } - static get actionTitle() { return t("delete_note.delete_note"); } + static get actionName() { + return "deleteNote"; + } + static get actionTitle() { + return t("delete_note.delete_note"); + } doRender() { return $(TPL); diff --git a/src/public/app/widgets/bulk_actions/note/delete_revisions.ts b/src/public/app/widgets/bulk_actions/note/delete_revisions.ts index d6363f3ab..25ecf247f 100644 --- a/src/public/app/widgets/bulk_actions/note/delete_revisions.ts +++ b/src/public/app/widgets/bulk_actions/note/delete_revisions.ts @@ -5,13 +5,13 @@ const TPL = ` - ${t('delete_revisions.delete_note_revisions')} + ${t("delete_revisions.delete_note_revisions")} @@ -19,8 +19,12 @@ const TPL = ` `; export default class DeleteRevisionsBulkAction extends AbstractBulkAction { - static get actionName() { return "deleteRevisions"; } - static get actionTitle() { return t('delete_revisions.delete_note_revisions'); } + static get actionName() { + return "deleteRevisions"; + } + static get actionTitle() { + return t("delete_revisions.delete_note_revisions"); + } doRender() { return $(TPL); diff --git a/src/public/app/widgets/bulk_actions/note/move_note.ts b/src/public/app/widgets/bulk_actions/note/move_note.ts index 440cd4bd6..7d1127012 100644 --- a/src/public/app/widgets/bulk_actions/note/move_note.ts +++ b/src/public/app/widgets/bulk_actions/note/move_note.ts @@ -7,12 +7,12 @@ const TPL = `
    -
    ${t('move_note.move_note')}
    +
    ${t("move_note.move_note")}
    -
    ${t('move_note.to')}
    +
    ${t("move_note.to")}
    - +
    @@ -20,12 +20,12 @@ const TPL = ` @@ -35,25 +35,29 @@ const TPL = ` `; export default class MoveNoteBulkAction extends AbstractBulkAction { - static get actionName() { return "moveNote"; } - static get actionTitle() { return t('move_note.move_note'); } + static get actionName() { + return "moveNote"; + } + static get actionTitle() { + return t("move_note.move_note"); + } doRender() { const $action = $(TPL); - const $targetParentNote = $action.find('.target-parent-note'); + const $targetParentNote = $action.find(".target-parent-note"); noteAutocompleteService.initNoteAutocomplete($targetParentNote); $targetParentNote.setNote(this.actionDef.targetParentNoteId); - $targetParentNote.on('autocomplete:closed', () => spacedUpdate.scheduleUpdate()); + $targetParentNote.on("autocomplete:closed", () => spacedUpdate.scheduleUpdate()); const spacedUpdate = new SpacedUpdate(async () => { await this.saveAction({ targetParentNoteId: $targetParentNote.getSelectedNoteId() }); - }, 1000) + }, 1000); - $targetParentNote.on('input', () => spacedUpdate.scheduleUpdate()); + $targetParentNote.on("input", () => spacedUpdate.scheduleUpdate()); return $action; } diff --git a/src/public/app/widgets/bulk_actions/note/rename_note.ts b/src/public/app/widgets/bulk_actions/note/rename_note.ts index 355992707..0f610ebcc 100644 --- a/src/public/app/widgets/bulk_actions/note/rename_note.ts +++ b/src/public/app/widgets/bulk_actions/note/rename_note.ts @@ -6,27 +6,27 @@ const TPL = `
    -
    ${t('rename_note.rename_note_title_to')}
    +
    ${t("rename_note.rename_note_title_to")}
    + placeholder="${t("rename_note.new_note_title")}" + title="${t("rename_note.click_help_icon")}"/>
    @@ -35,22 +35,26 @@ const TPL = ` `; export default class RenameNoteBulkAction extends AbstractBulkAction { - static get actionName() { return "renameNote"; } - static get actionTitle() { return t('rename_note.rename_note'); } + static get actionName() { + return "renameNote"; + } + static get actionTitle() { + return t("rename_note.rename_note"); + } doRender() { const $action = $(TPL); - const $newTitle = $action.find('.new-title'); + const $newTitle = $action.find(".new-title"); $newTitle.val(this.actionDef.newTitle || ""); const spacedUpdate = new SpacedUpdate(async () => { await this.saveAction({ - newTitle: $newTitle.val(), + newTitle: $newTitle.val() }); }, 1000); - $newTitle.on('input', () => spacedUpdate.scheduleUpdate()); + $newTitle.on("input", () => spacedUpdate.scheduleUpdate()); return $action; } diff --git a/src/public/app/widgets/bulk_actions/relation/add_relation.ts b/src/public/app/widgets/bulk_actions/relation/add_relation.ts index 6dfb6c06d..55efee54d 100644 --- a/src/public/app/widgets/bulk_actions/relation/add_relation.ts +++ b/src/public/app/widgets/bulk_actions/relation/add_relation.ts @@ -7,19 +7,19 @@ const TPL = `
    -
    ${t('add_relation.add_relation')}
    +
    ${t("add_relation.add_relation")}
    + title="${t("add_relation.allowed_characters")}"/> -
    ${t('add_relation.to')}
    +
    ${t("add_relation.to")}
    - +
    @@ -27,7 +27,7 @@ const TPL = ` @@ -36,20 +36,24 @@ const TPL = ` `; export default class AddRelationBulkAction extends AbstractBulkAction { - static get actionName() { return "addRelation"; } - static get actionTitle() { return t('add_relation.add_relation'); } + static get actionName() { + return "addRelation"; + } + static get actionTitle() { + return t("add_relation.add_relation"); + } doRender() { const $action = $(TPL); - const $relationName = $action.find('.relation-name'); + const $relationName = $action.find(".relation-name"); $relationName.val(this.actionDef.relationName || ""); - const $targetNote = $action.find('.target-note'); + const $targetNote = $action.find(".target-note"); noteAutocompleteService.initNoteAutocomplete($targetNote); $targetNote.setNote(this.actionDef.targetNoteId); - $targetNote.on('autocomplete:closed', () => spacedUpdate.scheduleUpdate()); + $targetNote.on("autocomplete:closed", () => spacedUpdate.scheduleUpdate()); const spacedUpdate = new SpacedUpdate(async () => { await this.saveAction({ @@ -58,8 +62,8 @@ export default class AddRelationBulkAction extends AbstractBulkAction { }); }, 1000); - $relationName.on('input', () => spacedUpdate.scheduleUpdate()); - $targetNote.on('input', () => spacedUpdate.scheduleUpdate()); + $relationName.on("input", () => spacedUpdate.scheduleUpdate()); + $targetNote.on("input", () => spacedUpdate.scheduleUpdate()); return $action; } diff --git a/src/public/app/widgets/bulk_actions/relation/delete_relation.ts b/src/public/app/widgets/bulk_actions/relation/delete_relation.ts index f382841a8..379f4760c 100644 --- a/src/public/app/widgets/bulk_actions/relation/delete_relation.ts +++ b/src/public/app/widgets/bulk_actions/relation/delete_relation.ts @@ -5,15 +5,15 @@ import { t } from "../../../services/i18n.js"; const TPL = ` - ${t('delete_relation.delete_relation')} + ${t("delete_relation.delete_relation")}
    + placeholder="${t("delete_relation.relation_name")}" + title="${t("delete_relation.allowed_characters")}"/>
    @@ -22,19 +22,23 @@ const TPL = ` `; export default class DeleteRelationBulkAction extends AbstractBulkAction { - static get actionName() { return "deleteRelation"; } - static get actionTitle() { return t('delete_relation.delete_relation'); } + static get actionName() { + return "deleteRelation"; + } + static get actionTitle() { + return t("delete_relation.delete_relation"); + } doRender() { const $action = $(TPL); - const $relationName = $action.find('.relation-name'); + const $relationName = $action.find(".relation-name"); $relationName.val(this.actionDef.relationName || ""); const spacedUpdate = new SpacedUpdate(async () => { await this.saveAction({ relationName: $relationName.val() }); }, 1000); - $relationName.on('input', () => spacedUpdate.scheduleUpdate()); + $relationName.on("input", () => spacedUpdate.scheduleUpdate()); return $action; } diff --git a/src/public/app/widgets/bulk_actions/relation/rename_relation.ts b/src/public/app/widgets/bulk_actions/relation/rename_relation.ts index a2b3e9886..5cb8dc35b 100644 --- a/src/public/app/widgets/bulk_actions/relation/rename_relation.ts +++ b/src/public/app/widgets/bulk_actions/relation/rename_relation.ts @@ -6,21 +6,21 @@ const TPL = `
    -
    ${t('rename_relation.rename_relation_from')}
    +
    ${t("rename_relation.rename_relation_from")}
    + title="${t("rename_relation.allowed_characters")}"/> -
    ${t('rename_relation.to')}
    +
    ${t("rename_relation.to")}
    + title="${t("rename_relation.allowed_characters")}"/>
    @@ -29,16 +29,20 @@ const TPL = ` `; export default class RenameRelationBulkAction extends AbstractBulkAction { - static get actionName() { return "renameRelation"; } - static get actionTitle() { return t('rename_relation.rename_relation'); } + static get actionName() { + return "renameRelation"; + } + static get actionTitle() { + return t("rename_relation.rename_relation"); + } doRender() { const $action = $(TPL); - const $oldRelationName = $action.find('.old-relation-name'); + const $oldRelationName = $action.find(".old-relation-name"); $oldRelationName.val(this.actionDef.oldRelationName || ""); - const $newRelationName = $action.find('.new-relation-name'); + const $newRelationName = $action.find(".new-relation-name"); $newRelationName.val(this.actionDef.newRelationName || ""); const spacedUpdate = new SpacedUpdate(async () => { @@ -48,8 +52,8 @@ export default class RenameRelationBulkAction extends AbstractBulkAction { }); }, 1000); - $oldRelationName.on('input', () => spacedUpdate.scheduleUpdate()); - $newRelationName.on('input', () => spacedUpdate.scheduleUpdate()); + $oldRelationName.on("input", () => spacedUpdate.scheduleUpdate()); + $newRelationName.on("input", () => spacedUpdate.scheduleUpdate()); return $action; } diff --git a/src/public/app/widgets/bulk_actions/relation/update_relation_target.ts b/src/public/app/widgets/bulk_actions/relation/update_relation_target.ts index f4b28d034..e839e8c41 100644 --- a/src/public/app/widgets/bulk_actions/relation/update_relation_target.ts +++ b/src/public/app/widgets/bulk_actions/relation/update_relation_target.ts @@ -7,19 +7,19 @@ const TPL = `
    -
    ${t('update_relation_target.update_relation')}
    +
    ${t("update_relation_target.update_relation")}
    + title="${t("update_relation_target.allowed_characters")}"/> -
    ${t('update_relation_target.to')}
    +
    ${t("update_relation_target.to")}
    - +
    @@ -27,11 +27,11 @@ const TPL = ` @@ -41,20 +41,24 @@ const TPL = ` `; export default class UpdateRelationTargetBulkAction extends AbstractBulkAction { - static get actionName() { return "updateRelationTarget"; } - static get actionTitle() { return t('update_relation_target.update_relation_target'); } + static get actionName() { + return "updateRelationTarget"; + } + static get actionTitle() { + return t("update_relation_target.update_relation_target"); + } doRender() { const $action = $(TPL); - const $relationName = $action.find('.relation-name'); + const $relationName = $action.find(".relation-name"); $relationName.val(this.actionDef.relationName || ""); - const $targetNote = $action.find('.target-note'); + const $targetNote = $action.find(".target-note"); noteAutocompleteService.initNoteAutocomplete($targetNote); $targetNote.setNote(this.actionDef.targetNoteId); - $targetNote.on('autocomplete:closed', () => spacedUpdate.scheduleUpdate()); + $targetNote.on("autocomplete:closed", () => spacedUpdate.scheduleUpdate()); const spacedUpdate = new SpacedUpdate(async () => { await this.saveAction({ @@ -63,8 +67,8 @@ export default class UpdateRelationTargetBulkAction extends AbstractBulkAction { }); }, 1000); - $relationName.on('input', () => spacedUpdate.scheduleUpdate()); - $targetNote.on('input', () => spacedUpdate.scheduleUpdate()); + $relationName.on("input", () => spacedUpdate.scheduleUpdate()); + $targetNote.on("input", () => spacedUpdate.scheduleUpdate()); return $action; } diff --git a/src/public/app/widgets/buttons/abstract_button.ts b/src/public/app/widgets/buttons/abstract_button.ts index 9b26c392e..c30c23593 100644 --- a/src/public/app/widgets/buttons/abstract_button.ts +++ b/src/public/app/widgets/buttons/abstract_button.ts @@ -16,7 +16,6 @@ export interface AbstractButtonWidgetSettings { } export default class AbstractButtonWidget extends NoteContextAwareWidget { - protected settings!: SettingsT; protected tooltip!: bootstrap.Tooltip; @@ -31,13 +30,13 @@ export default class AbstractButtonWidget this.getTitle(), - trigger: 'hover', + trigger: "hover", placement: this.settings.titlePlacement, - fallbackPlacements: [ this.settings.titlePlacement ] - }) + fallbackPlacements: [this.settings.titlePlacement] + }); if (this.settings.onContextMenu) { - this.$widget.on("contextmenu", e => { + this.$widget.on("contextmenu", (e) => { this.tooltip.hide(); if (this.settings.onContextMenu) { @@ -52,9 +51,7 @@ export default class AbstractButtonWidget
  • + title="${t("attachments_actions.open_externally_title")}"> ${t("attachments_actions.open_externally")}
  • + title="${t("attachments_actions.open_custom_title")}"> ${t("attachments_actions.open_custom")} + ${t("attachments_actions.download")} +
    ${t("attachments_actions.copy_link_to_clipboard")} + ${t("attachments_actions.upload_new_revision")} + ${t("attachments_actions.rename_attachment")} + ${t("attachments_actions.delete_attachment")} + ${t("attachments_actions.convert_attachment_into_note")}
    @@ -92,46 +92,34 @@ export default class AttachmentActionsWidget extends BasicWidget { doRender() { this.$widget = $(TPL); this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")); - this.$widget.on('click', '.dropdown-item', () => this.dropdown.toggle()); + this.$widget.on("click", ".dropdown-item", () => this.dropdown.toggle()); this.$uploadNewRevisionInput = this.$widget.find(".attachment-upload-new-revision-input"); - this.$uploadNewRevisionInput.on('change', async () => { + this.$uploadNewRevisionInput.on("change", async () => { const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below - this.$uploadNewRevisionInput.val(''); + this.$uploadNewRevisionInput.val(""); const result = await server.upload(`attachments/${this.attachmentId}/file`, fileToUpload); if (result.uploaded) { - toastService.showMessage(t('attachments_actions.upload_success')); + toastService.showMessage(t("attachments_actions.upload_success")); } else { - toastService.showError(t('attachments_actions.upload_failed')); + toastService.showError(t("attachments_actions.upload_failed")); } }); const isElectron = utils.isElectron(); if (!this.isFullDetail) { const $openAttachmentButton = this.$widget.find("[data-trigger-command='openAttachment']"); - $openAttachmentButton - .addClass("disabled") - .append($('') - .attr("title", t('attachments_actions.open_externally_detail_page')) - ); + $openAttachmentButton.addClass("disabled").append($('').attr("title", t("attachments_actions.open_externally_detail_page"))); if (isElectron) { const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']"); - $openAttachmentCustomButton - .addClass("disabled") - .append($('') - .attr("title", t('attachments_actions.open_externally_detail_page')) - ); + $openAttachmentCustomButton.addClass("disabled").append($('').attr("title", t("attachments_actions.open_externally_detail_page"))); } } if (!isElectron) { const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']"); - $openAttachmentCustomButton - .addClass("disabled") - .append($('') - .attr("title", t('attachments_actions.open_custom_client_only')) - ); + $openAttachmentCustomButton.addClass("disabled").append($('').attr("title", t("attachments_actions.open_custom_client_only"))); } } @@ -148,7 +136,7 @@ export default class AttachmentActionsWidget extends BasicWidget { } async uploadNewAttachmentRevisionCommand() { - this.$uploadNewRevisionInput.trigger('click'); + this.$uploadNewRevisionInput.trigger("click"); } async copyAttachmentLinkToClipboardCommand() { @@ -156,29 +144,29 @@ export default class AttachmentActionsWidget extends BasicWidget { } async deleteAttachmentCommand() { - if (!await dialogService.confirm(t('attachments_actions.delete_confirm', { title: this.attachment.title }))) { + if (!(await dialogService.confirm(t("attachments_actions.delete_confirm", { title: this.attachment.title })))) { return; } await server.remove(`attachments/${this.attachmentId}`); - toastService.showMessage(t('attachments_actions.delete_success', { title: this.attachment.title })); + toastService.showMessage(t("attachments_actions.delete_success", { title: this.attachment.title })); } async convertAttachmentIntoNoteCommand() { - if (!await dialogService.confirm(t('attachments_actions.convert_confirm', { title: this.attachment.title }))) { + if (!(await dialogService.confirm(t("attachments_actions.convert_confirm", { title: this.attachment.title })))) { return; } - const { note: newNote } = await server.post(`attachments/${this.attachmentId}/convert-to-note`) - toastService.showMessage(t('attachments_actions.convert_success', { title: this.attachment.title })); + const { note: newNote } = await server.post(`attachments/${this.attachmentId}/convert-to-note`); + toastService.showMessage(t("attachments_actions.convert_success", { title: this.attachment.title })); await ws.waitForMaxKnownEntityChangeId(); await appContext.tabManager.getActiveContext().setNote(newNote.noteId); } async renameAttachmentCommand() { const attachmentTitle = await dialogService.prompt({ - title: t('attachments_actions.rename_attachment'), - message: t('attachments_actions.enter_new_name'), + title: t("attachments_actions.rename_attachment"), + message: t("attachments_actions.enter_new_name"), defaultValue: this.attachment.title }); diff --git a/src/public/app/widgets/buttons/bookmark_folder.js b/src/public/app/widgets/buttons/bookmark_folder.js index e627208b5..7a7d46423 100644 --- a/src/public/app/widgets/buttons/bookmark_folder.js +++ b/src/public/app/widgets/buttons/bookmark_folder.js @@ -53,8 +53,8 @@ export default class BookmarkFolderWidget extends RightDropdownButtonWidget { doRender() { super.doRender(); - this.$parentNote = this.$dropdownContent.find('.parent-note'); - this.$childrenNotes = this.$dropdownContent.find('.children-notes'); + this.$parentNote = this.$dropdownContent.find(".parent-note"); + this.$childrenNotes = this.$dropdownContent.find(".children-notes"); } async dropdownShown() { @@ -66,19 +66,10 @@ export default class BookmarkFolderWidget extends RightDropdownButtonWidget { showNoteIcon: true }; - this.$parentNote.append( - (await linkService.createLink(this.note.noteId, linkOptions)) - .addClass("note-link") - ); + this.$parentNote.append((await linkService.createLink(this.note.noteId, linkOptions)).addClass("note-link")); for (const childNote of await this.note.getChildNotes()) { - this.$childrenNotes.append( - $("
  • ") - .append( - (await linkService.createLink(childNote.noteId, linkOptions)) - .addClass("note-link") - ) - ); + this.$childrenNotes.append($("
  • ").append((await linkService.createLink(childNote.noteId, linkOptions)).addClass("note-link"))); } } diff --git a/src/public/app/widgets/buttons/button_from_note.js b/src/public/app/widgets/buttons/button_from_note.js index 31d93195a..8e64acb41 100644 --- a/src/public/app/widgets/buttons/button_from_note.js +++ b/src/public/app/widgets/buttons/button_from_note.js @@ -28,26 +28,22 @@ export default class ButtonFromNoteWidget extends CommandButtonWidget { return; } - froca.getNote(buttonNoteId).then(note => { + froca.getNote(buttonNoteId).then((note) => { this.settings.icon = note.getIcon(); this.refreshIcon(); }); } - entitiesReloadedEvent({loadResults}) { + entitiesReloadedEvent({ loadResults }) { const buttonNote = froca.getNoteFromCache(this.buttonNoteIdProvider()); if (!buttonNote) { return; } - if (loadResults.getAttributeRows(this.componentId).find(attr => - attr.type === 'label' - && attr.name === 'iconClass' - && attributeService.isAffecting(attr, buttonNote))) { - + if (loadResults.getAttributeRows(this.componentId).find((attr) => attr.type === "label" && attr.name === "iconClass" && attributeService.isAffecting(attr, buttonNote))) { this.updateIcon(); } } -} \ No newline at end of file +} diff --git a/src/public/app/widgets/buttons/calendar.js b/src/public/app/widgets/buttons/calendar.js index 609f771a4..9c5d81507 100644 --- a/src/public/app/widgets/buttons/calendar.js +++ b/src/public/app/widgets/buttons/calendar.js @@ -40,7 +40,9 @@ const DROPDOWN_TPL = ` aria-expanded="false" data-calendar-input="month"> @@ -61,15 +63,7 @@ const DROPDOWN_TPL = `
    `; -const DAYS_OF_WEEK = [ - t("calendar.sun"), - t("calendar.mon"), - t("calendar.tue"), - t("calendar.wed"), - t("calendar.thu"), - t("calendar.fri"), - t("calendar.sat") -]; +const DAYS_OF_WEEK = [t("calendar.sun"), t("calendar.mon"), t("calendar.tue"), t("calendar.wed"), t("calendar.thu"), t("calendar.fri"), t("calendar.sat")]; export default class CalendarWidget extends RightDropdownButtonWidget { constructor(title, icon) { @@ -91,18 +85,18 @@ export default class CalendarWidget extends RightDropdownButtonWidget { e.stopPropagation(); }); this.monthDropdown = bootstrap.Dropdown.getOrCreateInstance(this.$monthSelect); - this.$dropdownContent.find('[data-calendar-input="month-list"] button').on("click", (e) => { + this.$dropdownContent.find('[data-calendar-input="month-list"] button').on("click", (e) => { this.date.setMonth(e.target.dataset.value); this.createMonth(); this.monthDropdown.hide(); }); this.$next = this.$dropdownContent.find('[data-calendar-toggle="next"]'); - this.$next.on('click', () => { + this.$next.on("click", () => { this.date.setMonth(this.date.getMonth() + 1); this.createMonth(); }); this.$previous = this.$dropdownContent.find('[data-calendar-toggle="previous"]'); - this.$previous.on('click', e => { + this.$previous.on("click", (e) => { this.date.setMonth(this.date.getMonth() - 1); this.createMonth(); }); @@ -114,34 +108,33 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.createMonth(); }); this.$nextYear = this.$dropdownContent.find('[data-calendar-toggle="nextYear"]'); - this.$nextYear.on('click', () => { + this.$nextYear.on("click", () => { this.date.setFullYear(this.date.getFullYear() + 1); this.createMonth(); }); this.$previousYear = this.$dropdownContent.find('[data-calendar-toggle="previousYear"]'); - this.$previousYear.on('click', e => { + this.$previousYear.on("click", (e) => { this.date.setFullYear(this.date.getFullYear() - 1); this.createMonth(); }); - this.$dropdownContent.find('.calendar-header').on("click", e => e.stopPropagation()); + this.$dropdownContent.find(".calendar-header").on("click", (e) => e.stopPropagation()); - this.$dropdownContent.on('click', '.calendar-date', async ev => { - const date = $(ev.target).closest('.calendar-date').attr('data-calendar-date'); + this.$dropdownContent.on("click", ".calendar-date", async (ev) => { + const date = $(ev.target).closest(".calendar-date").attr("data-calendar-date"); const note = await dateNoteService.getDayNote(date); if (note) { appContext.tabManager.getActiveContext().setNote(note.noteId); this.dropdown.hide(); - } - else { + } else { toastService.showError(t("calendar.cannot_find_day_note")); } ev.stopPropagation(); - }); - + }); + // Prevent dismissing the calendar popup by clicking on an empty space inside it. this.$dropdownContent.on("click", (e) => e.stopPropagation()); } @@ -175,10 +168,8 @@ export default class CalendarWidget extends RightDropdownButtonWidget { } createDay(dateNotesForMonth, num, day) { - const $newDay = $('
    ') - .addClass("calendar-date") - .attr('data-calendar-date', utils.formatDateISO(this.date)); - const $date = $('').html(num); + const $newDay = $("").addClass("calendar-date").attr("data-calendar-date", utils.formatDateISO(this.date)); + const $date = $("").html(num); // if it's the first day of the month if (num === 1) { @@ -187,24 +178,23 @@ export default class CalendarWidget extends RightDropdownButtonWidget { // 1 2 3 4 5 6 0 // Mo Tu We Th Fr Sa Su let dayOffset = day - this.firstDayOfWeek; - if (dayOffset < 0) - dayOffset = 7 + dayOffset; - $newDay.css("marginLeft", (dayOffset * 14.28) + '%'); + if (dayOffset < 0) dayOffset = 7 + dayOffset; + $newDay.css("marginLeft", dayOffset * 14.28 + "%"); } const dateNoteId = dateNotesForMonth[utils.formatDateISO(this.date)]; if (dateNoteId) { - $newDay.addClass('calendar-date-exists'); + $newDay.addClass("calendar-date-exists"); $newDay.attr("data-href", `#root/${dateNoteId}`); } if (this.isEqual(this.date, this.activeDate)) { - $newDay.addClass('calendar-date-active'); + $newDay.addClass("calendar-date-active"); } if (this.isEqual(this.date, this.todaysDate)) { - $newDay.addClass('calendar-date-today'); + $newDay.addClass("calendar-date-today"); } $newDay.append($date); @@ -212,13 +202,11 @@ export default class CalendarWidget extends RightDropdownButtonWidget { } isEqual(a, b) { - if (!a && b || a && !b) { + if ((!a && b) || (a && !b)) { return false; } - return a.getFullYear() === b.getFullYear() - && a.getMonth() === b.getMonth() - && a.getDate() === b.getDate(); + return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); } async createMonth() { @@ -229,12 +217,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget { const currentMonth = this.date.getMonth(); while (this.date.getMonth() === currentMonth) { - const $day = this.createDay( - dateNotesForMonth, - this.date.getDate(), - this.date.getDay(), - this.date.getFullYear() - ); + const $day = this.createDay(dateNotesForMonth, this.date.getDate(), this.date.getDay(), this.date.getFullYear()); this.$month.append($day); @@ -256,5 +239,4 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.manageFirstDayOfWeek(); this.createMonth(); } - } diff --git a/src/public/app/widgets/buttons/close_pane_button.js b/src/public/app/widgets/buttons/close_pane_button.js index b236894f0..c41bdfc10 100644 --- a/src/public/app/widgets/buttons/close_pane_button.js +++ b/src/public/app/widgets/buttons/close_pane_button.js @@ -3,12 +3,15 @@ import OnClickButtonWidget from "./onclick_button.js"; export default class ClosePaneButton extends OnClickButtonWidget { isEnabled() { - return super.isEnabled() + return ( + super.isEnabled() && // main note context should not be closeable - && this.noteContext && !!this.noteContext.mainNtxId; + this.noteContext && + !!this.noteContext.mainNtxId + ); } - async noteContextReorderEvent({ntxIdsInOrder}) { + async noteContextReorderEvent({ ntxIdsInOrder }) { this.refresh(); } diff --git a/src/public/app/widgets/buttons/command_button.ts b/src/public/app/widgets/buttons/command_button.ts index affe737c1..0bfb8625c 100644 --- a/src/public/app/widgets/buttons/command_button.ts +++ b/src/public/app/widgets/buttons/command_button.ts @@ -4,11 +4,11 @@ import AbstractButtonWidget, { AbstractButtonWidgetSettings } from "./abstract_b let actions: Action[]; -keyboardActionsService.getActions().then(as => actions = as); +keyboardActionsService.getActions().then((as) => (actions = as)); // TODO: Is this actually used? export type ClickHandler = (widget: CommandButtonWidget, e: JQuery.ClickEvent) => void; -type CommandOrCallback = (CommandNames | (() => CommandNames)); +type CommandOrCallback = CommandNames | (() => CommandNames); interface CommandButtonWidgetSettings extends AbstractButtonWidgetSettings { command?: CommandOrCallback; @@ -16,11 +16,10 @@ interface CommandButtonWidgetSettings extends AbstractButtonWidgetSettings { } export default class CommandButtonWidget extends AbstractButtonWidget { - constructor() { super(); this.settings = { - titlePlacement: 'right', + titlePlacement: "right", title: null, icon: null, onContextMenu: null @@ -46,7 +45,7 @@ export default class CommandButtonWidget extends AbstractButtonWidget act.actionName === this._command); + const action = actions.find((act) => act.actionName === this._command); if (action && action.effectiveShortcuts.length > 0) { return `${title} (${action.effectiveShortcuts.join(", ")})`; @@ -66,8 +65,6 @@ export default class CommandButtonWidget extends AbstractButtonWidget widget.triggerCommand("openNewNoteSplit", { ntxId: widget.getClosestNtxId() })) + .onClick((widget) => widget.triggerCommand("openNewNoteSplit", { ntxId: widget.getClosestNtxId() })) .class("icon-action"); } } diff --git a/src/public/app/widgets/buttons/edit_button.js b/src/public/app/widgets/buttons/edit_button.js index 50c490dc2..b55d44584 100644 --- a/src/public/app/widgets/buttons/edit_button.js +++ b/src/public/app/widgets/buttons/edit_button.js @@ -6,9 +6,7 @@ import { t } from "../../services/i18n.js"; export default class EditButton extends OnClickButtonWidget { isEnabled() { - return super.isEnabled() - && this.note - && this.noteContext.viewScope.viewMode === 'default'; + return super.isEnabled() && this.note && this.noteContext.viewScope.viewMode === "default"; } constructor() { @@ -17,10 +15,10 @@ export default class EditButton extends OnClickButtonWidget { this.icon("bx-edit-alt") .title(t("edit_button.edit_this_note")) .titlePlacement("bottom") - .onClick(widget => { + .onClick((widget) => { this.noteContext.viewScope.readOnlyTemporarilyDisabled = true; - appContext.triggerEvent('readOnlyTemporarilyDisabled', {noteContext: this.noteContext}); + appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext: this.noteContext }); this.refresh(); }); @@ -29,8 +27,7 @@ export default class EditButton extends OnClickButtonWidget { async refreshWithNote(note) { if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) { this.toggleInt(false); - } - else { + } else { // prevent flickering by assuming hidden before async operation this.toggleInt(false); @@ -53,19 +50,15 @@ export default class EditButton extends OnClickButtonWidget { await super.refreshWithNote(note); } - entitiesReloadedEvent({loadResults}) { - if (loadResults.getAttributeRows().find( - attr => attr.type === 'label' - && attr.name.toLowerCase().includes("readonly") - && attributeService.isAffecting(attr, this.note) - )) { + entitiesReloadedEvent({ loadResults }) { + if (loadResults.getAttributeRows().find((attr) => attr.type === "label" && attr.name.toLowerCase().includes("readonly") && attributeService.isAffecting(attr, this.note))) { this.noteContext.viewScope.readOnlyTemporarilyDisabled = false; this.refresh(); } } - async noteTypeMimeChangedEvent({noteId}) { + async noteTypeMimeChangedEvent({ noteId }) { if (this.isNote(noteId)) { await this.refresh(); } diff --git a/src/public/app/widgets/buttons/global_menu.ts b/src/public/app/widgets/buttons/global_menu.ts index d7893499f..1f906b1fd 100644 --- a/src/public/app/widgets/buttons/global_menu.ts +++ b/src/public/app/widgets/buttons/global_menu.ts @@ -108,13 +108,13 @@ const TPL = `